├── .gitignore ├── LICENSE ├── README.md ├── connector ├── client_factory.go ├── client_test.go ├── connector.go ├── connector_config.go ├── connector_socket.go ├── intermediate_request.go ├── plugin │ └── plugin.go └── ws_cli_farm_conn_info.go ├── cranker-http-request.png ├── cranker-websockets.png ├── go.mod ├── go.sum ├── performance_test.png ├── protocol └── cranker_protocol.go ├── router ├── api │ ├── darklaunch_gray_toggle_resource.go │ ├── darklaunch_ip_resource.go │ ├── darklaunch_service_resource.go │ ├── healthservice_resource.go │ ├── media_type │ │ └── media_type.go │ ├── registrations_resource.go │ ├── resource_interface.go │ └── response.go ├── corsheader_processor │ └── corsheader_processor.go ├── darklaunch_manager │ ├── darklaunch_manager.go │ └── darklaunch_toggle.go ├── handler │ ├── filter.go │ ├── handler.go │ └── websocket_handler.go ├── interceptor │ └── interceptor.go ├── plugin │ └── plugin.go ├── reverse_proxy.go ├── router.go ├── router_availability.go ├── router_config.go ├── router_socket │ ├── iterable_chan.go │ ├── router_socket.go │ └── websocket_farm.go ├── service │ └── health_service.go └── validator.go ├── test ├── e2etest │ ├── connector_manual_test.go │ ├── cranker_with_all_extention_single_service_test.go │ └── dryrun_router_test.go ├── init.go ├── scaffolding │ ├── connector_app.go │ ├── connector_health_service.go │ ├── contextualized_webserver.go │ ├── restfull_server.go │ ├── router_app.go │ ├── router_health_service.go │ ├── util.go │ └── util_test.go └── static │ ├── cert │ ├── client.key │ ├── client.pem │ ├── server.key │ └── server.pem │ ├── hello.html │ └── img │ └── nana.jpg └── util ├── catchall.go ├── catchall_test.go ├── connection_monitor.go ├── exception.go ├── log.go ├── shutdown.go └── toggle.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IDE configuration 18 | .vs 19 | .vscode 20 | .idea/ 21 | 22 | #go.sum 23 | #go.mod 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 torchcc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Crank4go API Gateway 3 | ======= 4 | 5 | Brief Introduction 6 | ----- 7 | It is a Golang implementation of [Crank4j](https://github.com/danielflower/crank4j), which derived from [Cranker](https://github.com/nicferrier/cranker). 8 | the follow introduction is quoted from the origin project: 9 | 10 | It consists of 2 executables that together act as a reverse proxy. What it 11 | allows is for cases where there is a firewall between your inside network and 12 | a [DMZ zone](https://en.wikipedia.org/wiki/DMZ_(computing)). 13 | 14 | For a normal reverse proxy, a port would need to be open allowing traffic from 15 | the DMZ to the internal network. Crank4j allows you to reverse this: Just open 16 | a port from the internal network to the DMZ, and Crank4j will tunnel HTTP traffic 17 | over the opened network. 18 | 19 | So there are two pieces: 20 | 21 | * **The router** that faces the internet, and accepts client HTTP requests 22 | * **The connector** that opens connections to the router, and then passes tunneled 23 | requests to target servers. 24 | 25 | The connections look like this: 26 | 27 | Browser | DMZ | Internal Network 28 | GET --------> router <-------- connector ---> HTTP Service 29 | | | 30 | 31 | But from the point of view of the browser, and your HTTP service, it just looks 32 | like a normal reverse proxy. 33 | 34 | 35 | Dive deeper 36 | --- 37 | 38 | this 2 picture are quoted from [Cranker](https://github.com/nicferrier/cranker) 39 | 40 | HTTP requests with cranker: 41 | ![cranker for http](cranker-http-request.png) 42 | 43 | Websockets with cranker: 44 | 45 | ![cranker for websockets](cranker-websockets.png) 46 | 47 | 48 | Running locally for test 49 | --------------- 50 | 51 | ### Running from an IDE 52 | 1. go mod tidy 53 | 2. open `test/e2etest/dryrun_router_test.go` run the test and a router will be started 54 | 3. open `test/e2etest/connector_manual_test.go`, a connector and a web-service will be started and connector to the router 55 | 4. open `https://localhost:9000` in your browser , it is the side facing to users 56 | - `https://localhost:9070/api/registrations` shows the registration status of router 57 | - `http://0.0.0.0:12439/health` shows the health status of router 58 | 59 | ### Use it in your project 60 | 61 | #### connector usage 62 | 0. `go get -v github.com/torchcc/crank4go` 63 | 1. create a web-service with a path prefix (context-path concept in java) e.g. `/my-service/...` 64 | 2. start the web-service listening on a random port 65 | 3. register your web server with one or more routers: 66 | ```go 67 | package main 68 | 69 | import ( 70 | "fmt" 71 | . "github.com/torchcc/crank4go/connector" 72 | "net/http" 73 | "net/url" 74 | ) 75 | func HelloHandler(w http.ResponseWriter, r *http.Request) { 76 | fmt.Fprintf(w, "Hello Crank4go") 77 | } 78 | 79 | func main() { 80 | targetURI, _ := url.Parse("http://localhost:5000") 81 | routerURI, _ := url.Parse("wss://localhost:9070") // should be the port which your Router Registration server listens on 82 | 83 | connectorConfig := NewConnectorConfig2(targetURI, "my-service", []*url.URL{routerURI}, "my-service-component-name", nil). 84 | SetSlidingWindowSize(2) 85 | _ = CreateAndStartConnector(connectorConfig) 86 | 87 | http.HandleFunc("/my-service", HelloHandler) 88 | http.ListenAndServe(":5000", nil) 89 | // and then you can query your api gateway to access your service. 90 | // e.g. if your router listens on https://localhost:9000, then you can access https://localhost:9000/my-service 91 | } 92 | 93 | ``` 94 | 95 | 96 | #### router usage 97 | 98 | 1. here is example of deploying the [API Gateway crank4go router](https://github.com/torchcc/crank4go-router) 99 | 2. if you want more functionalities, you might want to refer to `crank4go/test/e2etest/cranker_with_all_extention_single_service_test.go` 100 | 101 | 102 | Advantages 103 | ---- 104 | 1. Less dependencies: only use 4 (all of which are quite small and light ): 105 | `google/uuid`, `gorilla/websocket`, `julienschmidt/httprouter`, `op/go-logging` 106 | 2. Horizontally scalable architecture: 107 | we can deploy multiple Router instance on the loadBalancer side. 108 | Each web-service can be registered to multiple router 109 | 110 | 3. Multi-language supported: communication between router and connector is through websocket, so apart from crank4go-connector, 111 | other connectors written by other language can also be registered to Go-router. such as 112 | 1. [java](https://github.com/danielflower/crank4j/tree/master/crank4j-connector) 113 | 2. [python](https://github.com/torchcc/crank4py-connector) (making it convenient to play micro-service with python) 114 | 3. [java script](https://github.com/danielflower/npm-cranker-connector) 115 | 116 | 117 | 4. Hooks supported: hooks are preset in the form of plugin and interceptor to monitor the connection activity of router and connector. 118 | 119 | Performance 120 | --- 121 | As the picture shows, under the same testing condition on my local Macbook Pro 2018, 122 | the left side is crank4go router's performance, the right side is crank4j router's, the former is much better than the latter. 123 | 124 | ![performance_test](performance_test.png) 125 | 126 | ### TODO 127 | 1. add request rate control plugin. -------------------------------------------------------------------------------- /connector/client_factory.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "sync" 7 | "time" 8 | 9 | ws "github.com/gorilla/websocket" 10 | ) 11 | 12 | var ( 13 | once, once2 sync.Once 14 | httpClient *http.Client 15 | websocketDialer *ws.Dialer 16 | ) 17 | 18 | const ( 19 | // for websocket conn to read msg 20 | maxWebsocketMsgSize = 16384 21 | // WriteBufferSize for httpClient to write msg, use jetty's reverseProxy default value 22 | WriteBufferSize = 4 * 8192 23 | ) 24 | 25 | func GetHttpClient() *http.Client { 26 | once.Do(func() { 27 | tr := &http.Transport{ 28 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 29 | IdleConnTimeout: 2 * time.Hour, 30 | MaxConnsPerHost: 32768, 31 | WriteBufferSize: WriteBufferSize, 32 | DisableCompression: false, 33 | } 34 | httpClient = &http.Client{ 35 | Transport: tr, 36 | Timeout: 0, 37 | } 38 | }) 39 | return httpClient 40 | } 41 | 42 | func GetWebsocketDialer() *ws.Dialer { 43 | once2.Do(func() { 44 | websocketDialer = &ws.Dialer{ 45 | Proxy: http.ProxyFromEnvironment, 46 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 47 | HandshakeTimeout: 45 * time.Second, 48 | ReadBufferSize: 0, // default is 4096 49 | WriteBufferSize: 0, // default is 4096 50 | WriteBufferPool: &sync.Pool{}, // java version do not use pool 51 | EnableCompression: false, 52 | } 53 | }) 54 | return websocketDialer 55 | } 56 | -------------------------------------------------------------------------------- /connector/client_test.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func TestClient(t *testing.T) { 9 | client := GetHttpClient() 10 | resp, err := client.Get("https://localhost:8443") 11 | if err != nil { 12 | t.Errorf("failed, err: %s", err.Error()) 13 | return 14 | } 15 | defer resp.Body.Close() 16 | bytes, err := io.ReadAll(resp.Body) 17 | if err != nil { 18 | t.Errorf("failed, err: %s", err.Error()) 19 | return 20 | } 21 | t.Log(string(bytes)) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /connector/connector.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | 10 | ws "github.com/gorilla/websocket" 11 | . "github.com/torchcc/crank4go/connector/plugin" 12 | ptc "github.com/torchcc/crank4go/protocol" 13 | . "github.com/torchcc/crank4go/util" 14 | ) 15 | 16 | type State int 17 | 18 | const ( 19 | NotStarted State = iota // value --> 0 20 | RUNNING // value --> 1 21 | ShuttingDown // value --> 2 22 | ShutDown // value --> 3 23 | // idleTimeout = 60 * 1000 // mili second 24 | ) 25 | 26 | type Connector struct { 27 | slidingWindowSize int 28 | ParentCtx context.Context 29 | ParentCancel context.CancelFunc 30 | routerURIs []*url.URL 31 | targetURI *url.URL 32 | connMonitor *ConnectionMonitor 33 | websocketClientFarm *WebsocketClientFarm 34 | websocketDialer *ws.Dialer 35 | targetServiceName string 36 | connectorInstanceID string 37 | componentName string 38 | connectorPlugins []ConnectorPlugin 39 | state State 40 | } 41 | 42 | func NewConnector(routerURIs []*url.URL, targetURI *url.URL, targetServiceName string, slidingWindowSize int, 43 | connMonitor *ConnectionMonitor, connectorInstanceID string, componentName string, 44 | connectorPlugins []ConnectorPlugin) *Connector { 45 | 46 | c := &Connector{ 47 | slidingWindowSize: slidingWindowSize, 48 | routerURIs: routerURIs, 49 | targetURI: targetURI, 50 | connMonitor: connMonitor, 51 | websocketClientFarm: NewWebsocketClientFarm(slidingWindowSize), 52 | websocketDialer: GetWebsocketDialer(), 53 | targetServiceName: targetServiceName, 54 | connectorInstanceID: connectorInstanceID, 55 | componentName: componentName, 56 | state: NotStarted, 57 | } 58 | if connectorPlugins != nil { 59 | c.connectorPlugins = connectorPlugins 60 | } else { 61 | c.connectorPlugins = make([]ConnectorPlugin, 0, 0) 62 | } 63 | c.ParentCtx, c.ParentCancel = context.WithCancel(context.Background()) 64 | return c 65 | } 66 | 67 | func (c *Connector) ConnMonitor() *ConnectionMonitor { 68 | return c.connMonitor 69 | } 70 | func (c *Connector) Start() { 71 | for _, routerURI := range c.routerURIs { 72 | registerURI := routerURI.ResolveReference(&url.URL{ 73 | Path: "register/", 74 | RawQuery: fmt.Sprintf("connectorInstanceID=%s&componentName=%s", c.connectorInstanceID, c.componentName), 75 | }) 76 | LOG.Infof("Connecting to %s", registerURI.String()) 77 | for i := 0; i < c.slidingWindowSize; i++ { 78 | connInfo := NewConnectionInfo(registerURI, i) 79 | c.connectToRouter(registerURI, connInfo) 80 | c.websocketClientFarm.addWebsocket(registerURI.String()) 81 | c.connMonitor.OnConnectionAvailable() 82 | } 83 | } 84 | LOG.Infof("connector started for component=%s for path=/%s", c.componentName, c.targetServiceName) 85 | c.state = RUNNING 86 | } 87 | 88 | func (c *Connector) connectToRouter(registerURI *url.URL, connInfo *ConnectionInfo) { 89 | LOG.Debugf("connecting to router, registerURI is %s", registerURI) 90 | socket := NewConnectorSocket(registerURI, c.targetURI, c.connMonitor, connInfo, c.connectorPlugins, c.websocketClientFarm, c.componentName, c.ParentCtx) 91 | 92 | // whenConsumedAction 93 | runnable := func() { 94 | c.connMonitor.OnConnectionConsumed() 95 | if c.state == ShuttingDown || c.state == ShutDown { 96 | LOG.Infof("connector {%s} will not reconnect to router as it is being shut down", c.connectorInstanceID) 97 | } else if c.websocketClientFarm.isSafeToAddWebsocket(registerURI) { 98 | LOG.Debugf("connector {%s} is adding another connectorSocket...", c.connectorInstanceID) 99 | c.connectToRouter(socket.RegisterURI(), connInfo) 100 | c.websocketClientFarm.addWebsocket(socket.RegisterURI().String()) 101 | c.connMonitor.OnConnectionAvailable() 102 | } else { 103 | LOG.Warningf("unexpected error happened, no websocket will be added for connector {%s}, current websocketClientFarm is {%#v}", c.connectorInstanceID, c.websocketClientFarm) 104 | } 105 | } 106 | 107 | socket.WhenConsumed(runnable) 108 | 109 | if c.state != ShutDown { 110 | connInfo.OnConnectionStarting() 111 | headers := make(http.Header) 112 | headers.Add("CrankerProtocol", ptc.CrankerProtocolVersion10) 113 | headers.Add("Route", c.targetServiceName) 114 | go func() { 115 | if conn, _, err := c.websocketDialer.Dial(registerURI.String(), headers); err != nil { 116 | LOG.Errorf("cannot replace socket for %s, err: %s", registerURI, err.Error()) 117 | socket.OnWebsocketError(err) 118 | } else { 119 | LOG.Debugf("connected to router, register url: %s", registerURI) 120 | socket.OnWebsocketConnect(conn) 121 | } 122 | }() 123 | } 124 | } 125 | 126 | // IdleWebsocketFarmInfo return websocketClientFarm info as Map: 127 | func (c *Connector) IdleWebsocketFarmInfo() map[string]int { 128 | return c.websocketClientFarm.ToMap() 129 | } 130 | 131 | // ShutDown shutdown the connection to the router, waiting up to 20 seconds for existing requests to complete 132 | // any requests not finished in that time will be terminated. 133 | func (c *Connector) ShutDown() { 134 | c.ShutDownAfterTimeout(30 * time.Second) 135 | } 136 | 137 | // ShutDownAfterTimeout gracefully shuts down the connection to the router, any in-flight requests will continue to be process 138 | // until the timeout limit. this method will block until all requests are finished or the timeout is reached. 139 | // so after it returns you can shutdown your webServer. Note: on timeout, false will be returned as an indicator 140 | // if there are some remaining requests terminated due to timeout 141 | func (c *Connector) ShutDownAfterTimeout(timeout time.Duration) bool { 142 | endTime := time.Now().Add(timeout) 143 | if c.state != ShutDown { 144 | c.callShutDown() 145 | } 146 | for { 147 | if c.connMonitor.ConnectionCount() == 0 { 148 | return true 149 | } 150 | if time.Now().After(endTime) { 151 | return false 152 | } 153 | time.Sleep(50 * time.Millisecond) 154 | } 155 | } 156 | 157 | func (c *Connector) callShutDown() { 158 | c.state = ShuttingDown 159 | 160 | c.ParentCancel() // cancel alive pingTask, stop reconnectOnError mechanism 161 | for _, routerURI := range c.routerURIs { 162 | deRegisterURI := routerURI.ResolveReference(&url.URL{ 163 | Path: "deregister/", 164 | RawQuery: fmt.Sprintf("connectorInstanceID=%s&componentName=%s", c.connectorInstanceID, c.componentName), 165 | }) 166 | LOG.Infof("disconnecting... deregister URL: %s", deRegisterURI) 167 | deRegisterInfo := NewConnectionInfo(deRegisterURI, 0) 168 | c.connectToRouter(deRegisterURI, deRegisterInfo) 169 | } 170 | LOG.Infof("After deregister to router, AvailableConnections=%d, ConnectionCount=%d, OpenFiles=%d", 171 | c.connMonitor.AvailableConns(), c.connMonitor.ConnectionCount(), c.connMonitor.OpenFiles()) 172 | 173 | c.state = ShutDown 174 | } 175 | 176 | // program shutdown hooks 177 | var shutDownHooks []func() 178 | 179 | func addShutDownHook(f func()) { 180 | if shutDownHooks == nil { 181 | shutDownHooks = make([]func(), 0, 8) 182 | } 183 | shutDownHooks = append(shutDownHooks, f) 184 | } 185 | 186 | func exitFunc() { 187 | for _, f := range shutDownHooks { 188 | f() 189 | } 190 | } 191 | 192 | func init() { 193 | ExitProgram(exitFunc) 194 | } 195 | 196 | func CreateAndStartConnector(c *ConnectorConfig) *Connector { 197 | connMonitor := NewConnectionMonitor(c.dataPublishHandlers) 198 | connector := NewConnector(c.RouterURIs(), c.TargetURI(), c.TargetServiceName(), c.SlidingWindowSize(), 199 | connMonitor, c.InstanceID(), c.ComponentName(), c.Plugins()) 200 | connector.Start() 201 | if c.IsShutDownHookAdded() { 202 | addShutDownHook(connector.ShutDown) 203 | } 204 | return connector 205 | } 206 | -------------------------------------------------------------------------------- /connector/connector_config.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/google/uuid" 7 | "github.com/torchcc/crank4go/connector/plugin" 8 | "github.com/torchcc/crank4go/util" 9 | ) 10 | 11 | type ConnectorConfig struct { 12 | slidingWindowSize int 13 | isShutDownHookAdded bool 14 | targetServiceName string 15 | instanceID string 16 | componentName string 17 | plugins []plugin.ConnectorPlugin 18 | routerURIs []*url.URL 19 | dataPublishHandlers []util.DataPublishHandler 20 | targetURI *url.URL 21 | } 22 | 23 | func NewConnectorConfig(targetURI *url.URL, targetServiceName string, routerURIs []*url.URL, componentName string) *ConnectorConfig { 24 | return NewConnectorConfig2(targetURI, targetServiceName, routerURIs, componentName, make([]plugin.ConnectorPlugin, 0, 0)) 25 | } 26 | 27 | func NewConnectorConfig2(targetURI *url.URL, targetServiceName string, routerURIs []*url.URL, componentName string, plugins []plugin.ConnectorPlugin) *ConnectorConfig { 28 | return &ConnectorConfig{ 29 | slidingWindowSize: 2, 30 | isShutDownHookAdded: false, 31 | targetServiceName: targetServiceName, 32 | instanceID: uuid.New().String(), 33 | componentName: componentName, 34 | plugins: plugins, 35 | routerURIs: routerURIs, 36 | dataPublishHandlers: make([]util.DataPublishHandler, 0, 0), 37 | targetURI: targetURI, 38 | } 39 | 40 | } 41 | 42 | func (c *ConnectorConfig) SetIsShutDownHookAdded(isAddShutDownHook bool) *ConnectorConfig { 43 | c.isShutDownHookAdded = isAddShutDownHook 44 | return c 45 | } 46 | 47 | func (c *ConnectorConfig) IsShutDownHookAdded() bool { 48 | return c.isShutDownHookAdded 49 | } 50 | 51 | func (c *ConnectorConfig) DataPublishHandlers() []util.DataPublishHandler { 52 | return c.dataPublishHandlers 53 | } 54 | 55 | // SetDataPublishHandlers you can subscribe to this to see various metrics 56 | func (c *ConnectorConfig) SetDataPublishHandlers(dataPublishHandlers []util.DataPublishHandler) *ConnectorConfig { 57 | if dataPublishHandlers != nil { 58 | c.dataPublishHandlers = dataPublishHandlers 59 | } 60 | return c 61 | } 62 | 63 | func (c *ConnectorConfig) Plugins() []plugin.ConnectorPlugin { 64 | return c.plugins 65 | } 66 | 67 | func (c *ConnectorConfig) TargetURI() *url.URL { 68 | return c.targetURI 69 | } 70 | 71 | func (c *ConnectorConfig) TargetServiceName() string { 72 | return c.targetServiceName 73 | } 74 | 75 | func (c *ConnectorConfig) RouterURIs() []*url.URL { 76 | return c.routerURIs 77 | } 78 | 79 | func (c *ConnectorConfig) SlidingWindowSize() int { 80 | return c.slidingWindowSize 81 | } 82 | 83 | // SetSlidingWindowSize controls the idle socket windows of the pool size. please do not set this parameter unless you understand you need more 84 | // max_value is 100 85 | func (c *ConnectorConfig) SetSlidingWindowSize(slidingWindowSize int) *ConnectorConfig { 86 | c.slidingWindowSize = 2 87 | if slidingWindowSize > 0 && slidingWindowSize <= 1000 { 88 | c.slidingWindowSize = slidingWindowSize 89 | } 90 | return c 91 | } 92 | 93 | func (c *ConnectorConfig) ComponentName() string { 94 | return c.componentName 95 | } 96 | 97 | // InstanceID returns a unique ID for this instance of the connector, This is visible to the router 98 | func (c *ConnectorConfig) InstanceID() string { 99 | return c.instanceID 100 | } 101 | -------------------------------------------------------------------------------- /connector/connector_socket.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/google/uuid" 14 | ws "github.com/gorilla/websocket" 15 | . "github.com/torchcc/crank4go/connector/plugin" 16 | . "github.com/torchcc/crank4go/protocol" 17 | . "github.com/torchcc/crank4go/util" 18 | ) 19 | 20 | // if the msg are not written back within this time span, the writing will be aborted 21 | const writeWait = time.Second 22 | 23 | type ConnectorSocket struct { 24 | SockId uuid.UUID 25 | httpClient *http.Client 26 | targetURI *url.URL 27 | registerURI *url.URL 28 | connMonitor *ConnectionMonitor 29 | connInfo *ConnectionInfo 30 | createTime int64 31 | session *ws.Conn 32 | requestToTarget *IntermediateRequest 33 | whenConsumedAction func() 34 | websocketClientFarm *WebsocketClientFarm 35 | componentName string 36 | targetRequestContentProvider *io.PipeReader 37 | targetRequestContentWriter *io.PipeWriter 38 | cancelPing context.CancelFunc // it's used to cancelPing pingTask when a socket dies naturally. it is idempotent 39 | parentCtx context.Context // comes from the connector, use to stop pingTask and prevent error-reconnect-mechanism 40 | requestComplete bool // does it need to guarantee atomicity ? 41 | newSocketAdded bool 42 | hadError bool 43 | plugins []ConnectorPlugin 44 | } 45 | 46 | func NewConnectorSocket(sourceURI *url.URL, targetURI *url.URL, connMonitor *ConnectionMonitor, 47 | connInfo *ConnectionInfo, plugins []ConnectorPlugin, websocketClientFarm *WebsocketClientFarm, 48 | componentName string, parentCtx context.Context) *ConnectorSocket { 49 | s := &ConnectorSocket{ 50 | SockId: uuid.New(), 51 | httpClient: GetHttpClient(), 52 | targetURI: targetURI, 53 | registerURI: sourceURI, 54 | connMonitor: connMonitor, 55 | connInfo: connInfo, 56 | createTime: time.Now().Unix(), 57 | websocketClientFarm: websocketClientFarm, 58 | componentName: componentName, 59 | parentCtx: parentCtx, 60 | } 61 | if plugins != nil { 62 | s.plugins = plugins 63 | } else { 64 | s.plugins = make([]ConnectorPlugin, 0, 0) 65 | } 66 | return s 67 | } 68 | 69 | func (s *ConnectorSocket) WhenConsumed(runnable func()) { 70 | s.whenConsumedAction = runnable 71 | } 72 | 73 | func (s *ConnectorSocket) RegisterURI() *url.URL { 74 | return s.registerURI 75 | } 76 | 77 | // OnWebsocketConnect A Websocket Session has connected successfully and is ready to be used. 78 | func (s *ConnectorSocket) OnWebsocketConnect(conn *ws.Conn) { 79 | s.session = conn 80 | LOG.Debugf("connected to %s, sockId= %s", conn.RemoteAddr().String(), s.SockId.String()) 81 | 82 | var count int64 83 | ctx, cancel := context.WithCancel(context.Background()) 84 | s.cancelPing = cancel 85 | s.session.SetReadLimit(maxWebsocketMsgSize) 86 | go func(ctx, parentCtx context.Context) { 87 | LOOP: 88 | for { 89 | if s.session == nil { 90 | LOG.Infof("cancel ping task because the websocket is closed, sockID is :%s", s.SockId.String()) 91 | return 92 | } 93 | select { 94 | case <-ctx.Done(): 95 | LOG.Infof("the socket itself dies , cancelling its pingTask, sockID is: %s", s.SockId.String()) 96 | break LOOP 97 | case <-parentCtx.Done(): 98 | LOG.Infof("the connectorApp is shutting down, cancelling its pingTask, sockID is: %s", s.SockId.String()) 99 | break LOOP 100 | case <-time.After(5 * time.Second): 101 | } 102 | s.session.WriteControl(ws.PingMessage, []byte( 103 | fmt.Sprintf("send the %dth ping to %s for sockId= %s", 104 | atomic.AddInt64(&count, 1), s.session.LocalAddr().String(), s.SockId.String())), time.Now().Add(writeWait)) 105 | } 106 | 107 | }(ctx, s.parentCtx) 108 | s.runForever(conn) 109 | } 110 | 111 | func (s *ConnectorSocket) OnWebsocketBinary(payload []byte) { 112 | if n, err := s.targetRequestContentWriter.Write(payload); err != nil { 113 | LOG.Warningf("failed to feed request content to target, sockId: %s, request: %s, err: %s", s.SockId, s.requestToTarget.url, err.Error()) 114 | } else { 115 | LOG.Debugf("content added: %s", payload[:n]) 116 | } 117 | } 118 | 119 | func (s *ConnectorSocket) OnWebsocketText(msg string) { 120 | ptcReq := NewCrankerProtocolRequest(msg) 121 | 122 | LOG.Debugf("connectorSocket %s receive msg from routerSocket, request is %s", s.SockId, ptcReq.ToProtocolMessage()) 123 | if IsDebugReq(ptcReq) { 124 | LOG.Infof("onWebsocketText -> connectorSocket %s receive msg from routerSocket, request is %s", s.SockId, ptcReq.ToProtocolMessage()) 125 | } 126 | 127 | // fire the req to target only when endMarker is RequestHasNoBodyMarker: str = "_2" or RequestBodyEndedMarker: str = "_1" 128 | if s.requestToTarget == nil { 129 | s.onRequestReceived() 130 | s.newRequestToTarget(ptcReq) 131 | s.sendRequestToTarget(ptcReq) 132 | } else if ptcReq.RequestBodyEnded() { 133 | LOG.Debugf("there will be no more request body coming. sockId: %s", s.SockId) 134 | _ = s.targetRequestContentWriter.Close() 135 | } 136 | } 137 | 138 | func (s *ConnectorSocket) onRequestReceived() { 139 | LOG.Debugf("connectorSocket %s connected with connectionInfo %s", s.SockId, s.connInfo) 140 | s.websocketClientFarm.removeWebsocket(s.RegisterURI().String()) 141 | s.connInfo.OnConnectedSuccessfully() 142 | s.whenConsumedAction() 143 | s.newSocketAdded = true 144 | } 145 | 146 | func (s *ConnectorSocket) newRequestToTarget(ptcReq *CrankerProtocolRequest) { 147 | if IsDebugReq(ptcReq) { 148 | LOG.Infof("newRequestToTarget -> connector receive msg from router socket, request is %s", ptcReq.ToProtocolMessage()) 149 | } 150 | ptcResp := new(CrankerProtocolResponseBuilder).WithSourceUrl(ptcReq.Dest).WithHttpMethod(ptcReq.HttpMethod) 151 | reqDest, _ := url.Parse(ptcReq.Dest) 152 | dest := s.targetURI.ResolveReference(reqDest) 153 | LOG.Infof("going to send %s to %s and component is %s", ptcReq, dest, s.componentName) 154 | carriers := NewConnectorPluginStatCarriers() 155 | _ = s.handlePluginsBeforeRequestSent(ptcReq, carriers) 156 | s.requestToTarget = NewIntermediateRequest(dest.String()).Method(ptcReq.HttpMethod).Agent("").WithWebsocketSession(s.session) // use the client's agent rather than golang agent 157 | 158 | putHeadersTo(s.requestToTarget, ptcReq) 159 | 160 | onResponseBegin := func(resp *http.Response) { 161 | // handle response line 162 | ptcResp.WithRespStatus(resp.StatusCode).WithRespReason(resp.Status) 163 | // handler response Headers 164 | LOG.Debugf("golang's httpClient finished its job and here is response. request: %s, method: %s", dest, ptcReq.HttpMethod) 165 | ptcResp.WithRespHeaders(parseHeaders(resp.Header)) 166 | ptcRespMsg := NewCrankerProtocolResponse(ptcResp.Build()) 167 | LOG.Debugf("going to send response to cranker router. response: %s, request: %s, method: %s", ptcRespMsg.ToProtocolMessage(), dest, ptcReq.HttpMethod) 168 | if IsDebugResp(ptcRespMsg) { 169 | LOG.Infof("onResponseHeaders -> connector receive msg from target service, response is %s", ptcRespMsg.ToProtocolMessage()) 170 | } 171 | _ = s.handlePluginsAfterResponseReceived(ptcRespMsg, carriers) 172 | if err := s.session.WriteMessage(ws.TextMessage, []byte(ptcRespMsg.ToProtocolMessage())); err != nil { 173 | LOG.Errorf("failed to send response header back to router through websocket, request: %s, err: %s", dest, err.Error()) 174 | } 175 | } 176 | s.requestToTarget.WithResponseBeginHandler(onResponseBegin) 177 | } 178 | 179 | func (s *ConnectorSocket) sendRequestToTarget(ptcReq *CrankerProtocolRequest) { 180 | if !ptcReq.RequestBodyPending() && !ptcReq.RequestHasNoBody() { 181 | return 182 | } 183 | 184 | if ptcReq.RequestBodyPending() { 185 | s.targetRequestContentProvider, s.targetRequestContentWriter = io.Pipe() 186 | s.requestToTarget.Content(s.targetRequestContentProvider) 187 | LOG.Debugf("request body pending, sockId=%s", s.SockId) 188 | } 189 | 190 | LOG.Debug("request Headers are received") 191 | s.connMonitor.OnConnectionStarted() 192 | callback := func(result *result) { 193 | LOG.Debugf("connectorSocket got response from target service and finish sending back to routerSocket,sockId: %s, request: %s", s.SockId, s.requestToTarget.url) 194 | s.connMonitor.OnConnectionEnded() 195 | // cancelPing first 196 | if s.cancelPing != nil { 197 | LOG.Debugf("target service response finished, socket's cancelPing is not nil, so it is going to be cancelled, sockID is %s", s.SockId.String()) 198 | s.cancelPing() 199 | } 200 | if result.isSucceeded { 201 | s.requestComplete = true 202 | LOG.Debugf("closing websocket because response is fully processed, sockId: %s, request: %s", s.SockId, s.requestToTarget.url) 203 | if s.session != nil { 204 | if err := s.session.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseNormalClosure, "Proxy complete"), time.Now().Add(writeWait)); err != nil { 205 | LOG.Errorf("failed to close websocket connection normally from ws client side, err: %s", err.Error()) 206 | } 207 | } 208 | } else { 209 | s.requestComplete = false 210 | errorID := uuid.New().String() 211 | if _, ok := result.failure.(CancelErr); !ok { 212 | LOG.Warningf("failed for %s, ErrorID: %s, err: %s", result.response, errorID, result.failure.Error()) 213 | } 214 | // nil means onWebsocketClose has been invoked, clientServer close the conn actively,cancelError, not nil means connector side error 215 | if s.session != nil { 216 | if err := s.session.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseInternalServerErr, "ErrorID: "+errorID), time.Now().Add(writeWait)); err != nil { 217 | LOG.Errorf("failed to close websocket connection normally from ws client side, err: %s", err.Error()) 218 | } 219 | } 220 | } 221 | } 222 | go s.requestToTarget.FireRequestFromConnectorToTargetService(callback) 223 | LOG.Debug("request body is fully sent") 224 | } 225 | 226 | func putHeadersTo(reqToTarget *IntermediateRequest, ptcReq *CrankerProtocolRequest) { 227 | if IsDebugReq(ptcReq) { 228 | LOG.Infof("putHeadersTo -> connector receive msg from router socket, request is %s", ptcReq.ToProtocolMessage()) 229 | } 230 | for _, line := range ptcReq.Headers { 231 | if pos := strings.Index(line, ":"); pos > 0 { 232 | header := line[0:pos] 233 | value := line[pos+1:] 234 | var h string 235 | if header == "Authorization" { 236 | h = strings.Split(value, ":")[0] 237 | } else { 238 | h = value 239 | } 240 | LOG.Debugf("target request header %s = %s", header, h) 241 | reqToTarget.Headers.Add(header, value) 242 | } 243 | } 244 | reqToTarget.Headers.Add("Via", "1.1 crnk") 245 | } 246 | 247 | func parseHeaders(h http.Header) *HeadersBuilder { 248 | hb := new(HeadersBuilder) 249 | for k, vs := range h { 250 | for _, v := range vs { 251 | hb.AppendHeader(k, v) 252 | } 253 | } 254 | return hb 255 | } 256 | 257 | type StatCarrier struct { 258 | } 259 | 260 | func (c *StatCarrier) Close() error { 261 | return nil 262 | } 263 | 264 | func (c *StatCarrier) GetStat() interface{} { 265 | return 0 266 | } 267 | 268 | func (s *ConnectorSocket) handlePluginsBeforeRequestSent(req *CrankerProtocolRequest, carriers *ConnectorPluginStatCarriers) (err error) { 269 | for _, p := range s.plugins { 270 | if carrier, err := p.HandleBeforeRequestSent(req, &StatCarrier{}); err != nil { 271 | LOG.Errorf("failed to apply handleBeforeRequestSent for plugin [%s] with protocol request %s, err: %s", p, req, err.Error()) 272 | return err 273 | } else { 274 | carriers.Put(p, carrier) 275 | } 276 | } 277 | return 278 | } 279 | 280 | func (s *ConnectorSocket) handlePluginsAfterResponseReceived(resp *CrankerProtocolResponse, carriers *ConnectorPluginStatCarriers) (err error) { 281 | defer carriers.Close() 282 | for _, p := range s.plugins { 283 | carrier := carriers.Get(p) 284 | if carrierAfterResponse, err := p.HandleAfterResponseReceived(resp, carrier); err != nil { 285 | LOG.Errorf("failed to apply handleAfterResponseReceived for plugin [%s] with protocol response %s, err: %s", p, resp, err.Error()) 286 | carriers.CleanCarrier(p) 287 | return err 288 | } else if err = carrierAfterResponse.Close(); err != nil { 289 | LOG.Errorf("failed to close carrierAfterResponse %s for plugin [%s] with protocol response %s, err: %s", p, resp, err.Error()) 290 | carriers.CleanCarrier(p) 291 | return err 292 | } 293 | } 294 | return 295 | } 296 | 297 | // A Close Event was received. if we set this hook, we need to response a CloseMessage back to websocket client. 298 | func (s *ConnectorSocket) OnWebsocketClose(statusCode int, reason string) error { 299 | LOG.Debugf("connection with sockId %s, closed, statusCode: %s, reason: %s", s.SockId, statusCode, reason) 300 | s.clean() 301 | if s.cancelPing != nil { 302 | LOG.Debugf("OnWebsocketClose, socket's cancelPing is not nil, so it is going to be cancelled, sockID is %s", s.SockId.String()) 303 | s.cancelPing() 304 | s.cancelPing = nil 305 | } 306 | if !s.newSocketAdded { 307 | LOG.Debugf("going to reconnect to router, the dying conn's sockId: %s, close code: %d", s.SockId, statusCode) 308 | s.websocketClientFarm.removeWebsocket(s.RegisterURI().String()) 309 | s.whenConsumedAction() 310 | s.newSocketAdded = true 311 | } 312 | if !s.requestComplete && s.requestToTarget != nil { 313 | if statusCode != ws.CloseInternalServerErr { 314 | LOG.Infof("the websocket closed before the target response was processed, this may be because the user closed their browser."+ 315 | "going to cancel request to target %s", s.requestToTarget.url) 316 | s.requestToTarget.Abort(CancelErr{Msg: "the websocket session to router is close"}) 317 | } 318 | } 319 | if s.session != nil { 320 | LOG.Debugf("OnWebsocketClose. Replying CloseMessage from client side.. ") 321 | return s.session.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(statusCode, ""), time.Now().Add(writeWait)) 322 | } 323 | return nil 324 | } 325 | 326 | // this func will be invoked under 2 circumstances: 327 | // 1. websocket connection has been built but error happened when reading msg from conn 328 | // 2. failed to dial a connection. 329 | func (s *ConnectorSocket) OnWebsocketError(cause error) { 330 | if s.hadError { 331 | LOG.Infof("received error, connectionInfo=%s, but it was already handed, so ignoring it.", s.connInfo.String()) 332 | return 333 | } 334 | LOG.Debug("going to remove websocket as websocket error") 335 | s.hadError = true 336 | LOG.Warningf("websocket error, %s to %s - Error: %s", s.connInfo, s.targetURI, cause.Error()) 337 | if s.cancelPing != nil { 338 | LOG.Debugf("OnWebsocketError, socket's cancelPing is not nil, so it is going to be cancelled, sockID is %s", s.SockId.String()) 339 | s.cancelPing() 340 | s.cancelPing = nil 341 | } 342 | if !s.newSocketAdded { 343 | s.websocketClientFarm.removeWebsocket(s.RegisterURI().String()) 344 | delay := s.connInfo.RetryAfterMillis() 345 | LOG.Infof("going to reconnect to router after %d ms", delay) 346 | go func() { 347 | select { 348 | case <-s.parentCtx.Done(): // if parent from the connector says, shutdown, new connectorSocket are not allow to be added 349 | return 350 | default: 351 | } 352 | time.AfterFunc(time.Duration(delay)*time.Millisecond, func() { 353 | s.whenConsumedAction() 354 | s.newSocketAdded = true 355 | }) 356 | }() 357 | s.clean() 358 | } 359 | } 360 | 361 | func (s *ConnectorSocket) clean() { 362 | sessionToClean := s.session 363 | if sessionToClean != nil { 364 | if err := sessionToClean.Close(); err != nil { // close the underlying conn without sending or waiting for a close msg. 365 | LOG.Warningf("failed to close the underlying net.conn connection, sockId: %s, err: %s", s.SockId, err.Error()) 366 | } 367 | } 368 | s.session = nil 369 | } 370 | 371 | func (s *ConnectorSocket) runForever(conn *ws.Conn) { 372 | for { 373 | // msgType must be either TextMessage or BinaryMessage 374 | msgType, msg, err := conn.ReadMessage() 375 | if err != nil { 376 | if closeErr, ok := err.(*ws.CloseError); !ok { 377 | s.OnWebsocketError(err) 378 | } else { 379 | s.OnWebsocketClose(closeErr.Code, closeErr.Text) 380 | } 381 | return 382 | } 383 | if msgType == ws.TextMessage { 384 | s.OnWebsocketText(string(msg)) 385 | } else if msgType == ws.BinaryMessage { 386 | s.OnWebsocketBinary(msg) 387 | } else { 388 | LOG.Errorf("unexpected msgType got: %d", msgType) 389 | } 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /connector/intermediate_request.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | ws "github.com/gorilla/websocket" 10 | . "github.com/torchcc/crank4go/util" 11 | ) 12 | 13 | type result struct { 14 | isSucceeded bool 15 | failure error 16 | response *http.Response 17 | } 18 | 19 | type IntermediateRequest struct { 20 | method string 21 | Headers http.Header 22 | url string 23 | client *http.Client 24 | contentProvider *io.PipeReader 25 | onResponseBegin func(resp *http.Response) 26 | websocketSession *ws.Conn 27 | result *result 28 | } 29 | 30 | func NewIntermediateRequest(url string) *IntermediateRequest { 31 | return &IntermediateRequest{ 32 | url: url, 33 | client: GetHttpClient(), 34 | Headers: http.Header{}, 35 | result: &result{}, 36 | } 37 | } 38 | 39 | // Agent set user agent. we need to set user agent to "" so that golang's http client will not be used and the origin user-agent will be use 40 | func (r *IntermediateRequest) Agent(agent string) *IntermediateRequest { 41 | r.Headers.Add("User-Agent", agent) 42 | return r 43 | } 44 | 45 | // Method set http method 46 | func (r *IntermediateRequest) Method(method string) *IntermediateRequest { 47 | r.method = method 48 | return r 49 | } 50 | 51 | func (r *IntermediateRequest) WithResponseBeginHandler(runnable func(resp *http.Response)) *IntermediateRequest { 52 | r.onResponseBegin = runnable 53 | return r 54 | } 55 | 56 | func (r *IntermediateRequest) WithWebsocketSession(session *ws.Conn) *IntermediateRequest { 57 | r.websocketSession = session 58 | return r 59 | } 60 | 61 | // Abort abort sending request to target service 62 | func (r *IntermediateRequest) Abort(err error) { 63 | r.result.failure = err 64 | } 65 | 66 | // FireRequestFromConnectorToTargetService send the composed httpRequest from connector to target service 67 | func (r *IntermediateRequest) FireRequestFromConnectorToTargetService(callback func(result *result)) { 68 | defer func() { 69 | if e := recover(); e != nil { 70 | LOG.Errorf("failed to fire http request, %s, err: %s", r.String(), e.(error).Error()) 71 | } 72 | callback(r.result) 73 | }() 74 | 75 | var ( 76 | request *http.Request 77 | response *http.Response 78 | err error 79 | ) 80 | // please don't change the if else clause 81 | if r.contentProvider == nil { 82 | request, err = http.NewRequest(r.method, r.url, nil) 83 | } else { 84 | request, err = http.NewRequest(r.method, r.url, r.contentProvider) 85 | } 86 | if err != nil { 87 | r.result.failure = err 88 | LOG.Errorf("fail to compose a request from intermediate request, request: %s", r.String()) 89 | panic(errors.New("fail to compose a request from intermediate request")) 90 | } 91 | request.Header = r.Headers 92 | response, err = r.client.Do(request) 93 | if err != nil { 94 | e := HttpClientPolicyErr{Msg: "failed to send request, err detail: " + err.Error()} 95 | r.result.failure = e 96 | LOG.Errorf("failed to send request from connector to target-service, err: %s", err.Error()) 97 | panic(e) 98 | } 99 | // check is the request is aborted 100 | if r.result.failure != nil { 101 | panic(r.result.failure) 102 | } 103 | // set resp Headers 104 | r.onResponseBegin(response) 105 | // read resp body 106 | defer response.Body.Close() 107 | buf := make([]byte, WriteBufferSize) 108 | n := 0 109 | for { 110 | n, err = response.Body.Read(buf) 111 | if n > 0 { 112 | if err = r.websocketSession.WriteMessage(ws.BinaryMessage, buf[:n]); err != nil { 113 | r.result.failure = err 114 | LOG.Errorf("got response from target-service, but failed to write binary back to router side websocket server, server address: %s, err: %s", r.websocketSession.RemoteAddr(), err.Error()) 115 | panic(err) 116 | } 117 | } 118 | if err == io.EOF { 119 | r.result.isSucceeded = true 120 | r.result.response = response 121 | break 122 | } else if err != nil { 123 | err = errors.New("failed to read response body from service response, err detail: " + err.Error()) 124 | LOG.Error(err.Error()) 125 | r.result.failure = err 126 | panic(err) 127 | } 128 | } 129 | } 130 | 131 | func (r *IntermediateRequest) Content(reader *io.PipeReader) { 132 | r.contentProvider = reader 133 | } 134 | 135 | func (r *IntermediateRequest) String() string { 136 | return fmt.Sprintf("IntermediateRequest{url: %s, method: %s}", r.url, r.method) 137 | } 138 | -------------------------------------------------------------------------------- /connector/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | . "github.com/torchcc/crank4go/protocol" 5 | "github.com/torchcc/crank4go/util" 6 | ) 7 | 8 | type ConnectorPluginStatCarrier interface { 9 | // GetStat the stat that connector plugin carries from connector socket context to 2 plugins method in a connectorPlugin 10 | GetStat() interface{} 11 | 12 | // Close close the stat when a plugin method finished its job 13 | Close() error 14 | } 15 | 16 | type ConnectorPlugin interface { 17 | // HandleBeforeRequestSent run the plugin method before request is sent to local service connector after connector accepted the cranker router 18 | // eg: sometime when connector side need to send some event to notify / log the status of request processing 19 | HandleBeforeRequestSent(req *CrankerProtocolRequest, carrier ConnectorPluginStatCarrier) (ConnectorPluginStatCarrier, error) 20 | 21 | // HandleAfterResponseReceived run the plugin method after local service connector response received 22 | // eg: sometime when connector side need to send some event to notify / log the status of request processing 23 | HandleAfterResponseReceived(resp *CrankerProtocolResponse, carrier ConnectorPluginStatCarrier) (ConnectorPluginStatCarrier, error) 24 | } 25 | 26 | type ConnectorPluginStatCarriers struct { 27 | statCarrierMap map[ConnectorPlugin]ConnectorPluginStatCarrier 28 | } 29 | 30 | func (cs *ConnectorPluginStatCarriers) Close() { 31 | var err error 32 | for _, carrier := range cs.statCarrierMap { 33 | util.LOG.Debugf("closing carrier %s", carrier) 34 | if err = carrier.Close(); err != nil { 35 | util.LOG.Warningf("failed to close carrier, err: %s", err.Error()) 36 | } 37 | } 38 | } 39 | 40 | func (cs *ConnectorPluginStatCarriers) Put(plugin ConnectorPlugin, carrier ConnectorPluginStatCarrier) { 41 | if plugin != nil && carrier != nil { 42 | cs.statCarrierMap[plugin] = carrier 43 | } 44 | } 45 | 46 | func (cs *ConnectorPluginStatCarriers) Get(plugin ConnectorPlugin) ConnectorPluginStatCarrier { 47 | if c, ok := cs.statCarrierMap[plugin]; ok { 48 | return c 49 | } 50 | return nil 51 | } 52 | 53 | func (cs *ConnectorPluginStatCarriers) CleanCarrier(plugin ConnectorPlugin) { 54 | if carrier, ok := cs.statCarrierMap[plugin]; ok { 55 | _ = carrier.Close() 56 | delete(cs.statCarrierMap, plugin) 57 | } 58 | } 59 | 60 | func NewConnectorPluginStatCarriers() *ConnectorPluginStatCarriers { 61 | return &ConnectorPluginStatCarriers{statCarrierMap: make(map[ConnectorPlugin]ConnectorPluginStatCarrier)} 62 | } 63 | -------------------------------------------------------------------------------- /connector/ws_cli_farm_conn_info.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | 12 | . "github.com/torchcc/crank4go/util" 13 | ) 14 | 15 | type WebsocketClientFarm struct { 16 | maxSlidingWindowSize int 17 | websocketClientFarmInfoMap map[string]int 18 | connectorSockets *sync.Map 19 | } 20 | 21 | // NewWebsocketClientFarm Set by service. maxSlidingWindowSize = 2 * slidingWindowSize because connector add and remove websocket in different goroutines, 22 | // which may result in adding websocket goes before removing websocket. 23 | func NewWebsocketClientFarm(slidingWindowSize int) *WebsocketClientFarm { 24 | return &WebsocketClientFarm{ 25 | maxSlidingWindowSize: slidingWindowSize * 2, 26 | websocketClientFarmInfoMap: nil, 27 | connectorSockets: &sync.Map{}, 28 | } 29 | } 30 | 31 | func (f *WebsocketClientFarm) addWebsocket(registerUrl string) { 32 | var atomicInt int32 33 | atomicInt = 0 34 | addr, _ := f.connectorSockets.LoadOrStore(registerUrl, &atomicInt) 35 | atomic.AddInt32(addr.(*int32), 1) 36 | LOG.Debugf("add websocket for registerUrl=%s, current websocketClientFarm=%#v", registerUrl, f) 37 | } 38 | 39 | func (f *WebsocketClientFarm) removeWebsocket(registerUrl string) { 40 | if addr, ok := f.connectorSockets.Load(registerUrl); ok { 41 | atomic.AddInt32(addr.(*int32), -1) 42 | LOG.Debugf("remove websocket for registerUrl=%s, current websocketClientFarm=%#v", registerUrl, f) 43 | } 44 | } 45 | 46 | func (f *WebsocketClientFarm) isSafeToAddWebsocket(registerUrl *url.URL) bool { 47 | isNotDeregisterPath := !strings.HasPrefix(registerUrl.Path, "/deregister") 48 | var idleSocketNum int32 49 | if addr, ok := f.connectorSockets.Load(registerUrl.String()); ok { 50 | idleSocketNum = *(addr.(*int32)) 51 | } 52 | return isNotDeregisterPath && f.maxSlidingWindowSize > int(idleSocketNum) 53 | } 54 | 55 | func (f *WebsocketClientFarm) ToMap() map[string]int { 56 | f.websocketClientFarmInfoMap = make(map[string]int) 57 | f.connectorSockets.Range(func(key, value interface{}) bool { 58 | f.websocketClientFarmInfoMap[key.(string)] = int(*(value.(*int32))) 59 | return true 60 | }) 61 | return f.websocketClientFarmInfoMap 62 | } 63 | 64 | type ConnectionInfo struct { 65 | routerURI *url.URL 66 | connIndex int 67 | curConnAttempts *int64 68 | } 69 | 70 | func NewConnectionInfo(routerURI *url.URL, connIndex int) *ConnectionInfo { 71 | return &ConnectionInfo{ 72 | routerURI: routerURI, 73 | connIndex: connIndex, 74 | curConnAttempts: new(int64), 75 | } 76 | } 77 | 78 | func (ci *ConnectionInfo) OnConnectedSuccessfully() { 79 | atomic.StoreInt64(ci.curConnAttempts, 0) 80 | } 81 | 82 | func (ci *ConnectionInfo) OnConnectionStarting() { 83 | atomic.AddInt64(ci.curConnAttempts, 1) 84 | } 85 | 86 | func (ci *ConnectionInfo) RetryAfterMillis() int64 { 87 | return int64(500 * math.Min(10000, math.Pow(2, float64(atomic.LoadInt64(ci.curConnAttempts))))) 88 | } 89 | 90 | func (ci *ConnectionInfo) String() string { 91 | return "ConnectionInfo{" + 92 | "routerURI=" + ci.routerURI.String() + 93 | ", connIndex=" + strconv.Itoa(ci.connIndex) + 94 | fmt.Sprintf(", curConnAttempts=%d", atomic.LoadInt64(ci.curConnAttempts)) + 95 | "}" 96 | } 97 | -------------------------------------------------------------------------------- /cranker-http-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchcc/crank4go/b961b8b3bbf7434f36b20e229d72e90ea4629a55/cranker-http-request.png -------------------------------------------------------------------------------- /cranker-websockets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchcc/crank4go/b961b8b3bbf7434f36b20e229d72e90ea4629a55/cranker-websockets.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/torchcc/crank4go 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/google/uuid v1.2.0 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/julienschmidt/httprouter v1.3.0 9 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 2 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 4 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 6 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 7 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 8 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 9 | -------------------------------------------------------------------------------- /performance_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchcc/crank4go/b961b8b3bbf7434f36b20e229d72e90ea4629a55/performance_test.png -------------------------------------------------------------------------------- /protocol/cranker_protocol.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/torchcc/crank4go/util" 9 | ) 10 | 11 | const ( 12 | SupportingHttpVersion = "HTTP/1.1" 13 | CrankerProtocolVersion10 = "1.0" 14 | CrankerProtocolDebugHeader = "X-ROUTER_DEBUG_MODE" 15 | CrankerProtocolDebugHeaderValueEnable = "1" 16 | 17 | RequestBodyPendingMarker = "_1" 18 | RequestHasNoBodyMarker = "_2" 19 | RequestBodyEndedMarker = "_3" 20 | ) 21 | 22 | func ValidateCrankerProtocolVersion(version string) bool { 23 | if version == "" { 24 | return false 25 | } 26 | if version != CrankerProtocolVersion10 { 27 | return false 28 | } 29 | util.LOG.Debugf("i can establish connection with Cranker Protocol %s, currently support %s", CrankerProtocolVersion10, SupportingHttpVersion) 30 | return true 31 | } 32 | 33 | func IsDebugResp(response *CrankerProtocolResponse) bool { 34 | if response == nil { 35 | return false 36 | } 37 | return checkDebugHeader(response.Headers) 38 | } 39 | 40 | func IsDebugReq(request *CrankerProtocolRequest) bool { 41 | if request == nil { 42 | return false 43 | } 44 | return checkDebugHeader(request.Headers) 45 | } 46 | 47 | func checkDebugHeader(headersArr []string) bool { 48 | if headersArr == nil || len(headersArr) == 0 { 49 | return false 50 | } 51 | pattern := CrankerProtocolDebugHeader + ":" 52 | for _, headerNameValue := range headersArr { 53 | if strings.HasPrefix(headerNameValue, pattern) { 54 | nameValue := strings.Split(headerNameValue, ":") 55 | if len(nameValue) == 2 && strings.TrimSpace(nameValue[1]) == CrankerProtocolDebugHeaderValueEnable { 56 | return true 57 | } 58 | } 59 | } 60 | return false 61 | } 62 | 63 | // ################ 64 | type HeadersBuilder struct { 65 | headers string 66 | } 67 | 68 | func (builder *HeadersBuilder) AppendHeader(header, value string) { 69 | builder.headers += header + ":" + value + "\n" 70 | } 71 | 72 | func (builder *HeadersBuilder) AppendHeaders(headers []string) { 73 | for _, header := range headers { 74 | builder.headers += header + "\n" 75 | } 76 | } 77 | 78 | func (builder *HeadersBuilder) String() string { 79 | return builder.headers 80 | } 81 | 82 | // ################# 83 | type CrankerProtocolResponseBuilder struct { 84 | sourceUrl string 85 | httpMethod string 86 | status int 87 | reason string 88 | headers *HeadersBuilder 89 | } 90 | 91 | func (b *CrankerProtocolResponseBuilder) WithRespStatus(status int) *CrankerProtocolResponseBuilder { 92 | b.status = status 93 | return b 94 | } 95 | 96 | func (b *CrankerProtocolResponseBuilder) WithRespReason(reason string) *CrankerProtocolResponseBuilder { 97 | b.reason = reason 98 | return b 99 | } 100 | 101 | func (b *CrankerProtocolResponseBuilder) WithRespHeaders(headers *HeadersBuilder) *CrankerProtocolResponseBuilder { 102 | b.headers = headers 103 | return b 104 | } 105 | 106 | func (b *CrankerProtocolResponseBuilder) WithSourceUrl(requestDest string) *CrankerProtocolResponseBuilder { 107 | b.sourceUrl = requestDest 108 | return b 109 | } 110 | 111 | func (b *CrankerProtocolResponseBuilder) WithHttpMethod(method string) *CrankerProtocolResponseBuilder { 112 | b.httpMethod = method 113 | return b 114 | } 115 | 116 | func (b *CrankerProtocolResponseBuilder) Build() string { 117 | return fmt.Sprintf("HTTP/1.1 %d %s \n%s %s\n%s", b.status, b.reason, b.httpMethod, b.sourceUrl, b.headers.String()) 118 | } 119 | 120 | // ############ 121 | type CrankerProtocolRequestBuilder struct { 122 | reqLine string 123 | headers *HeadersBuilder 124 | endMarker string 125 | } 126 | 127 | func (b *CrankerProtocolRequestBuilder) WithReqLine(line string) *CrankerProtocolRequestBuilder { 128 | b.reqLine = line 129 | return b 130 | } 131 | 132 | func (b *CrankerProtocolRequestBuilder) WithReqHeaders(headers *HeadersBuilder) *CrankerProtocolRequestBuilder { 133 | b.headers = headers 134 | return b 135 | } 136 | 137 | func (b *CrankerProtocolRequestBuilder) WithReqBodyPending() *CrankerProtocolRequestBuilder { 138 | b.endMarker = RequestBodyPendingMarker 139 | return b 140 | } 141 | 142 | func (b *CrankerProtocolRequestBuilder) WithReqHasNoBody() *CrankerProtocolRequestBuilder { 143 | b.endMarker = RequestHasNoBodyMarker 144 | return b 145 | } 146 | 147 | func (b *CrankerProtocolRequestBuilder) WithReqBodyEnded() *CrankerProtocolRequestBuilder { 148 | b.endMarker = RequestBodyEndedMarker 149 | return b 150 | } 151 | 152 | func (b *CrankerProtocolRequestBuilder) Build() string { 153 | if b.reqLine != "" && b.headers != nil { 154 | return b.reqLine + "\n" + b.headers.String() + "\n" + b.endMarker 155 | } 156 | return b.endMarker 157 | } 158 | 159 | // ################ 160 | // define an interface 161 | type CrankerProtocolMessage interface { 162 | ToProtocolMessage() string 163 | } 164 | 165 | /* 166 | * CRANKER PROTOCOL_ VERSION_1_0 167 | * request msg format: 168 | *

169 | * ==== msg without body ===== 170 | * ** GET /modules/uui-allocation/1.0.68/uui-allocation.min.js.map HTTP/1.1\n 171 | * ** [headers]\n 172 | * ** \n 173 | * ** endmarker 174 | *

175 | *

176 | * OR 177 | *

178 | * ==== msg with body part 1 ==== 179 | * ** GET /modules/uui-allocation/1.0.68/uui-allocation.min.js.map HTTP/1.1\n 180 | * ** [headers]\n 181 | * ** \n 182 | * ** endmarker 183 | * ==== msg with body part 2 ==== 184 | ** [BINARY BODY] 185 | * ==== msg with body part 3 ==== 186 | * ** endmarker 187 | */ 188 | 189 | // CrankerProtocolRequest define a implementation of the interface 190 | type CrankerProtocolRequest struct { 191 | HttpMethod string 192 | Dest string 193 | Headers []string 194 | endMarker string 195 | requestLine string 196 | } 197 | 198 | func NewCrankerProtocolRequest(msg string) *CrankerProtocolRequest { 199 | req := new(CrankerProtocolRequest) 200 | if msg == RequestBodyEndedMarker { 201 | req.endMarker = msg 202 | } else { 203 | msgArr := strings.Split(msg, "\n") 204 | req.requestLine = msgArr[0] 205 | util.LOG.Debugf("requestLine >>> %s", req.requestLine) 206 | bits := strings.Split(req.requestLine, " ") 207 | req.HttpMethod = bits[0] 208 | req.Dest = bits[1] 209 | req.Headers = make([]string, len(msgArr)-2) 210 | copy(req.Headers, msgArr[1:len(msgArr)-1]) 211 | util.LOG.Debugf("headers >>> %#v", req.Headers) 212 | req.endMarker = msgArr[len(msgArr)-1] 213 | util.LOG.Debugf("marker >>> %s", req.endMarker) 214 | } 215 | return req 216 | } 217 | 218 | func (req *CrankerProtocolRequest) RequestBodyPending() bool { 219 | return req.endMarker == RequestBodyPendingMarker 220 | } 221 | 222 | func (req *CrankerProtocolRequest) RequestBodyEnded() bool { 223 | return req.endMarker == RequestBodyEndedMarker 224 | } 225 | 226 | func (req *CrankerProtocolRequest) RequestHasNoBody() bool { 227 | return req.endMarker == RequestHasNoBodyMarker 228 | } 229 | 230 | // ToProtocolMessage return rawMsg 231 | func (req *CrankerProtocolRequest) ToProtocolMessage() string { 232 | if req.requestLine != "" && req.Headers != nil { 233 | headersStr := "" 234 | for _, hl := range req.Headers { 235 | headersStr += hl + "\n" 236 | } 237 | return req.requestLine + "\n" + headersStr + req.endMarker 238 | } else { 239 | return req.endMarker 240 | } 241 | } 242 | 243 | type RequestCallback interface { 244 | callback() 245 | } 246 | 247 | func (req *CrankerProtocolRequest) String() string { 248 | return "CrankerProtocolRequest{" + req.HttpMethod + " " + req.Dest + "}" 249 | } 250 | 251 | /** 252 | * CRANKER PROTOCOL_ VERSION_1_0 253 | *

254 | * response msg format: 255 | *

256 | * ==== part 1 ==== 257 | * ** HTTP/1.1 200 OK\n 258 | * ** GET /appstore/api/health 259 | * ** [headers]\n 260 | * ** \n 261 | * ==== part 2 (if msg has body) ==== 262 | * ** Binary Content 263 | */ 264 | 265 | type CrankerProtocolResponse struct { 266 | Headers []string 267 | status int 268 | reason string 269 | sourceUrl string 270 | httpMethod string 271 | } 272 | 273 | func NewCrankerProtocolResponse(msg string) *CrankerProtocolResponse { 274 | resp := new(CrankerProtocolResponse) 275 | msgArr := strings.Split(msg, "\n") 276 | bits := strings.Split(msgArr[0], " ") 277 | resp.status, _ = strconv.Atoi(bits[1]) 278 | if len(bits) >= 3 { 279 | resp.reason = bits[2] 280 | } 281 | originalRequest := msgArr[1] 282 | requestBits := strings.Split(originalRequest, " ") 283 | resp.httpMethod = requestBits[0] 284 | if len(requestBits) >= 2 { 285 | resp.sourceUrl = requestBits[1] 286 | } 287 | resp.Headers = make([]string, 0, len(msgArr)-2) 288 | copy(resp.Headers, msgArr[2:]) 289 | return resp 290 | } 291 | 292 | func (resp *CrankerProtocolResponse) GetSourceUrl() string { 293 | return resp.sourceUrl 294 | } 295 | 296 | func (resp *CrankerProtocolResponse) GetHttpMethod() string { 297 | return resp.httpMethod 298 | } 299 | 300 | func (resp *CrankerProtocolResponse) Status() int { 301 | return resp.status 302 | } 303 | 304 | func (resp *CrankerProtocolResponse) ToProtocolMessage() string { 305 | builder := new(HeadersBuilder) 306 | builder.AppendHeaders(resp.Headers) 307 | return new(CrankerProtocolResponseBuilder). 308 | WithHttpMethod(resp.httpMethod). 309 | WithSourceUrl(resp.sourceUrl). 310 | WithRespReason(resp.reason). 311 | WithRespStatus(resp.status). 312 | WithRespHeaders(builder).Build() 313 | } 314 | 315 | func (resp *CrankerProtocolResponse) String() string { 316 | return "CrankerProtocolResponse{" + 317 | "headers=" + fmt.Sprintf("%s", resp.Headers) + 318 | ", status=" + strconv.Itoa(resp.status) + 319 | ", reason=" + resp.reason + 320 | ", sourceUrl=" + resp.sourceUrl + 321 | ", httpMethod=" + resp.httpMethod + 322 | "}" 323 | } 324 | -------------------------------------------------------------------------------- /router/api/darklaunch_gray_toggle_resource.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/uuid" 8 | "github.com/julienschmidt/httprouter" 9 | "github.com/torchcc/crank4go/router/darklaunch_manager" 10 | ) 11 | 12 | const grayToggleResourceBasePath string = "/dark-launch/gray" 13 | 14 | type DarkLaunchGrayToggleResource struct { 15 | basePath string 16 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 17 | *Filter 18 | } 19 | 20 | func NewDarkLaunchGrayToggleResource(darkLaunchManager *darklaunch_manager.DarkLaunchManager) *DarkLaunchGrayToggleResource { 21 | return &DarkLaunchGrayToggleResource{ 22 | darkLaunchManager: darkLaunchManager, 23 | basePath: grayToggleResourceBasePath, 24 | Filter: &Filter{}, 25 | } 26 | } 27 | 28 | func (t *DarkLaunchGrayToggleResource) GetDetail(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 29 | RespTextPlainOk(w, fmt.Sprintf("DarkMode=%v, darkModeGrayTestToggle=%v", 30 | t.darkLaunchManager.IsDarkModeOn(), darklaunch_manager.IsGrayTestingOn())) 31 | return true 32 | } 33 | 34 | // PAth("/on") 35 | func (t *DarkLaunchGrayToggleResource) PutOn(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 36 | if !t.darkLaunchManager.IsDarkModeOn() { 37 | RespTextPlainWithStatus(w, "Forbidden request, ErrorID="+uuid.New().String(), http.StatusForbidden) 38 | } else { 39 | darklaunch_manager.TurnGrayTestingOn("turn on gray testing by rest call") 40 | RespTextPlainOk(w, fmt.Sprintf("DarkMode=%v, darkModeGrayTestToggle=%v", t.darkLaunchManager.IsDarkModeOn(), darklaunch_manager.IsGrayTestingOn())) 41 | } 42 | return true 43 | } 44 | 45 | // PAth("/off") 46 | func (t *DarkLaunchGrayToggleResource) PutOff(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 47 | if !t.darkLaunchManager.IsDarkModeOn() { 48 | RespTextPlainWithStatus(w, "Forbidden request, ErrorID="+uuid.New().String(), http.StatusForbidden) 49 | } else { 50 | darklaunch_manager.TurnGrayTestingOff("turn off gray testing by rest call") 51 | RespTextPlainOk(w, fmt.Sprintf("DarkMode=%v, darkModeGrayTestToggle=%v", t.darkLaunchManager.IsDarkModeOn(), darklaunch_manager.IsGrayTestingOn())) 52 | } 53 | return true 54 | } 55 | 56 | func (t *DarkLaunchGrayToggleResource) RegisterResourceToHttpRouter(httpRouter *httprouter.Router, rootPath string) { 57 | basePath := rootPath + t.basePath 58 | httpRouter.GET(basePath, t.convertToHttpRouterHandlerWithFilters(t.GetDetail)) 59 | httpRouter.PUT(basePath+"/on", t.convertToHttpRouterHandlerWithFilters(t.PutOn)) 60 | httpRouter.PUT(basePath+"/off", t.convertToHttpRouterHandlerWithFilters(t.PutOff)) 61 | } 62 | -------------------------------------------------------------------------------- /router/api/darklaunch_ip_resource.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/uuid" 8 | "github.com/julienschmidt/httprouter" 9 | MediaType "github.com/torchcc/crank4go/router/api/media_type" 10 | "github.com/torchcc/crank4go/router/darklaunch_manager" 11 | "github.com/torchcc/crank4go/util" 12 | ) 13 | 14 | const ipResourceBasePath string = "/dark-launch/ip" 15 | 16 | type DarkLaunchIpResource struct { 17 | basePath string 18 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 19 | *Filter 20 | } 21 | 22 | func NewDarkLaunchIpResource(manager *darklaunch_manager.DarkLaunchManager) *DarkLaunchIpResource { 23 | return &DarkLaunchIpResource{ 24 | basePath: ipResourceBasePath, 25 | darkLaunchManager: manager, 26 | Filter: &Filter{}, 27 | } 28 | } 29 | 30 | func (d *DarkLaunchIpResource) GetDarkIps(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 31 | RespTextPlainOk(w, fmt.Sprintf("DarkMode IPs = %v", d.darkLaunchManager.IpList())) 32 | return true 33 | } 34 | 35 | // @Path("/{ip}") 36 | func (d *DarkLaunchIpResource) GetDarkModeByHost(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 37 | ip := params.ByName("ip") 38 | RespTextPlainOk(w, fmt.Sprintf("DarkMode = %v for ip = %s", d.darkLaunchManager.ContainsIp(ip), ip)) 39 | return true 40 | } 41 | 42 | // @Path("/{ip}") 43 | func (d *DarkLaunchIpResource) PutEnableDarkModeByIp(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 44 | ip := params.ByName("ip") 45 | if err := d.darkLaunchManager.AddIp(ip); err != nil { 46 | d.errorHandle(w, r, ip, "Add IP") 47 | } else { 48 | util.LOG.Infof("darkLaunch update: true, action: Add IP, ip: %s", ip) 49 | RespTextPlainOk(w, "update dark launch manager successfully") 50 | } 51 | return true 52 | } 53 | 54 | // @Path("/{ip}") 55 | func (d *DarkLaunchIpResource) DeleteDarkModeByIp(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 56 | ip := params.ByName("ip") 57 | if err := d.darkLaunchManager.RemoveIp(ip); err != nil { 58 | d.errorHandle(w, r, ip, "Remove IP") 59 | } else { 60 | util.LOG.Infof("darkLaunch update: true, action: Remove IP, ip: %s", ip) 61 | RespTextPlainOk(w, fmt.Sprintf("ip: %s was deleted successfully from dark launch manager", ip)) 62 | } 63 | return true 64 | } 65 | 66 | func (d *DarkLaunchIpResource) errorHandle(respWriter http.ResponseWriter, req *http.Request, ip, action string) { 67 | errorID := uuid.New().String() 68 | util.LOG.Warningf("Receive invalid ip: %s, action: %s, errorID: %s", ip, action, errorID) 69 | respWriter.Header().Add("Content-Type", MediaType.TextPlain) 70 | respWriter.WriteHeader(http.StatusBadRequest) 71 | _, _ = respWriter.Write([]byte(fmt.Sprintf("Invalid request, invalid ip: %s, action: %s, ErrorID: %s", ip, action, errorID))) 72 | } 73 | 74 | func (d *DarkLaunchIpResource) RegisterResourceToHttpRouter(httpRouter *httprouter.Router, rootPath string) { 75 | basePath := rootPath + d.basePath 76 | httpRouter.GET(basePath, d.convertToHttpRouterHandlerWithFilters(d.GetDarkIps)) 77 | httpRouter.GET(basePath+"/:ip", d.convertToHttpRouterHandlerWithFilters(d.GetDarkModeByHost)) 78 | httpRouter.PUT(basePath+"/:ip", d.convertToHttpRouterHandlerWithFilters(d.PutEnableDarkModeByIp)) 79 | httpRouter.DELETE(basePath+"/:ip", d.convertToHttpRouterHandlerWithFilters(d.DeleteDarkModeByIp)) 80 | } 81 | -------------------------------------------------------------------------------- /router/api/darklaunch_service_resource.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/google/uuid" 8 | "github.com/julienschmidt/httprouter" 9 | MediaType "github.com/torchcc/crank4go/router/api/media_type" 10 | "github.com/torchcc/crank4go/router/darklaunch_manager" 11 | "github.com/torchcc/crank4go/router/handler" 12 | "github.com/torchcc/crank4go/util" 13 | ) 14 | 15 | const serviceResourceBasePath string = "/dark-launch/service" 16 | 17 | type DarkLaunchServiceResource struct { 18 | basePath string 19 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 20 | *Filter 21 | } 22 | 23 | func NewDarkLaunchServiceResource(manager *darklaunch_manager.DarkLaunchManager) *DarkLaunchServiceResource { 24 | return &DarkLaunchServiceResource{ 25 | basePath: serviceResourceBasePath, 26 | darkLaunchManager: manager, 27 | Filter: &Filter{}, 28 | } 29 | } 30 | 31 | func (d *DarkLaunchServiceResource) GetDarkServices(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 32 | RespTextPlainOk(w, fmt.Sprintf("DarkMode Services = %v", d.darkLaunchManager.ServiceList())) 33 | return true 34 | } 35 | 36 | // @Path("/{service}") 37 | func (d *DarkLaunchServiceResource) GetDarkModeByHost(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 38 | service := params.ByName("service") 39 | RespTextPlainOk(w, fmt.Sprintf("DarkMode = %v for service = %s", d.darkLaunchManager.ContainsService(service), service)) 40 | return true 41 | } 42 | 43 | // @Path("/{service}") 44 | func (d *DarkLaunchServiceResource) PutEnableDarkModeByService(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 45 | service := params.ByName("service") 46 | if err := d.darkLaunchManager.AddService(service); err != nil { 47 | d.errorHandle(w, r, service, "Add service") 48 | } else { 49 | util.LOG.Infof("darkLaunch update: true, action: Add service, service: %s", service) 50 | RespTextPlainOk(w, "update dark launch manager successfully") 51 | } 52 | return true 53 | } 54 | 55 | // @Path("/{service}") 56 | func (d *DarkLaunchServiceResource) DeleteDarkModeByService(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 57 | service := params.ByName("service") 58 | if err := d.darkLaunchManager.RemoveService(service); err != nil { 59 | d.errorHandle(w, r, service, "Remove service") 60 | } else { 61 | util.LOG.Infof("darkLaunch update: true, action: Remove service, service: %s", service) 62 | RespTextPlainOk(w, fmt.Sprintf("service: %s was deleted successfully from dark launch manager", service)) 63 | } 64 | return true 65 | } 66 | 67 | func (d *DarkLaunchServiceResource) errorHandle(respWriter http.ResponseWriter, req *http.Request, service, action string) { 68 | errorID := uuid.New().String() 69 | util.LOG.Warningf("Receive invalid service: %s, action: %s, errorID: %s", service, action, errorID) 70 | respWriter.Header().Add("Content-Type", MediaType.TextPlain) 71 | respWriter.WriteHeader(http.StatusBadRequest) 72 | _, _ = respWriter.Write([]byte(fmt.Sprintf("Invalid request, invalid service: %s, action: %s, ErrorID: %s", service, action, errorID))) 73 | } 74 | 75 | func (d *DarkLaunchServiceResource) RegisterResourceToHttpRouter(httpRouter *httprouter.Router, rootPath string) { 76 | basePath := rootPath + d.basePath 77 | httpRouter.GET(basePath, d.convertToHttpRouterHandlerWithFilters(d.GetDarkServices)) 78 | httpRouter.GET(basePath+"/:service", d.convertToHttpRouterHandlerWithFilters(d.GetDarkModeByHost)) 79 | httpRouter.PUT(basePath+"/:service", d.convertToHttpRouterHandlerWithFilters(d.PutEnableDarkModeByService)) 80 | httpRouter.DELETE(basePath+"/:service", d.convertToHttpRouterHandlerWithFilters(d.DeleteDarkModeByService)) 81 | } 82 | 83 | type Filter struct { 84 | reqFilters []handler.XHandler 85 | respFilters []handler.XHandler 86 | } 87 | 88 | func (f *Filter) RespFilters() []handler.XHandler { 89 | return f.respFilters 90 | } 91 | 92 | func (f *Filter) ReqFilters() []handler.XHandler { 93 | return f.reqFilters 94 | } 95 | 96 | func (f *Filter) AddReqFilters(handlers ...handler.XHandler) *Filter { 97 | if f.reqFilters == nil { 98 | f.reqFilters = make([]handler.XHandler, 0, 8) 99 | } 100 | for _, h := range handlers { 101 | f.reqFilters = append(f.reqFilters, h) 102 | } 103 | return f 104 | } 105 | 106 | func (f *Filter) AddRespFilters(handlers ...handler.XHandler) *Filter { 107 | if f.respFilters == nil { 108 | f.respFilters = make([]handler.XHandler, 0, 8) 109 | } 110 | for _, h := range handlers { 111 | f.respFilters = append(f.respFilters, h) 112 | } 113 | return f 114 | } 115 | 116 | func (f *Filter) convertToHttpRouterHandlerWithFilters(function func(http.ResponseWriter, *http.Request, httprouter.Params) bool) func(http.ResponseWriter, *http.Request, httprouter.Params) { 117 | return handler.NewXHttpHandler(handler.XHandlerFunc(function)). 118 | AddReqHandlers(f.ReqFilters()...). 119 | AddRespHandlers(f.RespFilters()...). 120 | ServeXHTTP 121 | } 122 | -------------------------------------------------------------------------------- /router/api/healthservice_resource.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/julienschmidt/httprouter" 7 | "github.com/torchcc/crank4go/router/service" 8 | ) 9 | 10 | const healthResourceBasePath string = "/health" 11 | 12 | type HealthResource struct { 13 | basePath string 14 | healthService service.HealthService 15 | *Filter 16 | } 17 | 18 | func NewHealthResource(healthService service.HealthService) *HealthResource { 19 | return &HealthResource{ 20 | healthService: healthService, 21 | basePath: healthResourceBasePath, 22 | Filter: &Filter{}, 23 | } 24 | } 25 | 26 | // basePath 27 | func (h *HealthResource) GetHealthInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 28 | RespJsonOk(w, h.healthService.CreateHealthReport()) 29 | return true 30 | } 31 | 32 | // @Path("/connectors") 33 | func (h *HealthResource) GetConnectorsInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 34 | RespJsonOk(w, h.healthService.CreateConnectorsReport()) 35 | return true 36 | } 37 | 38 | // @Path("/categorizedConnectors") 39 | func (h *HealthResource) GetCategorizedConnectorsInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 40 | RespJsonOk(w, h.healthService.CreateCategorizedConnectorsReport()) 41 | return true 42 | } 43 | 44 | func (h *HealthResource) RegisterResourceToHttpServer(server *httprouter.Router, rootPath string) { 45 | basePath := rootPath + h.basePath 46 | server.GET(basePath, h.convertToHttpRouterHandlerWithFilters(h.GetHealthInfo)) 47 | server.GET(basePath+"/connectors", h.convertToHttpRouterHandlerWithFilters(h.GetConnectorsInfo)) 48 | server.GET(basePath+"/categorizedConnectors", h.convertToHttpRouterHandlerWithFilters(h.GetCategorizedConnectorsInfo)) 49 | } 50 | 51 | type HealthServiceResource2 struct { 52 | basePath string 53 | connectorPath string 54 | healthService service.HealthService 55 | *Filter 56 | } 57 | 58 | func NewHealthServiceResource2(healthService service.HealthService) *HealthServiceResource2 { 59 | return &HealthServiceResource2{ 60 | healthService: healthService, 61 | basePath: healthResourceBasePath, 62 | connectorPath: "/connectors", 63 | Filter: &Filter{}, 64 | } 65 | } 66 | 67 | func (h *HealthServiceResource2) GetHealthInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 68 | RespJsonOk(w, h.healthService.CreateHealthReport()) 69 | return true 70 | } 71 | 72 | // @Path("/connectors") 73 | func (h *HealthServiceResource2) GetConnectorsInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 74 | RespJsonOk(w, h.healthService.CreateConnectorsReport()) 75 | return true 76 | } 77 | 78 | func (h *HealthServiceResource2) RegisterResourceToHttpRouter(server *httprouter.Router, rootPath string) { 79 | basePath := rootPath + h.basePath 80 | server.GET(basePath, h.convertToHttpRouterHandlerWithFilters(h.GetHealthInfo)) 81 | server.GET(basePath+h.connectorPath, h.convertToHttpRouterHandlerWithFilters(h.GetConnectorsInfo)) 82 | } 83 | -------------------------------------------------------------------------------- /router/api/media_type/media_type.go: -------------------------------------------------------------------------------- 1 | package media_type 2 | 3 | const ( 4 | ApplicationXml = "application/xml" 5 | ApplicationJson = "application/json" 6 | ApplicationFormUrlencoded = "application/x-www-form-urlencoded" 7 | ApplicationOctetStream = "application/octet-stream" 8 | MultipartFormData = "multipart/form-data" 9 | TextPlain = "text/plain" 10 | TextXml = "text/xml" 11 | TextHtml = "text/html" 12 | ) 13 | -------------------------------------------------------------------------------- /router/api/registrations_resource.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "time" 7 | 8 | "github.com/julienschmidt/httprouter" 9 | "github.com/torchcc/crank4go/router/router_socket" 10 | "github.com/torchcc/crank4go/util" 11 | ) 12 | 13 | const registrationsResourceBasePath string = "/registrations" 14 | 15 | type RegistrationsResource struct { 16 | basePath string 17 | websocketFarm *router_socket.WebsocketFarm 18 | *Filter 19 | } 20 | 21 | func NewRegistrationsResource(websocketFarm *router_socket.WebsocketFarm) *RegistrationsResource { 22 | return &RegistrationsResource{ 23 | websocketFarm: websocketFarm, 24 | basePath: registrationsResourceBasePath, 25 | Filter: &Filter{}, 26 | } 27 | } 28 | 29 | func (r *RegistrationsResource) GetRegisterInfo(w http.ResponseWriter, req *http.Request, _ httprouter.Params) bool { 30 | begin := time.Now() 31 | servicesRegisterMap := make(map[string]interface{}) 32 | 33 | for serviceName, routerSocketQueue := range r.websocketFarm.AllSockets() { 34 | remoteAddrs := make(map[string]struct{}) 35 | for _, routerSocket := range routerSocketQueue { 36 | remoteAddrs[strings.Split(routerSocket.RemoteAddr(), ":")[0]] = struct{}{} 37 | servicesRegisterMap[serviceName] = remoteAddrs 38 | } 39 | } 40 | 41 | remoteAddrs := make(map[string]struct{}) 42 | for _, routerSocket := range r.websocketFarm.AllCatchall() { 43 | remoteAddrs[strings.Split(routerSocket.RemoteAddr(), ":")[0]] = struct{}{} 44 | } 45 | servicesRegisterMap["default"] = remoteAddrs 46 | util.LOG.Debugf("getRegisterInfo spent %v time, the request is from %s", time.Now().Sub(begin), req.RemoteAddr) 47 | RespJsonOk(w, servicesRegisterMap) 48 | return true 49 | } 50 | 51 | func (r *RegistrationsResource) RegisterResourceToHttpRouter(httpRouter *httprouter.Router, rootPath string) { 52 | basePath := rootPath + r.basePath 53 | httpRouter.GET(basePath, r.convertToHttpRouterHandlerWithFilters(r.GetRegisterInfo)) 54 | } 55 | -------------------------------------------------------------------------------- /router/api/resource_interface.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/julienschmidt/httprouter" 4 | 5 | type Resource interface { 6 | RegisterResourceToHttpRouter(server *httprouter.Router, rootPath string) 7 | } 8 | -------------------------------------------------------------------------------- /router/api/response.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | MediaType "github.com/torchcc/crank4go/router/api/media_type" 8 | ) 9 | 10 | func RespTextPlainOk(w http.ResponseWriter, text string) { 11 | RespTextPlainWithStatus(w, text, http.StatusOK) 12 | } 13 | 14 | func RespTextPlainWithStatus(w http.ResponseWriter, text string, status int) { 15 | w.Header().Add("Content-Type", MediaType.TextPlain) 16 | w.WriteHeader(status) 17 | _, _ = w.Write([]byte(text)) 18 | } 19 | 20 | func RespJsonOk(w http.ResponseWriter, jsonObj map[string]interface{}) { 21 | w.Header().Add("Content-Type", MediaType.ApplicationJson) 22 | w.WriteHeader(http.StatusOK) 23 | bytes, _ := json.Marshal(jsonObj) 24 | _, _ = w.Write(bytes) 25 | } 26 | -------------------------------------------------------------------------------- /router/corsheader_processor/corsheader_processor.go: -------------------------------------------------------------------------------- 1 | package corsheader_processor 2 | 3 | import "net/http" 4 | 5 | type CorsHeaderProcessor struct { 6 | checkOrigin func(string) bool 7 | } 8 | 9 | func NewCorsHeaderProcessor(checkOrigin func(string) bool) *CorsHeaderProcessor { 10 | return &CorsHeaderProcessor{checkOrigin: checkOrigin} 11 | } 12 | 13 | func (p *CorsHeaderProcessor) Process(req *http.Request, respWriter http.ResponseWriter) { 14 | origin := req.Header.Get("Origin") 15 | if origin != "" && p.checkOrigin(origin) { // is CORS and is a good onw 16 | respWriter.Header().Set("Access-Control-Allow-Origin", origin) 17 | if varyValue := respWriter.Header().Get("Vary"); varyValue != "" { 18 | respWriter.Header().Set("Vary", varyValue+", Origin") 19 | } else { 20 | respWriter.Header().Add("Vary", "Origin") 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /router/darklaunch_manager/darklaunch_manager.go: -------------------------------------------------------------------------------- 1 | package darklaunch_manager 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type IpListener interface { 11 | // AfterDarkIpAdded trigger something after an ip is marked as dark ip 12 | AfterDarkIpAdded(addedIp string) 13 | 14 | // AfterDarkIpRevoked trigger something after a ip is revoked from dark ip list 15 | AfterDarkIpRevoked(revokedIp string) 16 | } 17 | 18 | type ServiceListener interface { 19 | 20 | // AfterDarkServiceAdded trigger something after an ip is marked as dark ip 21 | AfterDarkServiceAdded(addedService string) 22 | 23 | // AfterDarkServiceRevoked trigger something after a ip is revoked from dark ip list 24 | AfterDarkServiceRevoked(revokedService string) 25 | } 26 | 27 | type DarkLaunchManager struct { 28 | currentIps map[string]struct{} 29 | currentServices map[string]struct{} 30 | ipListener IpListener 31 | serviceListener ServiceListener 32 | path string 33 | } 34 | 35 | func NewDarkLaunchManager() *DarkLaunchManager { 36 | return &DarkLaunchManager{ 37 | currentIps: make(map[string]struct{}), 38 | currentServices: make(map[string]struct{}), 39 | } 40 | } 41 | 42 | func NewDarkLaunchManager2(path string) *DarkLaunchManager { 43 | return &DarkLaunchManager{ 44 | path: path, 45 | currentIps: make(map[string]struct{}), 46 | currentServices: make(map[string]struct{}), 47 | } 48 | } 49 | 50 | func (m *DarkLaunchManager) IpList() []string { 51 | list := make([]string, 0, 8) 52 | for ip := range m.currentIps { 53 | list = append(list, ip) 54 | } 55 | return list 56 | } 57 | 58 | func (m *DarkLaunchManager) ServiceList() []string { 59 | list := make([]string, 0, 8) 60 | for service := range m.currentServices { 61 | list = append(list, service) 62 | } 63 | return list 64 | } 65 | 66 | func (m *DarkLaunchManager) AddIp(ip string) error { 67 | if isValidIp(ip) { 68 | m.currentIps[ip] = struct{}{} 69 | m.ipListener.AfterDarkIpAdded(ip) 70 | return nil 71 | } else { 72 | return errors.New("invalid ip address: " + ip) 73 | } 74 | } 75 | 76 | func (m *DarkLaunchManager) AddService(service string) error { 77 | if isValidService(service) { 78 | m.currentServices[service] = struct{}{} 79 | m.serviceListener.AfterDarkServiceAdded(service) 80 | return nil 81 | } else { 82 | return errors.New("invalid service: " + service) 83 | } 84 | } 85 | 86 | func (m *DarkLaunchManager) RemoveIp(ip string) error { 87 | if _, ok := m.currentIps[ip]; ok { 88 | delete(m.currentIps, ip) 89 | m.ipListener.AfterDarkIpRevoked(ip) 90 | return nil 91 | } else { 92 | return errors.New("ip: " + ip + " is not in current list") 93 | } 94 | } 95 | 96 | func (m *DarkLaunchManager) RemoveService(service string) error { 97 | if _, ok := m.currentServices[service]; ok { 98 | delete(m.currentServices, service) 99 | m.serviceListener.AfterDarkServiceRevoked(service) 100 | return nil 101 | } else { 102 | return errors.New("service: " + service + " is not in current list") 103 | } 104 | } 105 | 106 | func (m *DarkLaunchManager) IsDarkModeOn() bool { 107 | return len(m.currentServices) != 0 || len(m.currentIps) != 0 108 | } 109 | 110 | // judge if ip is in dark mode 111 | func (m *DarkLaunchManager) ContainsIp(ip string) bool { 112 | if _, ok := m.currentIps[ip]; ok { 113 | return true 114 | } 115 | return false 116 | } 117 | 118 | // judge if ip is in dark mode 119 | func (m *DarkLaunchManager) ContainsService(service string) bool { 120 | if _, ok := m.currentServices[service]; ok { 121 | return true 122 | } 123 | return false 124 | } 125 | 126 | func (m *DarkLaunchManager) SetServiceListener(serviceListener ServiceListener) *DarkLaunchManager { 127 | m.serviceListener = serviceListener 128 | return m 129 | } 130 | 131 | func (m *DarkLaunchManager) SetIpListener(ipListener IpListener) *DarkLaunchManager { 132 | m.ipListener = ipListener 133 | return m 134 | } 135 | 136 | func isValidService(service string) bool { 137 | if matched, err := regexp.MatchString("^[a-zA-Z]+((-|_)?\\w*)*$", service); err != nil || !matched { 138 | return false 139 | } 140 | return true 141 | } 142 | 143 | func isValidIp(ip string) bool { 144 | if ip == "" { 145 | return false 146 | } 147 | if groups := strings.Split(ip, "."); len(groups) != 4 { 148 | return false 149 | } else { 150 | for _, s := range groups { 151 | if len(s) == 0 { 152 | return false 153 | } 154 | if i, err := strconv.Atoi(s); err != nil || i < 0 || i > 255 { 155 | return false 156 | } 157 | } 158 | return true 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /router/darklaunch_manager/darklaunch_toggle.go: -------------------------------------------------------------------------------- 1 | package darklaunch_manager 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/torchcc/crank4go/util" 7 | ) 8 | 9 | var toggle int32 10 | 11 | func TurnGrayTestingOn(req string) { 12 | util.LOG.Infof("turn gray testing on as user request %s", req) 13 | atomic.StoreInt32(&toggle, 1) 14 | } 15 | 16 | func TurnGrayTestingOff(req string) { 17 | util.LOG.Infof("turn gray testing off as user request %s", req) 18 | atomic.StoreInt32(&toggle, 0) 19 | } 20 | 21 | func IsGrayTestingOn() bool { 22 | return toggle == 1 23 | } 24 | -------------------------------------------------------------------------------- /router/handler/filter.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/julienschmidt/httprouter" 9 | "github.com/torchcc/crank4go/util" 10 | ) 11 | 12 | // PreLoggingFilter 1. applied on crankerRouter's websocketServer's http request, not websocket request. 13 | // 2. applied on crankerRouter's httpServer 14 | // log before the request is handled by target handler 15 | func PreLoggingFilter(_ http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 16 | util.LOG.Infof("started - API {%s} being called by client {%s} through {%s}", r.URL.String(), r.RemoteAddr, r.Method) 17 | return false 18 | } 19 | 20 | // PostLoggingFilter a log filter apply on crankerRouter's registerHandler's http request, not websocket request 21 | // log after the request is handled by target handler 22 | func PostLoggingFilter(_ http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 23 | util.LOG.Infof("finished - API {%s} being called by client {%s} through {%s}", r.URL.String(), r.RemoteAddr, r.Method) 24 | return false 25 | } 26 | 27 | // ReqValidatorFilter applied on router's httpServer, it aborts requests with invalid HTTPMethod 28 | func ReqValidatorFilter(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 29 | var ( 30 | contentLen int 31 | err error 32 | ) 33 | if s := r.Header.Get("Content-Length"); s == "" { 34 | contentLen = -1 35 | } else if contentLen, err = strconv.Atoi(s); err != nil || 36 | ("chunked" == strings.ToLower(r.Header.Get("Transfer-Encoding")) && contentLen > 0) { 37 | w.WriteHeader(400) 38 | _, _ = w.Write([]byte("Invalid request: chunked request with Content-Length")) 39 | return true 40 | } 41 | 42 | if "trace" == strings.ToLower(r.Method) { 43 | w.WriteHeader(405) 44 | _, _ = w.Write([]byte("Method Not Allowed")) 45 | return true 46 | } 47 | 48 | if "options" == strings.ToLower(r.Method) && util.IsNotLocalEnv() { 49 | w.WriteHeader(405) 50 | _, _ = w.Write([]byte("Method Not Allowed")) 51 | return true 52 | } 53 | return false 54 | } 55 | -------------------------------------------------------------------------------- /router/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/julienschmidt/httprouter" 7 | ) 8 | 9 | type XHandler interface { 10 | // Handle if this method return true, it prevents pending reqHandlers from being called 11 | Handle(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool 12 | } 13 | 14 | // The XHandlerFunc type is an adapter to allow the use of 15 | // ordinary functions as XHTTPHandlers. If f is a function 16 | // with the appropriate signature, XHandlerFunc(f) is a 17 | // Handler that calls f. 18 | type XHandlerFunc func(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool 19 | 20 | func (f XHandlerFunc) Handle(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 21 | return f(w, r, params) 22 | } 23 | 24 | // XHTTPHandler an implement of net/http Handler, to implement pipeline/filter pattern easily 25 | type XHTTPHandler struct { 26 | // they are executed before request is handled by target handler 27 | reqHandlers []XHandler 28 | // they are executed after request is handled by target handler 29 | respHandlers []XHandler 30 | 31 | targetHandler XHandler 32 | } 33 | 34 | // To register on origin http ServeMux 35 | func (h *XHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 36 | h.ServeXHTTP(w, r, nil) 37 | } 38 | 39 | func NewXHttpHandler(targetHandler XHandler) *XHTTPHandler { 40 | return &XHTTPHandler{targetHandler: targetHandler} 41 | } 42 | 43 | // ServeXHTTP To register handler on httprouter 44 | func (h *XHTTPHandler) ServeXHTTP(w http.ResponseWriter, r *http.Request, params httprouter.Params) { 45 | stop := false 46 | for _, handler := range h.reqHandlers { 47 | stop = handler.Handle(w, r, params) 48 | if stop { 49 | return 50 | } 51 | } 52 | 53 | h.targetHandler.Handle(w, r, params) 54 | 55 | for _, handler := range h.respHandlers { 56 | handler.Handle(w, r, params) 57 | } 58 | } 59 | 60 | func (h *XHTTPHandler) AddReqHandlers(handlers ...XHandler) *XHTTPHandler { 61 | if h.reqHandlers == nil { 62 | h.respHandlers = make([]XHandler, 0, 8) 63 | } 64 | for _, handler := range handlers { 65 | h.reqHandlers = append(h.reqHandlers, handler) 66 | } 67 | return h 68 | } 69 | 70 | func (h *XHTTPHandler) AddRespHandlers(handlers ...XHandler) *XHTTPHandler { 71 | if h.respHandlers == nil { 72 | h.respHandlers = make([]XHandler, 0, 8) 73 | } 74 | for _, handler := range handlers { 75 | h.respHandlers = append(h.respHandlers, handler) 76 | } 77 | return h 78 | } 79 | -------------------------------------------------------------------------------- /router/handler/websocket_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | 8 | ws "github.com/gorilla/websocket" 9 | "github.com/julienschmidt/httprouter" 10 | "github.com/torchcc/crank4go/router/router_socket" 11 | ) 12 | 13 | var upgrader = &ws.Upgrader{ 14 | HandshakeTimeout: 45 * time.Second, 15 | ReadBufferSize: 4096, // default value 16 | WriteBufferSize: 4096, // default value 17 | WriteBufferPool: &sync.Pool{}, 18 | EnableCompression: false, 19 | } 20 | 21 | type WebsocketHandler struct { 22 | factory func(w http.ResponseWriter, r *http.Request) *router_socket.RouterSocket 23 | upgrader *ws.Upgrader 24 | } 25 | 26 | func NewWebsocketHandler() *WebsocketHandler { 27 | return NewWebsocketHandler2(nil) 28 | } 29 | 30 | func NewWebsocketHandler2(factory func(w http.ResponseWriter, r *http.Request) *router_socket.RouterSocket) *WebsocketHandler { 31 | return &WebsocketHandler{factory: factory, upgrader: upgrader} 32 | } 33 | 34 | func (w *WebsocketHandler) WithWebsocketFactory(factory func(w http.ResponseWriter, r *http.Request) *router_socket.RouterSocket) *WebsocketHandler { 35 | w.factory = factory 36 | return w 37 | } 38 | 39 | func (w *WebsocketHandler) Handle(respWriter http.ResponseWriter, req *http.Request, params httprouter.Params) bool { 40 | if socket := w.factory(respWriter, req); socket == nil { 41 | return true 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /router/interceptor/interceptor.go: -------------------------------------------------------------------------------- 1 | package interceptor 2 | 3 | import ( 4 | "net/http" 5 | 6 | ptc "github.com/torchcc/crank4go/protocol" 7 | "github.com/torchcc/crank4go/util" 8 | ) 9 | 10 | type InterceptorStatCarrier interface { 11 | Close() error 12 | } 13 | 14 | type ProxyInterceptor interface { 15 | /* 16 | Apply intercepting logic on original HTTPRequest and cranker protocol request created fromm current request context. 17 | e.g. when we need to modified http header or protocol request 18 | */ 19 | ApplyOnReq(req *http.Request, ptcReq *ptc.CrankerProtocolRequest) (InterceptorStatCarrier, error) 20 | } 21 | 22 | type InterceptorStatCarriers struct { 23 | statCarrierMap map[ProxyInterceptor]InterceptorStatCarrier 24 | } 25 | 26 | func NewInterceptorStatCarriers() *InterceptorStatCarriers { 27 | return &InterceptorStatCarriers{statCarrierMap: map[ProxyInterceptor]InterceptorStatCarrier{}} 28 | } 29 | 30 | func (c *InterceptorStatCarriers) Close() error { 31 | for _, carrier := range c.statCarrierMap { 32 | util.LOG.Infof("closing carrier %s", carrier) 33 | if err := carrier.Close(); err != nil { 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func (c *InterceptorStatCarriers) Add(interceptor ProxyInterceptor, carrier InterceptorStatCarrier) { 41 | if interceptor != nil && carrier != nil { 42 | c.statCarrierMap[interceptor] = carrier 43 | } 44 | } 45 | 46 | func (c *InterceptorStatCarriers) CleanCarrier(p ProxyInterceptor) { 47 | if carrier, ok := c.statCarrierMap[p]; ok { 48 | _ = carrier.Close() 49 | delete(c.statCarrierMap, p) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /router/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ptc "github.com/torchcc/crank4go/protocol" 4 | 5 | type RouterSocketPlugin interface { 6 | /* 7 | handle protocol response on router side. 8 | protocol response will be received after Cranker connector socket client(the service provider side) 9 | finish processing requests delegated by cranker router. 10 | 11 | business scenarios example: when we want to extract some information from the response and use it in downstream 12 | */ 13 | HandleAfterRespReceived(resp *ptc.CrankerProtocolResponse) error 14 | } 15 | -------------------------------------------------------------------------------- /router/reverse_proxy.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/julienschmidt/httprouter" 12 | ptc "github.com/torchcc/crank4go/protocol" 13 | itc "github.com/torchcc/crank4go/router/interceptor" 14 | "github.com/torchcc/crank4go/router/router_socket" 15 | "github.com/torchcc/crank4go/util" 16 | ) 17 | 18 | var ( 19 | ip string 20 | bufPool *sync.Pool 21 | ) 22 | 23 | func GetLocalIP() string { 24 | addrs, err := net.InterfaceAddrs() 25 | if err != nil { 26 | util.LOG.Infof("cannot get local ip.") 27 | return "unknown" 28 | } 29 | for _, address := range addrs { 30 | // check the address type and if it is not a loopback the display it 31 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 32 | if ipnet.IP.To4() != nil { 33 | return ipnet.IP.String() 34 | } 35 | } 36 | } 37 | util.LOG.Infof("cannot get local ip.") 38 | return "unknown" 39 | } 40 | 41 | func init() { 42 | ip = GetLocalIP() 43 | } 44 | 45 | type ReverseProxy struct { 46 | // see https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3 47 | HopByHopHeadersFields map[string]struct{} 48 | websocketFarm *router_socket.WebsocketFarm 49 | reqComponentHeader string 50 | interceptors []itc.ProxyInterceptor 51 | } 52 | 53 | func NewReverseProxy(farm *router_socket.WebsocketFarm, reqComponentHeader string, interceptors []itc.ProxyInterceptor) *ReverseProxy { 54 | proxy := &ReverseProxy{ 55 | HopByHopHeadersFields: map[string]struct{}{ 56 | "Connection": {}, "Keep-Alive": {}, "Proxy-Authenticate": {}, "Proxy-Authorization": {}, 57 | "TE": {}, "Trailer": {}, "Transfer-Encoding": {}, "Upgrade": {}}, 58 | websocketFarm: farm, 59 | reqComponentHeader: reqComponentHeader, 60 | interceptors: interceptors, 61 | } 62 | if interceptors == nil { 63 | proxy.interceptors = make([]itc.ProxyInterceptor, 0, 8) 64 | } 65 | util.LOG.Debugf("created reverse proxy: %s", proxy) 66 | return proxy 67 | } 68 | 69 | func (p *ReverseProxy) String() string { 70 | return fmt.Sprintf("ReverseProxy{websocketFarm=%v, requestComponentHeader=%s, interceptors=%s}", 71 | p.websocketFarm, p.reqComponentHeader, p.interceptors) 72 | } 73 | 74 | func (p *ReverseProxy) Handle(w http.ResponseWriter, r *http.Request, params httprouter.Params) bool { 75 | var ( 76 | crankedSocket *router_socket.RouterSocket 77 | target = r.URL.Path 78 | componentName = p.componentNameFromHeader(r) 79 | err error 80 | ) 81 | util.LOG.Debugf("websocketFarm target: %s, component: %s", target, componentName) 82 | if crankedSocket, err = p.websocketFarm.AcquireSocket(target, componentName); err != nil { 83 | if routeErr, ok := err.(util.NoRouteErr); ok { 84 | util.LOG.Errorf("failed to forward target %s, NoRouteErr occurs, err: %s", target, routeErr) 85 | w.WriteHeader(http.StatusNotFound) 86 | _, _ = w.Write([]byte("Service Not Found")) 87 | return true 88 | } else { // TODO should assert if err is timeoutErr or not ? 89 | util.LOG.Errorf("failed to forward target %s, after timeout, err: %s", target, routeErr) 90 | w.WriteHeader(http.StatusServiceUnavailable) 91 | _, _ = w.Write([]byte("No crankers Available")) 92 | return true 93 | } 94 | } 95 | util.LOG.Infof("proxying to target service, forwarding %s from %s to %s, "+ 96 | "connectorID=%s, requestComponentName=%s", target, r.RemoteAddr, crankedSocket.RemoteAddr(), 97 | crankedSocket.ConnectorInstanceID(), componentName) 98 | handleDone := &sync.WaitGroup{} // its Done method must be called only after the writing to respWriter is finished. 99 | handleDone.Add(1) 100 | p.sendRequestOverWebsocket(r, w, crankedSocket, handleDone) 101 | handleDone.Wait() 102 | return true 103 | 104 | } 105 | 106 | func (p *ReverseProxy) componentNameFromHeader(r *http.Request) (name string) { 107 | if strings.TrimSpace(p.reqComponentHeader) != "" { 108 | name = r.Header.Get(strings.TrimSpace(p.reqComponentHeader)) 109 | } 110 | return 111 | } 112 | 113 | func (p *ReverseProxy) sendRequestOverWebsocket(cliReq *http.Request, respWriter http.ResponseWriter, socket *router_socket.RouterSocket, handleDone *sync.WaitGroup) { 114 | socket.SetResponse(respWriter, cliReq, handleDone) 115 | 116 | ptcReqBuilder := new(ptc.CrankerProtocolRequestBuilder) 117 | ptcReqBuilder.WithReqLine(createReqLine(cliReq)) 118 | 119 | headers := new(ptc.HeadersBuilder) 120 | hasBody := p.setTargetReqHeaders(cliReq, headers) 121 | ptcReqBuilder.WithReqHeaders(headers) 122 | 123 | var ( 124 | carriers = itc.NewInterceptorStatCarriers() 125 | ptcMsg string 126 | err error 127 | buf []byte 128 | n int // offset, use to mark how many bytes have been read from request body 129 | ) 130 | defer func() { 131 | if e := recover(); e != nil { 132 | p.onProxyingError(cliReq, socket, e.(error)) 133 | } 134 | if buf != nil { 135 | bufPool.Put(buf) 136 | } 137 | _ = carriers.Close() 138 | util.LOG.Infof("finish - API %s being called by client %s through %s", cliReq.URL.String(), cliReq.RemoteAddr, cliReq.Method) 139 | }() 140 | 141 | if hasBody { 142 | // stream the body 143 | ptcMsg = ptcReqBuilder.WithReqBodyPending().Build() 144 | if err = socket.SendText(ptcMsg); err != nil { 145 | panic(err) 146 | } 147 | // read http request Body and send to connector through websocket 148 | buf = bufPool.Get().([]byte) 149 | for { 150 | n, err = cliReq.Body.Read(buf) 151 | if n > 0 { 152 | util.LOG.Debugf("about to send %s bytes to connector", n) 153 | if e := socket.SendData(buf[:n]); e != nil { 154 | panic(err) 155 | } 156 | } 157 | if err == io.EOF { 158 | err = nil 159 | break 160 | } else if err != nil { 161 | panic(err) 162 | } 163 | } 164 | if err = socket.SendText(new(ptc.CrankerProtocolRequestBuilder).WithReqBodyEnded().Build()); err != nil { 165 | panic(err) 166 | } 167 | } else { 168 | ptcMsg = ptcReqBuilder.WithReqHasNoBody().Build() 169 | if err := socket.SendText(ptcMsg); err != nil { 170 | panic(err) 171 | } 172 | } 173 | ptcReq := ptc.NewCrankerProtocolRequest(ptcMsg) 174 | if err = p.handleReqWithInterceptors(cliReq, carriers, ptcReq); err != nil { 175 | util.LOG.Errorf("failed to handleReqWithInterceptors, err: %s", err.Error()) 176 | } 177 | if ptc.IsDebugReq(ptcReq) { 178 | util.LOG.Infof("handle -> before proxy forwarding, request is %s", ptcReq.ToProtocolMessage()) 179 | } 180 | } 181 | 182 | func (p *ReverseProxy) setTargetReqHeaders(req *http.Request, headersBuilder *ptc.HeadersBuilder) bool { 183 | connHeaders := req.Header.Values("Connection") 184 | var ( 185 | hasContentLength, 186 | hasTransferEncodingHeader bool 187 | ) 188 | for headerName, headerValues := range req.Header { 189 | hasContentLength = hasContentLength || "content-length" == strings.ToLower(headerName) 190 | hasTransferEncodingHeader = hasContentLength || "transfer-encoding" == strings.ToLower(headerName) 191 | if !p.shouldSendHeaderFromClientToTarget(headerName, connHeaders) { 192 | continue 193 | } 194 | for _, value := range headerValues { 195 | headersBuilder.AppendHeader(headerName, value) 196 | } 197 | } 198 | addProxyForwardingHeaders(headersBuilder, req) 199 | return hasContentLength || hasTransferEncodingHeader 200 | 201 | } 202 | 203 | func addProxyForwardingHeaders(headers *ptc.HeadersBuilder, req *http.Request) { 204 | xfor := req.RemoteAddr 205 | proto := req.URL.Scheme 206 | host := req.Host 207 | by := ip 208 | headers.AppendHeader("Forwarded", fmt.Sprintf("for=%s;proto=%s;host=%s;by=%s", xfor, proto, host, by)) 209 | if req.Header.Get("X-Forwarded-For") == "" { 210 | headers.AppendHeader("X-Forwarded-For", xfor) 211 | } 212 | if req.Header.Get("X-Forwarded-Proto") == "" { 213 | headers.AppendHeader("X-Forwarded-Proto", proto) 214 | } 215 | if req.Header.Get("X-Forwarded-Host") == "" { 216 | headers.AppendHeader("X-Forwarded-Host", host) 217 | } 218 | if req.Header.Get("X-Forwarded-Server") == "" { 219 | headers.AppendHeader("X-Forwarded-Server", by) 220 | } 221 | } 222 | 223 | func (p *ReverseProxy) shouldSendHeaderFromClientToTarget(headerName string, connHeaders []string) bool { 224 | if util.UUI31223NotSendingHostHeader && "Host" == headerName { 225 | return false 226 | } 227 | if _, ok := p.HopByHopHeadersFields[headerName]; ok { 228 | return false 229 | } 230 | for _, v := range connHeaders { 231 | if v == headerName { 232 | return false 233 | } 234 | } 235 | return true 236 | } 237 | 238 | func (p *ReverseProxy) handleReqWithInterceptors(request *http.Request, carriers *itc.InterceptorStatCarriers, ptcReq *ptc.CrankerProtocolRequest) error { 239 | for _, interceptor := range p.interceptors { 240 | util.LOG.Debugf("applying interceptor %#v on request %s", interceptor, ptcReq) 241 | if carrier, err := interceptor.ApplyOnReq(request, ptcReq); err != nil { 242 | util.LOG.Errorf("failed to apply the plugin [%#v] on request %s, err: %s", interceptor, ptcReq.ToProtocolMessage(), err.Error()) 243 | return err 244 | } else { 245 | carriers.Add(interceptor, carrier) 246 | } 247 | } 248 | return nil 249 | } 250 | 251 | func (p *ReverseProxy) onProxyingError(req *http.Request, crankedSocket *router_socket.RouterSocket, err error) { 252 | errMsg := fmt.Sprintf("failed to proxy API %s being called by client %s through %s, err: %s", 253 | req.URL.String(), req.RemoteAddr, req.Method, err.Error()) 254 | util.LOG.Error(errMsg) 255 | crankedSocket.OnSendOrReceiveDataError(err) 256 | } 257 | 258 | func createReqLine(req *http.Request) string { 259 | // Request-Line Method SP Request-HttpURI SP HTTP-Version CRLF 260 | uri := req.URL.Path 261 | qs := req.URL.RawQuery 262 | if qs != "" { 263 | qs = "?" + qs 264 | } 265 | return req.Method + " " + uri + qs + " HTTP/1.1" 266 | } 267 | 268 | func init() { 269 | bufPool = &sync.Pool{ 270 | New: func() interface{} { 271 | return make([]byte, 4096) // java use 2048 272 | }, 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | ws "github.com/gorilla/websocket" 14 | "github.com/julienschmidt/httprouter" 15 | ptc "github.com/torchcc/crank4go/protocol" 16 | "github.com/torchcc/crank4go/router/api" 17 | "github.com/torchcc/crank4go/router/corsheader_processor" 18 | "github.com/torchcc/crank4go/router/darklaunch_manager" 19 | "github.com/torchcc/crank4go/router/handler" 20 | "github.com/torchcc/crank4go/router/router_socket" 21 | "github.com/torchcc/crank4go/util" 22 | ) 23 | 24 | var upgrader = &ws.Upgrader{ 25 | HandshakeTimeout: 45 * time.Second, 26 | ReadBufferSize: 4096, // default value 27 | WriteBufferSize: 4096, // default value 28 | WriteBufferPool: &sync.Pool{}, 29 | EnableCompression: false, 30 | } 31 | 32 | type Router struct { 33 | routerConfig *RouterConfig 34 | ipValidator Validator 35 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 36 | authPublicKey string 37 | authServiceName string 38 | reqComponentHeader string 39 | websocketFarm *router_socket.WebsocketFarm 40 | RegisterURI *url.URL 41 | HttpURI *url.URL 42 | websocketTLSConfig *tls.Config 43 | webserverTLSConfig *tls.Config 44 | connMonitor *util.ConnectionMonitor 45 | httpServer *http.Server 46 | registrationServer *http.Server 47 | idleTimeout time.Duration 48 | pingInterval time.Duration 49 | routerAvailability *RouterAvailability 50 | corsHeaderProcessor *corsheader_processor.CorsHeaderProcessor 51 | } 52 | 53 | func (r *Router) WebsocketFarm() *router_socket.WebsocketFarm { 54 | return r.websocketFarm 55 | } 56 | 57 | func (r *Router) RouterAvailability() *RouterAvailability { 58 | return r.routerAvailability 59 | } 60 | 61 | // ValidateReq validate register and deregister request 62 | func ValidateReq(ipValidator Validator, req *http.Request) error { 63 | if err := validateIpAddr(ipValidator, req); err != nil { 64 | return err 65 | } else { 66 | return validateCrankerProtocolVersion(req) 67 | } 68 | } 69 | 70 | func validateCrankerProtocolVersion(req *http.Request) error { 71 | version := req.Header.Get("CrankerProtocol") 72 | if flag := ptc.ValidateCrankerProtocolVersion(version); !flag { 73 | msg := fmt.Sprintf("failed to establish websocket connection to cranker connector for nonsupport cranker version: %s, routerName is: %s", version, req.Header.Get("Route")) 74 | util.LOG.Warningf(msg) 75 | return &util.CrankerErr{ 76 | Msg: msg, 77 | Code: http.StatusInternalServerError, 78 | } 79 | } else { 80 | return nil 81 | } 82 | } 83 | 84 | func validateIpAddr(validator Validator, req *http.Request) error { 85 | if !validator.IsValid(req.RemoteAddr) { 86 | msg := fmt.Sprintf("failed to establish websocket connection to cranker connector for invalid ip: %s, routerName is: %s", ip, req.Header.Get("Route")) 87 | util.LOG.Warning(msg) 88 | return &util.CrankerErr{ 89 | Msg: msg, 90 | Code: http.StatusForbidden, 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | func NewRouter(routerConfig *RouterConfig) *Router { 97 | r := &Router{ 98 | routerConfig: routerConfig, 99 | HttpURI: nil, 100 | ipValidator: routerConfig.IpValidator(), 101 | authPublicKey: routerConfig.DarkLaunchPublicKey(), 102 | authServiceName: routerConfig.DarkLaunchServiceName(), 103 | reqComponentHeader: routerConfig.ReqComponentHeader(), 104 | websocketTLSConfig: routerConfig.WebsocketTLSConfig(), 105 | webserverTLSConfig: routerConfig.WebserverTLSConfig(), 106 | darkLaunchManager: routerConfig.darkLaunchManager, 107 | websocketFarm: router_socket.NewWebsocketFarm(routerConfig.ConnMonitor(), routerConfig.DarkLaunchManager()), 108 | connMonitor: routerConfig.ConnMonitor(), 109 | idleTimeout: routerConfig.IdleTimeout(), 110 | pingInterval: routerConfig.PingScheduleInterval(), 111 | corsHeaderProcessor: corsheader_processor.NewCorsHeaderProcessor(routerConfig.CheckOrigin()), 112 | } 113 | 114 | r.routerAvailability = NewRouterAvailability2(r.connMonitor, r.websocketFarm, r.darkLaunchManager, routerConfig.IsShutDownHookAdded()) 115 | theSecureS := "" 116 | if r.webserverTLSConfig != nil { 117 | theSecureS = "s" 118 | } 119 | port := routerConfig.HttpPort() 120 | r.HttpURI, _ = url.Parse("http" + theSecureS + "://" + routerConfig.WebserverInterface() + ":" + strconv.Itoa(port)) 121 | theSecureS = "" 122 | if r.websocketTLSConfig != nil { 123 | theSecureS = "s" 124 | } 125 | r.RegisterURI, _ = url.Parse("ws" + theSecureS + "://" + routerConfig.WebsocketInterface() + ":" + strconv.Itoa(routerConfig.RegistrationWebsocketPort())) 126 | return r 127 | } 128 | 129 | func (r *Router) connectorRegisterToRouter(respWriter http.ResponseWriter, req *http.Request) *router_socket.RouterSocket { 130 | route := getRoute(req) 131 | connectorInstanceID := req.URL.Query().Get("connectorInstanceID") 132 | if connectorInstanceID == "" { 133 | connectorInstanceID = "unknown-" + req.RemoteAddr 134 | } 135 | util.LOG.Info("the register request connectorInstanceID is %s", connectorInstanceID) 136 | routerSocket := router_socket.NewRouterSocket2(route, r.connMonitor, r.websocketFarm, connectorInstanceID, true, req.RemoteAddr, r.corsHeaderProcessor, r.routerConfig.RouterSocketPlugins()) 137 | util.LOG.Infof("got routerSocket %s", routerSocket.String()) 138 | routerSocket.SetOnReadyToAct(func() { 139 | r.websocketFarm.AddWebsocket(route, routerSocket) 140 | }) 141 | 142 | // validation pass can upgrade to websocket now 143 | header := http.Header{} 144 | header.Set("CrankerProtocol", ptc.CrankerProtocolVersion10) 145 | if conn, err := upgrader.Upgrade(respWriter, req, header); err != nil { 146 | util.LOG.Errorf("upgrade error: %s, the requestURI is: %s, remoteAddr is %s", err.Error(), req.URL.String(), req.RemoteAddr) 147 | return nil 148 | } else if conn == nil { 149 | util.LOG.Errorf("upgrade error: got nil conn after upgrade, the requestURI is: %s, remoteAddr is %s", req.URL.String(), req.RemoteAddr) 150 | return nil 151 | } else { 152 | util.LOG.Infof("registered successfully, the connecting remote address is %s", req.RemoteAddr) 153 | go routerSocket.OnWebsocketConnect(conn) 154 | return routerSocket 155 | } 156 | } 157 | 158 | func (r *Router) connectorDeregisterFromRouter(respWriter http.ResponseWriter, req *http.Request) *router_socket.RouterSocket { 159 | route := getRoute(req) 160 | connectorInstanceID := req.URL.Query().Get("connectorInstanceID") 161 | if connectorInstanceID == "" { 162 | connectorInstanceID = "no connector instance id exist" 163 | } 164 | componentName := req.URL.Query().Get("componentName") 165 | if componentName == "" { 166 | componentName = "no component name provided" 167 | } 168 | if connectorInstanceID == "no connector instance id exist" { 169 | util.LOG.Infof("targetName {%s} is using unsupported zero down time connector, socket will not be deregistered") 170 | } else { 171 | util.LOG.Infof("going to deregister... remoteAddr: %s, targetName: %s, componentName: %s, connectorInstanceID: %s", req.RemoteAddr, route, componentName, connectorInstanceID) 172 | r.websocketFarm.DeregisterSocket(route, req.RemoteAddr, connectorInstanceID) 173 | } 174 | routerSocket := router_socket.NewRouterSocket(route, r.connMonitor, r.websocketFarm, connectorInstanceID, false, req.RemoteAddr, r.corsHeaderProcessor) 175 | header := http.Header{} 176 | header.Set("CrankerProtocol", ptc.CrankerProtocolVersion10) 177 | respWriter.Header().Set("CrankerProtocol", ptc.CrankerProtocolVersion10) 178 | 179 | if conn, err := upgrader.Upgrade(respWriter, req, header); err != nil { 180 | util.LOG.Errorf("upgrade error: %s, the requestURI is: %s, remoteAddr is %s", err.Error(), req.URL.String(), req.RemoteAddr) 181 | return nil 182 | } else { 183 | util.LOG.Infof("registered successfully, the connecting remote address is %s", req.RemoteAddr) 184 | go routerSocket.OnWebsocketConnect(conn) 185 | return routerSocket 186 | } 187 | } 188 | 189 | func (r *Router) CreateRegisterHandler() *httprouter.Router { 190 | httpRouter := httprouter.New() 191 | 192 | // use to register 193 | registerWsHandler := handler.NewWebsocketHandler().WithWebsocketFactory(r.registerWebsocketFactory) 194 | registerWsXHandler := handler.NewXHttpHandler(registerWsHandler) 195 | httpRouter.GET("/register", registerWsXHandler.ServeXHTTP) 196 | httpRouter.GET("/register/", registerWsXHandler.ServeXHTTP) 197 | 198 | deregisterWsHandler := handler.NewWebsocketHandler().WithWebsocketFactory(r.deregisterWebsocketFactory) 199 | deregisterWsXHandler := handler.NewXHttpHandler(deregisterWsHandler) 200 | httpRouter.GET("/deregister", deregisterWsXHandler.ServeXHTTP) 201 | httpRouter.GET("/deregister/", deregisterWsXHandler.ServeXHTTP) 202 | 203 | // TODO should add authentication filter to restraint access 204 | darkLaunchServiceResource := api.NewDarkLaunchServiceResource(r.darkLaunchManager) 205 | darkLaunchServiceResource. 206 | AddReqFilters(handler.XHandlerFunc(handler.PreLoggingFilter)). 207 | AddRespFilters(handler.XHandlerFunc(handler.PostLoggingFilter)) 208 | darkLaunchServiceResource.RegisterResourceToHttpRouter(httpRouter, "/api") 209 | 210 | darkLaunchIpResource := api.NewDarkLaunchIpResource(r.darkLaunchManager) 211 | darkLaunchIpResource. 212 | AddReqFilters(handler.XHandlerFunc(handler.PreLoggingFilter)). 213 | AddRespFilters(handler.XHandlerFunc(handler.PostLoggingFilter)) 214 | darkLaunchIpResource.RegisterResourceToHttpRouter(httpRouter, "/api") 215 | 216 | launchGrayToggleResource := api.NewDarkLaunchGrayToggleResource(r.darkLaunchManager) 217 | launchGrayToggleResource. 218 | AddReqFilters(handler.XHandlerFunc(handler.PreLoggingFilter)). 219 | AddRespFilters(handler.XHandlerFunc(handler.PostLoggingFilter)) 220 | launchGrayToggleResource.RegisterResourceToHttpRouter(httpRouter, "/api") 221 | 222 | registrationsResource := api.NewRegistrationsResource(r.websocketFarm) 223 | registrationsResource. 224 | AddReqFilters(handler.XHandlerFunc(handler.PreLoggingFilter)). 225 | AddRespFilters(handler.XHandlerFunc(handler.PostLoggingFilter)) 226 | registrationsResource.RegisterResourceToHttpRouter(httpRouter, "/api") 227 | 228 | return httpRouter 229 | } 230 | 231 | // CreateHttpHandler can configure rate limit here 232 | func (r *Router) CreateHttpHandler() *handler.XHTTPHandler { 233 | return handler.NewXHttpHandler(NewReverseProxy(r.websocketFarm, r.reqComponentHeader, r.routerConfig.ProxyInterceptors())). 234 | AddReqHandlers(handler.XHandlerFunc(handler.PreLoggingFilter)). 235 | AddReqHandlers(r.routerConfig.HandlerList()...). 236 | AddReqHandlers(handler.XHandlerFunc(handler.ReqValidatorFilter)) 237 | } 238 | 239 | func (r *Router) Start() *Router { 240 | serveMux := http.NewServeMux() 241 | serveMux.Handle("/", r.CreateHttpHandler()) 242 | r.httpServer = &http.Server{ 243 | Addr: r.HttpURI.Host, 244 | Handler: serveMux, 245 | TLSConfig: r.webserverTLSConfig, 246 | MaxHeaderBytes: 8192 * 4, 247 | } 248 | util.LOG.Infof("starting router httpServer on %v", r.HttpURI) 249 | go func() { 250 | if r.HttpURI.Scheme == "https" { 251 | util.LOG.Infof("httpServer uses TLS") 252 | if err := r.httpServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { 253 | util.LOG.Warningf("crankerRouter's httpServer failed to start, err: %s", err.Error()) 254 | panic(err) 255 | } 256 | } else { 257 | if err := r.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { 258 | util.LOG.Warningf("crankerRouter's httpServer failed to start, err: %s", err.Error()) 259 | panic(err) 260 | } 261 | } 262 | }() 263 | 264 | r.registrationServer = &http.Server{ 265 | Addr: fmt.Sprintf("%s", r.RegisterURI.Host), 266 | Handler: r.CreateRegisterHandler(), 267 | TLSConfig: r.websocketTLSConfig, 268 | MaxHeaderBytes: 8192 * 4, 269 | } 270 | util.LOG.Infof("starting router registrationServer on %v", r.RegisterURI) 271 | go func() { 272 | if r.RegisterURI.Scheme == "wss" { 273 | util.LOG.Infof("registerServer uses TLS") 274 | if err := r.registrationServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { 275 | util.LOG.Warningf("crankerRouter's registrationServer failed to start, err: %s", err.Error()) 276 | panic(err) 277 | } 278 | } else { 279 | if err := r.registrationServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { 280 | util.LOG.Warningf("crankerRouter's registrationServer failed to start, err: %s", err.Error()) 281 | panic(err) 282 | } 283 | } 284 | }() 285 | return r 286 | } 287 | 288 | func (r *Router) Shutdown() { 289 | var wg sync.WaitGroup 290 | 291 | go func() { 292 | wg.Add(1) 293 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 294 | defer cancel() 295 | if err := r.httpServer.Shutdown(ctx); err != nil { 296 | util.LOG.Warningf("crankerRouter's httpServer shutdown gracefully returned, err: %s", err.Error()) 297 | } 298 | wg.Done() 299 | }() 300 | 301 | go func() { 302 | wg.Add(1) 303 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 304 | defer cancel() 305 | if err := r.registrationServer.Shutdown(ctx); err != nil { 306 | util.LOG.Warningf("crankerRouter's registrationServer shutdown gracefully returned, err: %s", err.Error()) 307 | } 308 | wg.Done() 309 | 310 | }() 311 | wg.Wait() 312 | } 313 | 314 | func getRoute(req *http.Request) string { 315 | return req.Header.Get("Route") 316 | } 317 | 318 | func (r *Router) registerWebsocketFactory(respWriter http.ResponseWriter, req *http.Request) *router_socket.RouterSocket { 319 | util.LOG.Infof("got register request: %s, remote addr is %s, routeName is %s", 320 | req.RequestURI, req.RemoteAddr, req.Header.Get("Route")) 321 | if err := ValidateReq(r.ipValidator, req); err != nil { 322 | e := err.(*util.CrankerErr) 323 | api.RespTextPlainWithStatus(respWriter, e.Error(), e.Code) 324 | return nil 325 | } 326 | return r.connectorRegisterToRouter(respWriter, req) 327 | } 328 | 329 | func (r *Router) deregisterWebsocketFactory(respWriter http.ResponseWriter, req *http.Request) *router_socket.RouterSocket { 330 | if err := ValidateReq(r.ipValidator, req); err != nil { 331 | e := err.(*util.CrankerErr) 332 | api.RespTextPlainWithStatus(respWriter, e.Error(), e.Code) 333 | return nil 334 | } 335 | return r.connectorDeregisterFromRouter(respWriter, req) 336 | } 337 | 338 | // program shutdown hooks 339 | var shutDownHooks []func() 340 | 341 | func addShutDownHook(f func()) { 342 | if shutDownHooks == nil { 343 | shutDownHooks = make([]func(), 0, 8) 344 | } 345 | shutDownHooks = append(shutDownHooks, f) 346 | } 347 | 348 | func exitFunc() { 349 | for _, f := range shutDownHooks { 350 | f() 351 | } 352 | } 353 | 354 | func init() { 355 | util.ExitProgram(exitFunc) 356 | } 357 | 358 | func CreateAndStartRouter(config *RouterConfig) *Router { 359 | router := NewRouter(config).Start() 360 | if config.isShutDownHookAdded { 361 | addShutDownHook(router.Shutdown) 362 | } 363 | return router 364 | } 365 | -------------------------------------------------------------------------------- /router/router_availability.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | protocol "github.com/torchcc/crank4go/protocol" 9 | "github.com/torchcc/crank4go/router/darklaunch_manager" 10 | "github.com/torchcc/crank4go/router/router_socket" 11 | "github.com/torchcc/crank4go/util" 12 | ) 13 | 14 | type RouterAvailability struct { 15 | connMonitor *util.ConnectionMonitor 16 | websocketFarm *router_socket.WebsocketFarm 17 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 18 | cancelPing context.CancelFunc // it's used to cancelPing pingTask 19 | cancelCtx context.Context 20 | } 21 | 22 | func NewRouterAvailability(connMonitor *util.ConnectionMonitor, websocketFarm *router_socket.WebsocketFarm, darkLaunchManager *darklaunch_manager.DarkLaunchManager) *RouterAvailability { 23 | a := &RouterAvailability{connMonitor: connMonitor, websocketFarm: websocketFarm, darkLaunchManager: darkLaunchManager} 24 | a.cancelCtx, a.cancelPing = context.WithCancel(context.Background()) 25 | return a 26 | } 27 | 28 | func NewRouterAvailability2(connMonitor *util.ConnectionMonitor, websocketFarm *router_socket.WebsocketFarm, darkLaunchManager *darklaunch_manager.DarkLaunchManager, isShutDownHookAdded bool) *RouterAvailability { 29 | a := &RouterAvailability{ 30 | connMonitor: connMonitor, 31 | websocketFarm: websocketFarm, 32 | darkLaunchManager: darkLaunchManager, 33 | } 34 | a.cancelCtx, a.cancelPing = context.WithCancel(context.Background()) 35 | if isShutDownHookAdded { 36 | // shutdown ping. 37 | } 38 | return a 39 | 40 | } 41 | 42 | func (a *RouterAvailability) Status() map[string]interface{} { 43 | serviceRegisterMapping := make(map[string]interface{}) 44 | sockets := a.websocketFarm.AllSockets() 45 | 46 | for serviceName, socketQueue := range sockets { 47 | remoteAddr := make(map[string]int) 48 | for _, routerSocket := range socketQueue { 49 | getRemoteAddrMapping(remoteAddr, routerSocket) 50 | } 51 | serviceRegisterMapping[serviceName] = remoteAddr 52 | } 53 | 54 | catchallRemoteAddr := make(map[string]int) 55 | allCatchall := a.websocketFarm.AllCatchall() 56 | for _, routerSocket := range allCatchall { 57 | getRemoteAddrMapping(catchallRemoteAddr, routerSocket) 58 | } 59 | serviceRegisterMapping["default"] = catchallRemoteAddr 60 | 61 | status := make(map[string]interface{}) 62 | status["CrankerProtocol"] = protocol.CrankerProtocolVersion10 63 | status["activeConnections"] = a.connMonitor.ConnectionCount() 64 | status["openFiles"] = a.connMonitor.OpenFiles() 65 | status["Services Register Map"] = serviceRegisterMapping 66 | status["services"] = "/health/connectors" 67 | status["darkMode"] = a.darkLaunchManager 68 | status["isAvailable"] = true 69 | return status 70 | } 71 | 72 | func (a *RouterAvailability) ServicesCategorizedDetail() map[string]interface{} { 73 | servicesConnState := make(map[string]interface{}) 74 | catchall := a.websocketFarm.CategorizedAllSockets() 75 | for category, routerSocketMap := range catchall { 76 | state := make(map[string]interface{}) 77 | servicesConnState[category] = state 78 | for serviceName, routerSocketQueue := range routerSocketMap { 79 | serviceConnectors := make([]interface{}, 0, 8) 80 | state[serviceName] = map[string]interface{}{"name": serviceName, "connectors": serviceConnectors} 81 | remoteAddr := make(map[string]struct{}) 82 | for _, routerSocket := range routerSocketQueue { 83 | getServicesMap(remoteAddr, routerSocket, &serviceConnectors) 84 | } 85 | } 86 | } 87 | 88 | serviceConnectorsMap := make(map[string]interface{}) 89 | servicesConnState["default"] = map[string]interface{}{"name": "default", "connectors": serviceConnectorsMap} 90 | for category, routerSocketQueue := range a.websocketFarm.CategorizedAllCatchall() { 91 | serviceConnectors := make([]interface{}, 0, 8) 92 | catchallRemoteAddr := make(map[string]struct{}) 93 | for _, routerSocket := range routerSocketQueue { 94 | getServicesMap(catchallRemoteAddr, routerSocket, &serviceConnectors) 95 | } 96 | serviceConnectorsMap[category] = serviceConnectors 97 | } 98 | 99 | return servicesConnState 100 | } 101 | 102 | func (a *RouterAvailability) Services() map[string]interface{} { 103 | servicesConnState := make(map[string]interface{}) 104 | for serviceName, routerSocketQueue := range a.websocketFarm.AllSockets() { 105 | serviceConnectors := make([]interface{}, 0, 8) 106 | remoteAddr := make(map[string]struct{}) 107 | for _, routerSocket := range routerSocketQueue { 108 | getServicesMap(remoteAddr, routerSocket, &serviceConnectors) 109 | } 110 | servicesConnState[serviceName] = map[string]interface{}{"name": serviceName, "connectors": serviceConnectors} 111 | } 112 | 113 | serviceConnectors := make([]interface{}, 0, 8) 114 | catchallRemoteAddr := make(map[string]struct{}) 115 | for _, routerSocket := range a.websocketFarm.AllCatchall() { 116 | getServicesMap(catchallRemoteAddr, routerSocket, &serviceConnectors) 117 | } 118 | servicesConnState["default"] = map[string]interface{}{"name": "default", "connectors": serviceConnectors} 119 | return servicesConnState 120 | } 121 | 122 | func (a *RouterAvailability) scheduleSendPingToConnector() { 123 | 124 | go func(ctx context.Context) { 125 | LOOP: 126 | for { 127 | a.websocketFarm.DarkSockets().Range(func(key, value interface{}) bool { 128 | route := key.(string) 129 | routerSocketQueue := value.(*router_socket.IterableChan) 130 | a.sendPingToConnector(route, routerSocketQueue) 131 | return true 132 | }) 133 | a.websocketFarm.Sockets().Range(func(key, value interface{}) bool { 134 | route := key.(string) 135 | routerSocketQueue := value.(*router_socket.IterableChan) 136 | a.sendPingToConnector(route, routerSocketQueue) 137 | return true 138 | }) 139 | a.sendPingToConnector("default", a.websocketFarm.Catchall()) 140 | a.sendPingToConnector("default", a.websocketFarm.DarkCatchall()) 141 | 142 | select { 143 | case <-ctx.Done(): 144 | break LOOP 145 | default: 146 | } 147 | time.Sleep(5 * time.Second) 148 | } 149 | 150 | }(a.cancelCtx) 151 | } 152 | 153 | func (a *RouterAvailability) sendPingToConnector(route string, queue *router_socket.IterableChan) { 154 | queue.Range(func(value interface{}) bool { 155 | routerSocket := value.(*router_socket.RouterSocket) 156 | util.LOG.Debugf("inside router availability: going to send ping, routerName: %s, connectorInstanceID: %S", route, routerSocket.ConnectorInstanceID()) 157 | routerSocket.SendPingToConnector() 158 | return true 159 | }) 160 | } 161 | 162 | func (a *RouterAvailability) shutdownPing() { 163 | util.LOG.Infof("going to shutdown ping") 164 | a.cancelPing() 165 | } 166 | 167 | func (a *RouterAvailability) Shutdown() { 168 | a.shutdownPing() 169 | } 170 | 171 | func getServicesMap(remoteAddr map[string]struct{}, routerSocket *router_socket.RouterSocket, serviceConnectors *[]interface{}) { 172 | curRemoteAddr := strings.Split(routerSocket.RemoteAddr(), ":")[0] 173 | if _, ok := remoteAddr[curRemoteAddr]; ok { 174 | for _, a := range *serviceConnectors { 175 | serviceConnector := a.(map[string]interface{}) 176 | if ip, exist := serviceConnector["ip"]; exist && ip.(string) == curRemoteAddr { 177 | connArray := serviceConnector["connections"].([]interface{}) 178 | connArray = append(connArray, addRouterSocketIdByConnector(routerSocket, routerSocket.LastPingTime().String())) 179 | break 180 | } 181 | } 182 | } else { 183 | connPerIp := addRouterSocketIdByConnector(routerSocket, routerSocket.LastPingTime().String()) 184 | conns := make([]interface{}, 0, 8) 185 | conns = append(conns, connPerIp) 186 | connInfo := addConnectorByIp(routerSocket, curRemoteAddr, conns) 187 | *serviceConnectors = append(*serviceConnectors, connInfo) 188 | } 189 | } 190 | 191 | func addConnectorByIp(routerSocket *router_socket.RouterSocket, curRemoteAddr string, conns []interface{}) map[string]interface{} { 192 | return map[string]interface{}{ 193 | "connectorInstanceID": routerSocket.ConnectorInstanceID(), 194 | "ip": curRemoteAddr, 195 | "route": routerSocket.Route, 196 | "component": routerSocket.ReqComponentName(), 197 | "connections": conns, 198 | } 199 | } 200 | 201 | func addRouterSocketIdByConnector(routerSocket *router_socket.RouterSocket, lastPingTime string) map[string]interface{} { 202 | return map[string]interface{}{"socketID": routerSocket.RouterSocketID, "lastPingTime": lastPingTime} 203 | } 204 | 205 | func getRemoteAddrMapping(remoteAddr map[string]int, routerSocket *router_socket.RouterSocket) { 206 | curRemoteAddr := strings.Split(routerSocket.RemoteAddr(), ":")[0] 207 | if _, ok := remoteAddr[curRemoteAddr]; ok { 208 | remoteAddr[curRemoteAddr] += 1 209 | } else { 210 | remoteAddr[curRemoteAddr] = 1 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /router/router_config.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "crypto/tls" 5 | "time" 6 | 7 | "github.com/torchcc/crank4go/router/darklaunch_manager" 8 | "github.com/torchcc/crank4go/router/handler" 9 | "github.com/torchcc/crank4go/router/interceptor" 10 | "github.com/torchcc/crank4go/router/plugin" 11 | "github.com/torchcc/crank4go/util" 12 | ) 13 | 14 | type RouterConfig struct { 15 | websocketInterface string 16 | webserverInterface string 17 | registrationWebsocketPort int 18 | websocketTLSConfig *tls.Config 19 | webserverTLSConfig *tls.Config 20 | httpPort int 21 | connMonitor *util.ConnectionMonitor 22 | handlerList []handler.XHandler 23 | ipValidator Validator 24 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 25 | darkLaunchPublicKey string 26 | darkLaunchServiceName string 27 | // indicates which key to found requestComponentName from the Header obj of *http.Request 28 | reqComponentHeader string 29 | checkOrigin func(string) bool 30 | proxyInterceptors []interceptor.ProxyInterceptor 31 | routerSocketPlugins []plugin.RouterSocketPlugin 32 | isShutDownHookAdded bool 33 | idleTimeout time.Duration 34 | pingScheduleInterval time.Duration 35 | } 36 | 37 | func (r *RouterConfig) PingScheduleInterval() time.Duration { 38 | return r.pingScheduleInterval 39 | } 40 | 41 | func (r *RouterConfig) IdleTimeout() time.Duration { 42 | return r.idleTimeout 43 | } 44 | 45 | func (r *RouterConfig) DarkLaunchManager() *darklaunch_manager.DarkLaunchManager { 46 | return r.darkLaunchManager 47 | } 48 | 49 | func (r *RouterConfig) DarkLaunchPublicKey() string { 50 | return r.darkLaunchPublicKey 51 | } 52 | 53 | func (r *RouterConfig) IpValidator() Validator { 54 | return r.ipValidator 55 | } 56 | 57 | // NewRouterConfig Mandatory values are set in constructor; optional values are set by setters. 58 | // @param websocketInterface e.g. 0.0.0.0 59 | // @param webserverInterface e.g. 0.0.0.0 60 | // @param registrationWebsocketPort the port that crank which connector make websocket connect to 61 | // @param websocketTLSConfig 62 | // @param webserverTLSConfig 63 | // @param httpPorts the port to point your browser at. https://hostname:port 64 | // @return *RouterConfig 65 | func NewRouterConfig(websocketInterface, webserverInterface string, registrationWebsocketPort, httpPort int, websocketTLSConfig, webserverTLSConfig *tls.Config) *RouterConfig { 66 | return NewRouterConfig2(websocketInterface, webserverInterface, registrationWebsocketPort, httpPort, websocketTLSConfig, webserverTLSConfig, nil, nil) 67 | } 68 | 69 | func NewRouterConfig2(websocketInterface, webserverInterface string, registrationWebsocketPort, httpPort int, 70 | websocketTLSConfig, webserverTLSConfig *tls.Config, 71 | routerSocketPlugins []plugin.RouterSocketPlugin, proxyInterceptors []interceptor.ProxyInterceptor) *RouterConfig { 72 | config := &RouterConfig{ 73 | websocketInterface: websocketInterface, 74 | webserverInterface: webserverInterface, 75 | registrationWebsocketPort: registrationWebsocketPort, 76 | websocketTLSConfig: websocketTLSConfig, 77 | webserverTLSConfig: webserverTLSConfig, 78 | handlerList: make([]handler.XHandler, 0, 8), 79 | httpPort: httpPort, 80 | proxyInterceptors: proxyInterceptors, 81 | routerSocketPlugins: routerSocketPlugins, 82 | isShutDownHookAdded: true, 83 | ipValidator: &IpValidator{}, 84 | } 85 | 86 | if config.proxyInterceptors == nil { 87 | config.proxyInterceptors = make([]interceptor.ProxyInterceptor, 0, 0) 88 | } 89 | if config.routerSocketPlugins == nil { 90 | config.routerSocketPlugins = make([]plugin.RouterSocketPlugin, 0, 0) 91 | } 92 | return config 93 | } 94 | 95 | func (r *RouterConfig) RouterSocketPlugins() []plugin.RouterSocketPlugin { 96 | return r.routerSocketPlugins 97 | } 98 | 99 | func (r *RouterConfig) ProxyInterceptors() []interceptor.ProxyInterceptor { 100 | return r.proxyInterceptors 101 | } 102 | 103 | func (r *RouterConfig) CheckOrigin() func(string) bool { 104 | return r.checkOrigin 105 | } 106 | 107 | func (r *RouterConfig) SetCheckOrigin(checkOrigin func(string) bool) *RouterConfig { 108 | r.checkOrigin = checkOrigin 109 | return r 110 | } 111 | 112 | func (r *RouterConfig) SetReqComponentHeader(reqComponentHeader string) *RouterConfig { 113 | r.reqComponentHeader = reqComponentHeader 114 | return r 115 | } 116 | 117 | /* 118 | use to log request component name 119 | */ 120 | func (r *RouterConfig) ReqComponentHeader() string { 121 | return r.reqComponentHeader 122 | } 123 | 124 | func (r *RouterConfig) DarkLaunchServiceName() string { 125 | return r.darkLaunchServiceName 126 | } 127 | 128 | /* 129 | path is where to store the dark ip config in disk 130 | */ 131 | func (r *RouterConfig) SetupDarkLaunchManagerWithPath(path string) { 132 | r.darkLaunchManager = darklaunch_manager.NewDarkLaunchManager2(path) 133 | } 134 | 135 | /* 136 | darkLaunchPublicKey is used to authorize for traffic control in DarkLaunchHandler 137 | without darkLaunchPublicKey DarkLaunchHandler will return 500 138 | darkLaunchServiceName is used for authorization. if input header decode name is not equal to darkLaunchServiceName, then DarkLaunchHandler will return 500 139 | */ 140 | func (r *RouterConfig) ConfigDarkLaunch(darkLaunchPublicKey, darkLaunchServiceName string) *RouterConfig { 141 | r.darkLaunchPublicKey = darkLaunchPublicKey 142 | r.darkLaunchServiceName = darkLaunchServiceName 143 | return r 144 | } 145 | 146 | func (r *RouterConfig) ConnMonitor() *util.ConnectionMonitor { 147 | return r.connMonitor 148 | } 149 | 150 | /** 151 | * @Description: send metrics to various data publishers 152 | * @receiver r 153 | * @param connMonitor 154 | */ 155 | func (r *RouterConfig) SetConnMonitor(connMonitor *util.ConnectionMonitor) *RouterConfig { 156 | r.connMonitor = connMonitor 157 | return r 158 | } 159 | 160 | func (r *RouterConfig) HttpPort() int { 161 | return r.httpPort 162 | } 163 | 164 | func (r *RouterConfig) RegistrationWebsocketPort() int { 165 | return r.registrationWebsocketPort 166 | } 167 | 168 | func (r *RouterConfig) IsShutDownHookAdded() bool { 169 | return r.isShutDownHookAdded 170 | } 171 | 172 | /** 173 | * @Description: a parameter that determines if router shutdown Hook will be called before shutdown. 174 | * @receiver r 175 | * @param addShutDownHook 176 | */ 177 | func (r *RouterConfig) SetIsShutDownHookAdded(addShutDownHook bool) *RouterConfig { 178 | r.isShutDownHookAdded = addShutDownHook 179 | return r 180 | } 181 | 182 | func (r *RouterConfig) HandlerList() []handler.XHandler { 183 | return r.handlerList 184 | } 185 | 186 | /** 187 | * @Description: a list of request handlers that will be called before the request bis passed to cranker. 188 | * this lets you observe, change, or filter requests. to stop a request from sending to cranker, return true is your handler 189 | * @receiver r 190 | * @param handlerList 191 | */ 192 | func (r *RouterConfig) SetHandlerList(handlerList []handler.XHandler) { 193 | r.handlerList = handlerList 194 | } 195 | 196 | func (r *RouterConfig) WebserverInterface() string { 197 | return r.webserverInterface 198 | } 199 | 200 | func (r *RouterConfig) WebsocketInterface() string { 201 | return r.websocketInterface 202 | } 203 | 204 | func (r *RouterConfig) WebsocketTLSConfig() *tls.Config { 205 | return r.websocketTLSConfig 206 | } 207 | 208 | func (r *RouterConfig) WebserverTLSConfig() *tls.Config { 209 | return r.webserverTLSConfig 210 | } 211 | -------------------------------------------------------------------------------- /router/router_socket/iterable_chan.go: -------------------------------------------------------------------------------- 1 | package router_socket 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/torchcc/crank4go/util" 10 | ) 11 | 12 | const defaultChanCapacity = 200 13 | 14 | // IterableChan iterable chan to mock LinkedBlockingQueue 15 | type IterableChan struct { 16 | ch chan *RouterSocket 17 | aliveSocketSet *sync.Map // map[*RouterSocket]struct{} 18 | // the number of alive sockets 19 | length int32 20 | } 21 | 22 | func (c *IterableChan) AliveSocketSlice() []*RouterSocket { 23 | slice := make([]*RouterSocket, 0, 8) 24 | c.aliveSocketSet.Range(func(key, _ interface{}) bool { 25 | socket := key.(*RouterSocket) 26 | slice = append(slice, socket) 27 | return true 28 | }) 29 | return slice 30 | } 31 | 32 | // IsEmpty indicates if there is element in c.ch 33 | func (c *IterableChan) IsEmpty() bool { 34 | return len(c.ch) == 0 35 | 36 | } 37 | 38 | func (c *IterableChan) IsAliveSocketSetEmpty() bool { 39 | return c.LenAlive() == 0 40 | } 41 | 42 | // Len the total element (including dead socket) in c.ch 43 | func (c *IterableChan) Len() int { 44 | return len(c.ch) 45 | } 46 | 47 | // LenAlive return the len of the current LinkedBlockingQueue 48 | func (c *IterableChan) LenAlive() int { 49 | return int(atomic.LoadInt32(&c.length)) 50 | } 51 | 52 | func (c *IterableChan) incrementAliveSocketNumber() { 53 | atomic.AddInt32(&c.length, 1) 54 | } 55 | 56 | func (c *IterableChan) decrementAliveSocketNumber() { 57 | atomic.AddInt32(&c.length, -1) 58 | } 59 | 60 | // Remove remove a given alive item from the LinkedBlockingQueue, 61 | // if the given socket is nil, it is a no-op 62 | // if the given socket is not in the queue, close the socket 63 | // bool: indicates if the alive socket set has changed or not after return 64 | func (c *IterableChan) Remove(socket *RouterSocket) bool { 65 | if socket == nil { 66 | return false 67 | } 68 | if _, loaded := c.aliveSocketSet.LoadAndDelete(socket); loaded { 69 | c.decrementAliveSocketNumber() 70 | return true 71 | } else { 72 | return false 73 | } 74 | } 75 | 76 | // Offer it is non-blocking 77 | func (c *IterableChan) Offer(socket *RouterSocket) { 78 | if socket == nil { 79 | return 80 | } 81 | go func() { 82 | for { 83 | select { 84 | case c.ch <- socket: 85 | c.aliveSocketSet.Store(socket, struct{}{}) 86 | c.incrementAliveSocketNumber() 87 | return 88 | case <-time.After(5 * time.Minute): 89 | util.LOG.Warningf("can not push socket into channel, the channel of blocking queue is full, trying again in 5 min") 90 | } 91 | } 92 | }() 93 | } 94 | 95 | // Poll it is non-blocking, return the first VALID item of a LinkedBlockingQueue, it the queue is empty, return nil 96 | func (c *IterableChan) Poll() (socket *RouterSocket) { 97 | // do not delete for loop 98 | for { 99 | select { 100 | case socket = <-c.ch: 101 | if _, loaded := c.aliveSocketSet.LoadAndDelete(socket); loaded { 102 | c.decrementAliveSocketNumber() 103 | return socket 104 | } else { // is dead socket 105 | util.LOG.Warningf("Poll: got a Dead socket from channel, dropping it and going to poll again") 106 | } 107 | default: 108 | return nil 109 | } 110 | } 111 | } 112 | 113 | // PollTimeout if there is no alive socket util timeout, nil will be returned 114 | func (c *IterableChan) PollTimeout(timeout time.Duration) (socket *RouterSocket) { 115 | begin := time.Now() 116 | 117 | LOOP: 118 | select { 119 | case socket = <-c.ch: 120 | if _, loaded := c.aliveSocketSet.LoadAndDelete(socket); loaded { 121 | c.decrementAliveSocketNumber() 122 | util.LOG.Debugf("polled a socket from channel, socket: %s", socket.String()) 123 | return socket 124 | } 125 | case <-time.After(timeout): 126 | util.LOG.Warningf("PollTimeout: case timeout") 127 | return nil 128 | } 129 | // is dead socket 130 | if time.Now().Sub(begin) > timeout { 131 | util.LOG.Warningf("PollTimeout, time out!: returning nil") 132 | return nil 133 | } 134 | util.LOG.Infof("got a dead socket, going to poll again...") 135 | goto LOOP 136 | } 137 | 138 | func (c *IterableChan) Range(f func(value interface{}) bool) { 139 | c.aliveSocketSet.Range(func(key, _ interface{}) bool { 140 | return f(key) 141 | 142 | }) 143 | } 144 | 145 | // NewIterableChan capacity for alive sockets and dead sockets 146 | func NewIterableChan(capacity int) *IterableChan { 147 | if capacity < 0 { 148 | panic(errors.New("IllegalArgumentError: capacity should not be negative")) 149 | } 150 | if capacity == 0 { 151 | capacity = defaultChanCapacity 152 | } 153 | return &IterableChan{ 154 | ch: make(chan *RouterSocket, capacity), 155 | aliveSocketSet: &sync.Map{}, 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /router/router_socket/router_socket.go: -------------------------------------------------------------------------------- 1 | package router_socket 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/google/uuid" 12 | ws "github.com/gorilla/websocket" 13 | ptc "github.com/torchcc/crank4go/protocol" 14 | "github.com/torchcc/crank4go/router/corsheader_processor" 15 | "github.com/torchcc/crank4go/router/plugin" 16 | "github.com/torchcc/crank4go/util" 17 | ) 18 | 19 | const Authorization = "authorization" 20 | const writeWait = time.Second 21 | 22 | // RouterSocket It is a socket on router side for connector to send response content 23 | type RouterSocket struct { 24 | RespHeadersNotSendBack map[string]struct{} 25 | Route string 26 | RouterSocketID string 27 | connectorInstanceID string 28 | connMonitor *util.ConnectionMonitor 29 | websocketFarm *WebsocketFarm 30 | isRegister bool // true if the socket is from a registration to server HTTP request, false if it's a deRegistration of a connector 31 | corsHeaderProcessor *corsheader_processor.CorsHeaderProcessor 32 | session *ws.Conn 33 | respWriter http.ResponseWriter 34 | req *http.Request 35 | handleDone *sync.WaitGroup 36 | onReadyToAct func() 37 | remoteAddr string 38 | isRemoved bool 39 | hasResp bool 40 | lastPingTime time.Time 41 | reqStartTime time.Time 42 | bytesReceived int64 43 | bytesSent int64 44 | ip string 45 | reqComponentName string 46 | routerSocketPlugins []plugin.RouterSocketPlugin 47 | } 48 | 49 | func NewRouterSocket(route string, connMonitor *util.ConnectionMonitor, websocketFarm *WebsocketFarm, 50 | connectorInstanceID string, isRegister bool, ip string, corsHeaderProcessor *corsheader_processor.CorsHeaderProcessor) *RouterSocket { 51 | return NewRouterSocket2(route, connMonitor, websocketFarm, connectorInstanceID, isRegister, ip, corsHeaderProcessor, nil) 52 | } 53 | 54 | func NewRouterSocket2(route string, connMonitor *util.ConnectionMonitor, websocketFarm *WebsocketFarm, 55 | connectorInstanceID string, isRegister bool, ip string, corsHeaderProcessor *corsheader_processor.CorsHeaderProcessor, routerSocketPlugins []plugin.RouterSocketPlugin) *RouterSocket { 56 | s := &RouterSocket{ 57 | RespHeadersNotSendBack: map[string]struct{}{"server": {}}, 58 | Route: route, 59 | RouterSocketID: uuid.New().String(), 60 | connectorInstanceID: connectorInstanceID, 61 | connMonitor: connMonitor, 62 | websocketFarm: websocketFarm, 63 | isRegister: isRegister, 64 | corsHeaderProcessor: corsHeaderProcessor, 65 | ip: ip, 66 | } 67 | if !util.UUI51288AllowFixedLengthResponses { 68 | s.RespHeadersNotSendBack["content-length"] = struct{}{} 69 | } 70 | if routerSocketPlugins != nil { 71 | s.routerSocketPlugins = routerSocketPlugins 72 | } else { 73 | s.routerSocketPlugins = make([]plugin.RouterSocketPlugin, 0, 0) 74 | } 75 | util.LOG.Debugf("an adding websocket request received, routerName=%s, routerSocketID=%s", route, s.RouterSocketID) 76 | return s 77 | } 78 | 79 | func (s *RouterSocket) String() string { 80 | return fmt.Sprintf("RouterSocket{route=%s, routerSocketID=%s, connectorInstanceID=%s, isRegister=%v, ip=%s, requestComponentName=%s}", 81 | s.Route, s.RouterSocketID, s.connectorInstanceID, s.isRegister, s.ip, s.reqComponentName) 82 | } 83 | 84 | func (s *RouterSocket) IsCatchAll() bool { 85 | return s.Route == "" 86 | } 87 | 88 | // RemoteAddr the websocket connection's client peer's host and port 89 | func (s *RouterSocket) RemoteAddr() string { 90 | return s.remoteAddr 91 | } 92 | 93 | func (s *RouterSocket) LastPingTime() time.Time { 94 | return s.lastPingTime 95 | } 96 | 97 | func (s *RouterSocket) ConnectorInstanceID() string { 98 | return s.connectorInstanceID 99 | } 100 | 101 | func (s *RouterSocket) ReqComponentName() string { 102 | return s.reqComponentName 103 | } 104 | 105 | func (s *RouterSocket) SetReqComponentName(reqComponentName string) { 106 | s.reqComponentName = reqComponentName 107 | } 108 | 109 | func (s *RouterSocket) Ip() string { 110 | return s.ip 111 | } 112 | 113 | func (s *RouterSocket) SetResponse(respWriter http.ResponseWriter, req *http.Request, handleDone *sync.WaitGroup) { 114 | s.connMonitor.OnConnectionStarted2(s.Route, s.RouterSocketID) 115 | s.req = req 116 | s.respWriter = respWriter 117 | s.handleDone = handleDone 118 | s.reqStartTime = time.Now() 119 | } 120 | 121 | func (s *RouterSocket) SetOnReadyToAct(action func()) { 122 | s.onReadyToAct = action 123 | } 124 | 125 | func (s *RouterSocket) SendPingToConnector() { 126 | if s.session != nil { 127 | util.LOG.Debugf("sending ping message... routerName=%s, routerSocketID=%s, lastPingTime=%#v, connectorInstanceID=%s", 128 | s.Route, s.RouterSocketID, s.lastPingTime, s.connectorInstanceID) 129 | if err := s.session.WriteControl(ws.PingMessage, []byte("*ping*_%"), time.Now().Add(time.Second)); err != nil { 130 | util.LOG.Debugf("failed to send ping, killing bad websocket... routerSocketID: %s, err: %s", s.RouterSocketID, err.Error()) 131 | s.removeBadWebsocket() 132 | } 133 | s.lastPingTime = time.Now() 134 | } 135 | } 136 | 137 | // OnWebsocketClose A Close Event was received. The underlying Connection will be considered closed at this point. 138 | func (s *RouterSocket) OnWebsocketClose(statusCode int, reason string) error { 139 | util.LOG.Debugf("router side got closeMessage, statusCode=%d, reason=%s, routerName=%s, routerSocketID=%s", 140 | statusCode, reason, s.Route, s.RouterSocketID) 141 | s.session = nil 142 | // status code: https://tools.ietf.org/html/rfc6455#section-7.4.1 143 | if s.respWriter != nil { 144 | s.connMonitor.OnConnectionEnded3(s.RouterSocketID, s.Route, s.reqComponentName, 200, 145 | time.Now().Sub(s.reqStartTime).Milliseconds(), s.bytesSent, s.bytesReceived) 146 | if statusCode == ws.CloseInternalServerErr { 147 | s.respWriter.WriteHeader(http.StatusBadGateway) 148 | util.LOG.Debugf("client response is %#v, routerName=%s, routerSocketID=%s", s.respWriter, s.Route, s.RouterSocketID) 149 | } else if statusCode == ws.ClosePolicyViolation { 150 | s.respWriter.WriteHeader(http.StatusBadRequest) 151 | util.LOG.Debugf("client response is %#v, routerName=%s, routerSocketID=%s", s.respWriter, s.Route, s.RouterSocketID) 152 | } 153 | } 154 | if s.handleDone != nil { 155 | s.handleDone.Done() 156 | s.handleDone = nil 157 | } 158 | if s.isRegister && !s.isRemoved { 159 | util.LOG.Debugf("going to remove socket, statusCode=%d, reason=%s, routerName=%s, routerSocketID=%s", 160 | statusCode, reason, s.Route, s.RouterSocketID) 161 | s.websocketFarm.RemoveWebsocket(s.Route, s) 162 | s.isRemoved = true 163 | } 164 | return nil 165 | } 166 | 167 | func (s *RouterSocket) OnWebsocketConnect(session *ws.Conn) { 168 | s.session = session 169 | s.remoteAddr = session.RemoteAddr().String() 170 | if s.isRegister { 171 | s.onReadyToAct() 172 | } 173 | s.runForever(s.session) 174 | } 175 | 176 | func (s *RouterSocket) OnWebsocketText(msg string) { 177 | util.LOG.Debugf("cranker router socket received response from service connector onWebsocketText=%s", msg) 178 | if s.respWriter != nil { 179 | atomic.AddInt64(&s.bytesReceived, int64(len(msg))) 180 | ptcResp := ptc.NewCrankerProtocolResponse(msg) 181 | if ptc.IsDebugResp(ptcResp) { 182 | util.LOG.Infof("onWebsocketText: cranker router receive response from service connector,"+ 183 | " routeName: %s, routerSocketID: %s, msg: %s", s.Route, s.RouterSocketID, msg) 184 | } 185 | util.LOG.Debugf("onWebsocketText: cranker router receive response from service connector,"+ 186 | " routeName: %s, routerSocketID: %s, msg: %s, response status: %s", s.Route, s.RouterSocketID, msg, ptcResp.Status()) 187 | 188 | for _, p := range s.routerSocketPlugins { 189 | if err := p.HandleAfterRespReceived(ptcResp); err != nil { 190 | util.LOG.Errorf("failed to apply the plugin [%#v] on response %s, err: %s", p, ptcResp.ToProtocolMessage(), err.Error()) 191 | } 192 | } 193 | s.putHeadersTo(ptcResp) 194 | s.respWriter.WriteHeader(ptcResp.Status()) 195 | // s.corsHeaderProcessor.Process(s.req, s.respWriter) 196 | } 197 | } 198 | 199 | // OnWebsocketBinary please make sure the there is available data in buf before calling this method 200 | func (s *RouterSocket) OnWebsocketBinary(buf []byte) { 201 | atomic.AddInt64(&s.bytesReceived, int64(len(buf))) 202 | util.LOG.Debugf("router with routerName: %s, routerSocketID: %s is sending %d bytes to connector", 203 | s.Route, s.RouterSocketID, len(buf)) 204 | 205 | if _, err := s.respWriter.Write(buf); err != nil { 206 | util.LOG.Errorf("router with routerName: %s, routerSocketID: %s cannot write to client response writer "+ 207 | "(maybe the user closed their browser) so the request is cancelling. err: %s", s.Route, s.RouterSocketID, err.Error()) 208 | if s.handleDone != nil { 209 | util.LOG.Debugf("going to done, socketID: %s", s.RouterSocketID) 210 | s.handleDone.Done() // just in case router side failed to get websocket closeMessage. 211 | s.handleDone = nil 212 | } 213 | s.CloseSocketSession() 214 | } 215 | } 216 | 217 | // OnWebsocketError A WebSocket exception has occurred. 218 | // This is a way for the internal implementation to notify of exceptions occured during the processing of websocket. 219 | // Usually this occurs from bad / malformed incoming packets. (example: bad UTF8 data, frames that are too big, violations of the spec) 220 | // This will result in the {@link Session} being closed by the implementing side. 221 | func (s *RouterSocket) OnWebsocketError(cause error) { 222 | util.LOG.Errorf("websocket error occurs when websocket server side receiving reading msg from session, err: %s", cause.Error()) 223 | s.OnSendOrReceiveDataError(cause) 224 | } 225 | 226 | func (s *RouterSocket) SendText(msg string) error { 227 | atomic.AddInt64(&s.bytesSent, int64(len(msg))) 228 | return s.session.WriteMessage(ws.TextMessage, []byte(msg)) 229 | } 230 | 231 | func (s *RouterSocket) SendData(buf []byte) error { 232 | atomic.AddInt64(&s.bytesSent, int64(len(buf))) 233 | return s.session.WriteMessage(ws.BinaryMessage, buf) 234 | } 235 | 236 | // OnSendOrReceiveDataError this will be called when reverseProxy failed too send textMessage or binaryMessage to connector 237 | func (s *RouterSocket) OnSendOrReceiveDataError(err error) { 238 | s.removeBadWebsocket() 239 | errMsg := err.Error() 240 | if strings.Contains(errMsg, "timeout") { 241 | util.LOG.Warning("hit timeout err when sending data from router to connector, err: %s", err) 242 | if s.respWriter != nil { 243 | s.respWriter.WriteHeader(http.StatusGatewayTimeout) 244 | if _, e := s.respWriter.Write([]byte(fmt.Sprintf("504 Gateway Timeout, err: %s", err.Error()))); e != nil { 245 | util.LOG.Error("failed to write response from router reverseProxy to client, err: %s", err.Error()) 246 | } 247 | } 248 | } else { 249 | if s.respWriter != nil { 250 | s.respWriter.WriteHeader(http.StatusBadGateway) 251 | if _, e := s.respWriter.Write([]byte(fmt.Sprintf("502 Bad Gateway, err: %s", err.Error()))); e != nil { 252 | util.LOG.Error("failed to write response from router reverseProxy to client, err: %s", err.Error()) 253 | } 254 | } 255 | } 256 | if s.handleDone != nil { 257 | util.LOG.Debugf("OnSendOrReceiveDataError: going to done, socketID: %s", s.RouterSocketID) 258 | s.handleDone.Done() 259 | s.handleDone = nil 260 | } 261 | } 262 | 263 | func (s *RouterSocket) removeBadWebsocket() { 264 | if !s.isRemoved { 265 | s.CloseSocketSession() 266 | s.websocketFarm.RemoveWebsocket(s.Route, s) 267 | s.isRemoved = true 268 | } 269 | } 270 | 271 | func (s *RouterSocket) CloseSocketSession() { 272 | util.LOG.Debugf("closing socketSession %s ...", s.String()) 273 | if s.session != nil { 274 | _ = s.session.WriteControl(ws.CloseGoingAway, []byte("Going away"), time.Now().Add(time.Second)) 275 | s.session = nil 276 | } 277 | } 278 | 279 | // response statusCode must be written after this function is called, as is said by golang http package 280 | func (s *RouterSocket) putHeadersTo(ptcResp *ptc.CrankerProtocolResponse) { 281 | for _, line := range ptcResp.Headers { 282 | if pos := strings.Index(line, ":"); pos > 0 { 283 | header := line[:pos] 284 | if _, ok := s.RespHeadersNotSendBack[strings.ToLower(header)]; !ok && s.respWriter != nil { 285 | value := line[pos+1:] 286 | v := value 287 | if Authorization == strings.ToLower(header) { 288 | v = strings.Split(value, ":")[0] 289 | } 290 | util.LOG.Debugf("sending client response header %s=%s", header, v) 291 | s.respWriter.Header().Add(header, value) 292 | } 293 | } 294 | } 295 | s.respWriter.Header().Add("Via", "1.1 crnk") 296 | } 297 | 298 | func (s *RouterSocket) CorsHeaderProcessor() *corsheader_processor.CorsHeaderProcessor { 299 | return s.corsHeaderProcessor 300 | } 301 | 302 | func (s *RouterSocket) runForever(conn *ws.Conn) { 303 | for { 304 | // msgType must be either TextMessage or BinaryMessage 305 | msgType, msg, err := conn.ReadMessage() 306 | if err != nil { 307 | if closeErr, ok := err.(*ws.CloseError); !ok { // conn polling got CloseMessage, execute closeHandler, read again and got CloseError, so if we got CloseError, just ignore it 308 | s.OnWebsocketError(err) 309 | } else { 310 | _ = s.OnWebsocketClose(closeErr.Code, closeErr.Text) 311 | } 312 | return 313 | } 314 | if msgType == ws.TextMessage { 315 | s.OnWebsocketText(string(msg)) 316 | } else if msgType == ws.BinaryMessage { 317 | s.OnWebsocketBinary(msg) 318 | } else { 319 | util.LOG.Errorf("unexpected msgType got: %d", msgType) 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /router/router_socket/websocket_farm.go: -------------------------------------------------------------------------------- 1 | package router_socket 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/torchcc/crank4go/router/darklaunch_manager" 10 | "github.com/torchcc/crank4go/util" 11 | ) 12 | 13 | const blockingQueueCapacity = 0 // use default value 14 | 15 | type WebsocketFarm struct { 16 | sockets *sync.Map // in format of map[string]*IterableChan this Map is like java's ConcurrentHashMap 17 | darkSockets *sync.Map // in format of map[string]*IterableChan 18 | catchall *IterableChan 19 | darkCatchall *IterableChan 20 | connMonitor *util.ConnectionMonitor 21 | timeSpent int64 22 | socketAcquireTime time.Duration 23 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 24 | } 25 | 26 | func NewWebsocketFarm(connMonitor *util.ConnectionMonitor, darkLaunchManager *darklaunch_manager.DarkLaunchManager) *WebsocketFarm { 27 | f := &WebsocketFarm{ 28 | sockets: &sync.Map{}, 29 | darkSockets: &sync.Map{}, 30 | catchall: NewIterableChan(blockingQueueCapacity), 31 | darkCatchall: NewIterableChan(blockingQueueCapacity), 32 | connMonitor: connMonitor, 33 | timeSpent: 0, 34 | socketAcquireTime: time.Second * 15, 35 | darkLaunchManager: darkLaunchManager, 36 | } 37 | listener := &darkListener{ 38 | sockets: f.sockets, 39 | darkSockets: f.darkSockets, 40 | catchall: f.catchall, 41 | darkCatchAll: f.darkCatchall, 42 | } 43 | darkLaunchManager.SetIpListener(listener).SetServiceListener(listener) 44 | return f 45 | } 46 | 47 | func (f *WebsocketFarm) DarkCatchall() *IterableChan { 48 | return f.darkCatchall 49 | } 50 | 51 | func (f *WebsocketFarm) Catchall() *IterableChan { 52 | return f.catchall 53 | } 54 | 55 | func (f *WebsocketFarm) SetSocketAcquireTime(socketAcquireTime time.Duration) { 56 | f.socketAcquireTime = socketAcquireTime 57 | util.LOG.Infof("socketAcquireTime is %v", socketAcquireTime) 58 | } 59 | 60 | func (f *WebsocketFarm) Stop() { 61 | for !f.catchall.IsEmpty() { 62 | if socket := f.catchall.Poll(); socket == nil { 63 | util.LOG.Info("failed to pop socket from catchall linkedBlockingQueue, the queue is empty") 64 | break 65 | } else { 66 | socket.CloseSocketSession() 67 | } 68 | } 69 | f.sockets.Range(func(_, queue interface{}) bool { 70 | socketQueue := queue.(*IterableChan) 71 | for !socketQueue.IsEmpty() { 72 | if sock := socketQueue.Poll(); sock == nil { 73 | util.LOG.Info("failed to pop socket from sockets linkedBlockingQueue, the queue is empty") 74 | break 75 | } else { 76 | sock.CloseSocketSession() 77 | } 78 | } 79 | return true 80 | }) 81 | } 82 | 83 | func (f *WebsocketFarm) RemoveWebsocket(route string, socket *RouterSocket) { 84 | util.LOG.Debugf("removing websocket {%s}, its connectorInstanceID is %s", route, socket.ConnectorInstanceID()) 85 | if f.darkLaunchManager.IsDarkModeOn() && (f.darkLaunchManager.ContainsIp(socket.Ip()) || f.darkLaunchManager.ContainsService(socket.Route)) { 86 | util.LOG.Debugf("dark mode on and current socket %v contains ip %v, contains service %s", socket.String(), 87 | f.darkLaunchManager.ContainsIp(socket.Ip()), f.darkLaunchManager.ContainsService(socket.Route)) 88 | if socket.IsCatchAll() { 89 | f.darkCatchall.Remove(socket) 90 | } else { 91 | if queue, ok := f.darkSockets.Load(route); ok { 92 | queue.(*IterableChan).Remove(socket) 93 | } 94 | } 95 | } else { 96 | util.LOG.Debugf("removing socket %s of route %s", socket.String(), route) 97 | if socket.IsCatchAll() { 98 | f.catchall.Remove(socket) 99 | } else { 100 | if queue, ok := f.sockets.Load(route); ok { 101 | queue.(*IterableChan).Remove(socket) 102 | } 103 | } 104 | } 105 | } 106 | 107 | func (f *WebsocketFarm) AddWebsocket(route string, socket *RouterSocket) { 108 | if route == "" { 109 | route = "*" 110 | } 111 | var queue *IterableChan 112 | if f.darkLaunchManager.IsDarkModeOn() && (f.darkLaunchManager.ContainsIp(socket.Ip()) || f.darkLaunchManager.ContainsService(socket.Route)) { 113 | util.LOG.Debugf("addWebsocket, dork mode is on and current socket is %s, contains ip is %v, "+ 114 | "contains service is %v", socket.String(), f.darkLaunchManager.ContainsIp(socket.Ip()), f.darkLaunchManager.ContainsService(socket.Route)) 115 | if route == "*" { 116 | queue = f.darkCatchall 117 | } else { 118 | if queueInterface, ok := f.darkSockets.Load(route); !ok { 119 | queue = NewIterableChan(blockingQueueCapacity) 120 | f.darkSockets.Store(route, queue) 121 | } else { 122 | queue = queueInterface.(*IterableChan) 123 | } 124 | } 125 | } else { 126 | util.LOG.Debugf("add socket %s of route %s", socket.String(), route) 127 | if route == "*" { 128 | queue = f.catchall 129 | } else { 130 | if queueInterface, ok := f.sockets.Load(route); !ok { 131 | queue = NewIterableChan(blockingQueueCapacity) 132 | f.sockets.Store(route, queue) 133 | } else { 134 | queue = queueInterface.(*IterableChan) 135 | } 136 | } 137 | } 138 | 139 | queue.Offer(socket) 140 | util.LOG.Debugf("websocket added: route=%s, connectorInstanceID=%s, routeSocketID=%s", route, socket.ConnectorInstanceID(), socket.RouterSocketID) 141 | } 142 | 143 | func (f *WebsocketFarm) AcquireSocket(target string, componentName string) (socket *RouterSocket, err error) { 144 | if socket = f.getRouterSocket(target); socket == nil { 145 | util.LOG.Warningf("failed to wait socket for %s, requestComponentName: %s, queue is empty", target, componentName) 146 | return nil, util.TimeoutErr{Msg: fmt.Sprintf("failed to proxy %s, requestComponentName: %s", target, componentName)} 147 | } else { 148 | util.LOG.Infof("socket acquired, target: %s, socket: %s, requestComponentName: %s", target, socket.RouterSocketID, componentName) 149 | socket.SetReqComponentName(componentName) 150 | return socket, nil 151 | } 152 | } 153 | 154 | func (f *WebsocketFarm) getRouterSocket(target string) *RouterSocket { 155 | var ( 156 | sockets *sync.Map = f.sockets 157 | catchAll *IterableChan = f.catchall 158 | allRouterSockets *IterableChan 159 | ) 160 | if f.darkLaunchManager.IsDarkModeOn() && darklaunch_manager.IsGrayTestingOn() { 161 | util.LOG.Infof("gray testing on.") 162 | sockets = f.darkSockets 163 | catchAll = f.darkCatchall 164 | } 165 | route := resolveRoute(target) 166 | util.LOG.Debugf("handling target %s and getting router socket for route %s", target, route) 167 | 168 | if allRouterSocketsInterface, ok := sockets.Load(route); ok { 169 | allRouterSockets = allRouterSocketsInterface.(*IterableChan) 170 | } else { 171 | allRouterSockets = catchAll 172 | } 173 | f.connMonitor.ReportWebsocketPoolSize(allRouterSockets.LenAlive()) 174 | return allRouterSockets.PollTimeout(f.socketAcquireTime) 175 | } 176 | 177 | func resolveRoute(target string) string { 178 | if len(strings.Split(target, "/")) >= 2 { 179 | return strings.Split(target, "/")[1] 180 | } else { 181 | // it's either root target or blank target 182 | return "" 183 | } 184 | } 185 | 186 | func (f *WebsocketFarm) DeregisterSocket(target, remoteAddr, connectorInstanceID string) { 187 | util.LOG.Infof("going to deregister target %s, targetAddr: %s, connectorInstanceID: %s", target, remoteAddr, connectorInstanceID) 188 | var allRouterSockets *IterableChan 189 | if allRouterSocketsInterface, ok := f.sockets.Load(target); ok { 190 | allRouterSockets = allRouterSocketsInterface.(*IterableChan) 191 | } else { 192 | allRouterSockets = f.catchall 193 | } 194 | for !allRouterSockets.IsAliveSocketSetEmpty() { 195 | if socket := allRouterSockets.Poll(); socket != nil { 196 | f.removeWebsockets(remoteAddr, connectorInstanceID, socket) 197 | } 198 | } 199 | } 200 | 201 | func (f *WebsocketFarm) removeWebsockets(remoteAddr, connectorInstanceID string, routerSocket *RouterSocket) { 202 | curConnectorInstanceID := routerSocket.ConnectorInstanceID() 203 | curRemoteAddr := strings.Split(strings.Split(routerSocket.RemoteAddr(), "/")[1], ":")[0] 204 | if connectorInstanceID == curConnectorInstanceID { 205 | util.LOG.Infof("currentRemoteAddr: %s, remoteAddr: %s, connectorInstanceID: %s, routerSocketID: %s", curRemoteAddr, remoteAddr, curConnectorInstanceID, routerSocket.RouterSocketID) 206 | f.RemoveWebsocket(routerSocket.Route, routerSocket) 207 | } else { 208 | util.LOG.Infof("connectorInstanceID discrepancy: currentRemoteAddr: %s, remoteAddr: %s, connectorInstanceID: %s, routerSocketID: %s", curRemoteAddr, remoteAddr, curConnectorInstanceID, routerSocket.RouterSocketID) 209 | } 210 | } 211 | 212 | func (f *WebsocketFarm) AllCatchall() []*RouterSocket { 213 | return combineCatchall(f.catchall, f.darkCatchall) 214 | } 215 | 216 | func combineCatchall(catchall *IterableChan, darkCatchall *IterableChan) []*RouterSocket { 217 | all := catchall.AliveSocketSlice() 218 | all = append(all, darkCatchall.AliveSocketSlice()...) 219 | return all 220 | } 221 | 222 | func (f *WebsocketFarm) CategorizedAllCatchall() map[string][]*RouterSocket { 223 | categorized := make(map[string][]*RouterSocket) 224 | if !f.darkCatchall.IsAliveSocketSetEmpty() { 225 | categorized["dark"] = f.darkCatchall.AliveSocketSlice() 226 | } 227 | if !f.catchall.IsAliveSocketSetEmpty() { 228 | categorized["normal"] = f.catchall.AliveSocketSlice() 229 | } 230 | return categorized 231 | } 232 | 233 | func (f *WebsocketFarm) CategorizedAllSockets() map[string]map[string][]*RouterSocket { 234 | categorized := make(map[string]map[string][]*RouterSocket) 235 | detail := make(map[string][]*RouterSocket) 236 | f.darkSockets.Range(func(key, value interface{}) bool { 237 | route := key.(string) 238 | routerSocketQueue := value.(*IterableChan) 239 | detail[route] = routerSocketQueue.AliveSocketSlice() 240 | return true 241 | }) 242 | if len(detail) != 0 { 243 | categorized["dark"] = detail 244 | } 245 | 246 | detail2 := make(map[string][]*RouterSocket) 247 | f.sockets.Range(func(key, value interface{}) bool { 248 | route := key.(string) 249 | routerSocketQueue := value.(*IterableChan) 250 | detail2[route] = routerSocketQueue.AliveSocketSlice() 251 | return true 252 | }) 253 | if len(detail2) != 0 { 254 | categorized["normal"] = detail2 255 | } 256 | return categorized 257 | } 258 | 259 | func (f *WebsocketFarm) DarkSockets() *sync.Map { 260 | return f.darkSockets 261 | } 262 | 263 | func (f *WebsocketFarm) Sockets() *sync.Map { 264 | return f.sockets 265 | } 266 | 267 | func (f *WebsocketFarm) AllSockets() map[string][]*RouterSocket { 268 | return combineSyncMap(f.sockets, f.darkSockets) 269 | } 270 | 271 | func combineSyncMap(sockets *sync.Map, darkSockets *sync.Map) map[string][]*RouterSocket { 272 | m := make(map[string][]*RouterSocket) 273 | 274 | darkSockets.Range(func(key, value interface{}) bool { 275 | q := value.(*IterableChan) 276 | m[key.(string)] = q.AliveSocketSlice() 277 | return true 278 | }) 279 | 280 | sockets.Range(func(key, value interface{}) bool { 281 | route := key.(string) 282 | socketBlockingQu := value.(*IterableChan) 283 | if q, ok := m[route]; ok { 284 | q = append(q, socketBlockingQu.AliveSocketSlice()...) 285 | } else { 286 | m[route] = socketBlockingQu.AliveSocketSlice() 287 | } 288 | return true 289 | }) 290 | return m 291 | } 292 | 293 | type darkListener struct { 294 | sockets *sync.Map // in format of map[string]*BlockingQueue this Map is like java's ConcurrentHashMap 295 | darkSockets *sync.Map // in format of map[string]*BlockingQueue 296 | catchall *IterableChan 297 | darkCatchAll *IterableChan 298 | } 299 | 300 | func (l *darkListener) AfterDarkServiceAdded(addedService string) { 301 | l.sockets.Range(func(_, queueInterface interface{}) bool { 302 | queue := queueInterface.(*IterableChan) 303 | toBeRemoved := make([]*RouterSocket, 0, 8) 304 | queue.Range(func(socketInterface interface{}) bool { 305 | socket := socketInterface.(*RouterSocket) 306 | if socket.Ip() == addedService { 307 | var darkQu *IterableChan 308 | if darkQuInterface, ok := l.darkSockets.Load(socket.Route); !ok { 309 | darkQu = NewIterableChan(blockingQueueCapacity) 310 | l.darkSockets.Store(socket.Route, darkQu) 311 | } else { 312 | darkQu = darkQuInterface.(*IterableChan) 313 | } 314 | darkQu.Offer(socket) 315 | toBeRemoved = append(toBeRemoved, socket) 316 | } 317 | return true 318 | }) 319 | for _, socket := range toBeRemoved { 320 | queue.Remove(socket) 321 | } 322 | return true 323 | }) 324 | 325 | toBeRemoved := make([]*RouterSocket, 0, 8) 326 | l.catchall.Range(func(socketInterface interface{}) bool { 327 | socket := socketInterface.(*RouterSocket) 328 | if socket.Route == addedService { 329 | l.darkCatchAll.Offer(socket) 330 | toBeRemoved = append(toBeRemoved, socket) 331 | } 332 | return true 333 | }) 334 | for _, socket := range toBeRemoved { 335 | l.catchall.Remove(socket) 336 | } 337 | } 338 | 339 | func (l *darkListener) AfterDarkServiceRevoked(revokedService string) { 340 | darklaunch_manager.TurnGrayTestingOff("turn off gray testing after service revoked") 341 | l.darkSockets.Range(func(_, queueInterface interface{}) bool { 342 | darkQueue := queueInterface.(*IterableChan) 343 | toBeRemoved := make([]*RouterSocket, 0, 8) 344 | darkQueue.Range(func(socketInterface interface{}) bool { 345 | socket := socketInterface.(*RouterSocket) 346 | if socket.Route == revokedService { 347 | var qu *IterableChan 348 | if darkQuInterface, ok := l.darkSockets.Load(socket.Route); !ok { 349 | qu = NewIterableChan(blockingQueueCapacity) 350 | l.sockets.Store(socket.Route, qu) 351 | } else { 352 | qu = darkQuInterface.(*IterableChan) 353 | } 354 | qu.Offer(socket) 355 | toBeRemoved = append(toBeRemoved, socket) 356 | } 357 | return true 358 | }) 359 | for _, socket := range toBeRemoved { 360 | darkQueue.Remove(socket) 361 | } 362 | return true 363 | }) 364 | 365 | toBeRemoved := make([]*RouterSocket, 0, 8) 366 | l.darkCatchAll.Range(func(socketInterface interface{}) bool { 367 | socket := socketInterface.(*RouterSocket) 368 | if socket.Route == revokedService { 369 | l.catchall.Offer(socket) 370 | toBeRemoved = append(toBeRemoved, socket) 371 | } 372 | return true 373 | }) 374 | for _, socket := range toBeRemoved { 375 | l.darkCatchAll.Remove(socket) 376 | } 377 | } 378 | 379 | func (l *darkListener) AfterDarkIpAdded(addedIp string) { 380 | l.sockets.Range(func(_, queueInterface interface{}) bool { 381 | queue := queueInterface.(*IterableChan) 382 | toBeRemoved := make([]*RouterSocket, 0, 8) 383 | queue.Range(func(socketInterface interface{}) bool { 384 | socket := socketInterface.(*RouterSocket) 385 | if socket.Ip() == addedIp { 386 | var darkQu *IterableChan 387 | if darkQuInterface, ok := l.darkSockets.Load(socket.Route); !ok { 388 | darkQu = NewIterableChan(blockingQueueCapacity) 389 | l.darkSockets.Store(socket.Route, darkQu) 390 | } else { 391 | darkQu = darkQuInterface.(*IterableChan) 392 | } 393 | darkQu.Offer(socket) 394 | toBeRemoved = append(toBeRemoved, socket) 395 | } 396 | return true 397 | }) 398 | for _, socket := range toBeRemoved { 399 | queue.Remove(socket) 400 | } 401 | return true 402 | }) 403 | 404 | toBeRemoved := make([]*RouterSocket, 0, 8) 405 | l.catchall.Range(func(socketInterface interface{}) bool { 406 | socket := socketInterface.(*RouterSocket) 407 | if socket.Ip() == addedIp { 408 | l.darkCatchAll.Offer(socket) 409 | toBeRemoved = append(toBeRemoved, socket) 410 | } 411 | return true 412 | }) 413 | for _, socket := range toBeRemoved { 414 | l.catchall.Remove(socket) 415 | } 416 | } 417 | 418 | func (l *darkListener) AfterDarkIpRevoked(revokedIp string) { 419 | darklaunch_manager.TurnGrayTestingOff("turn off gray testing after ip revoked") 420 | l.darkSockets.Range(func(_, queueInterface interface{}) bool { 421 | darkQueue := queueInterface.(*IterableChan) 422 | toBeRemoved := make([]*RouterSocket, 0, 8) 423 | darkQueue.Range(func(socketInterface interface{}) bool { 424 | socket := socketInterface.(*RouterSocket) 425 | if socket.Ip() == revokedIp { 426 | var qu *IterableChan 427 | if darkQuInterface, ok := l.darkSockets.Load(socket.Route); !ok { 428 | qu = NewIterableChan(blockingQueueCapacity) 429 | l.sockets.Store(socket.Route, qu) 430 | } else { 431 | qu = darkQuInterface.(*IterableChan) 432 | } 433 | qu.Offer(socket) 434 | toBeRemoved = append(toBeRemoved, socket) 435 | } 436 | return true 437 | }) 438 | for _, socket := range toBeRemoved { 439 | darkQueue.Remove(socket) 440 | } 441 | return true 442 | }) 443 | 444 | toBeRemoved := make([]*RouterSocket, 0, 8) 445 | l.darkCatchAll.Range(func(socketInterface interface{}) bool { 446 | socket := socketInterface.(*RouterSocket) 447 | if socket.Ip() == revokedIp { 448 | l.catchall.Offer(socket) 449 | toBeRemoved = append(toBeRemoved, socket) 450 | } 451 | return true 452 | }) 453 | for _, socket := range toBeRemoved { 454 | l.darkCatchAll.Remove(socket) 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /router/service/health_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | type HealthService interface { 4 | CreateHealthReport() map[string]interface{} 5 | CreateConnectorsReport() map[string]interface{} 6 | GetVersion() string 7 | GetAvailable() bool 8 | CreateCategorizedConnectorsReport() map[string]interface{} 9 | } 10 | -------------------------------------------------------------------------------- /router/validator.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | type Validator interface { 4 | IsValid(string) bool 5 | } 6 | 7 | type IpValidator struct { 8 | ipWhiteList map[string]struct{} 9 | } 10 | 11 | // IsValid if ip set is nil, isValid will return true by default. 12 | // if ip set is not nil and the given ip is not in the set, it will return false 13 | func (v *IpValidator) IsValid(ip string) bool { 14 | if v.ipWhiteList == nil { 15 | return true 16 | } 17 | if _, ok := v.ipWhiteList[ip]; ok { 18 | return true 19 | } else { 20 | return false 21 | } 22 | } 23 | 24 | func (v *IpValidator) UpdateIpWhiteList(newIps []string) { 25 | m := make(map[string]struct{}) 26 | for _, ip := range newIps { 27 | m[ip] = struct{}{} 28 | } 29 | v.ipWhiteList = m 30 | } 31 | -------------------------------------------------------------------------------- /test/e2etest/connector_manual_test.go: -------------------------------------------------------------------------------- 1 | package e2etest 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "testing" 7 | "time" 8 | 9 | . "github.com/torchcc/crank4go/connector" 10 | . "github.com/torchcc/crank4go/test/scaffolding" 11 | ) 12 | 13 | // upload img done, get done, post done , all method done, connector is ok. 14 | func TestDryRunConnector(t *testing.T) { 15 | targetURI, _ := url.Parse("http://localhost:5000") 16 | routerURI, _ := url.Parse("wss://localhost:16488") 17 | 18 | connectorConfig := NewConnectorConfig2(targetURI, "z", []*url.URL{routerURI}, "z-service", nil). 19 | SetSlidingWindowSize(100). 20 | SetIsShutDownHookAdded(false) 21 | 22 | connector := CreateAndStartConnector(connectorConfig) 23 | fmt.Println(connector) 24 | 25 | serviceA := NewContextualizedWebserver(5000, "/z") 26 | serviceA.Start() 27 | time.Sleep(1000 * time.Minute) 28 | 29 | } 30 | 31 | // upload img done, get done, post done , all method done, connector is ok. 32 | func TestDryRunConnector2(t *testing.T) { 33 | targetURI, _ := url.Parse("http://localhost:5000") 34 | routerURI, _ := url.Parse("wss://localhost:9070") 35 | 36 | connectorConfig := NewConnectorConfig2(targetURI, "z", []*url.URL{routerURI}, "z-service", nil). 37 | SetSlidingWindowSize(300). 38 | SetIsShutDownHookAdded(false) 39 | 40 | connector := CreateAndStartConnector(connectorConfig) 41 | fmt.Println(connector) 42 | 43 | serviceA := NewContextualizedWebserver(5000, "/z") 44 | serviceA.Start() 45 | time.Sleep(1000 * time.Minute) 46 | } 47 | -------------------------------------------------------------------------------- /test/e2etest/cranker_with_all_extention_single_service_test.go: -------------------------------------------------------------------------------- 1 | package e2etest 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | 10 | connector "github.com/torchcc/crank4go/connector" 11 | "github.com/torchcc/crank4go/connector/plugin" 12 | protocol "github.com/torchcc/crank4go/protocol" 13 | router "github.com/torchcc/crank4go/router" 14 | . "github.com/torchcc/crank4go/test/scaffolding" 15 | "github.com/torchcc/crank4go/util" 16 | ) 17 | 18 | func TestCrankerWithAllExtensionSingleService(t *testing.T) { 19 | s := NewCrankerWithAllExtensionSingleService() 20 | s.start() 21 | defer s.stop() 22 | 23 | subUri, _ := url.Parse("/a/static/hello.html") 24 | resp, err := s.httpClient.Get(s.routerApp.HttpURI().ResolveReference(subUri).String()) 25 | if err != nil { 26 | t.Errorf("failed to send request, err: %s", err.Error()) 27 | return 28 | } 29 | if resp.StatusCode != 200 { 30 | t.Errorf("statusCode err: %s", err.Error()) 31 | return 32 | } 33 | defer resp.Body.Close() 34 | contentBytes, err := io.ReadAll(resp.Body) 35 | if err != nil { 36 | t.Errorf("failed, err: %s", err.Error()) 37 | return 38 | } 39 | if string(contentBytes) != HelloHtmlContents() { 40 | t.Errorf("failed, err: %s", err.Error()) 41 | return 42 | } 43 | 44 | // test to access service which is not registered on router: 45 | subUri, _ = url.Parse("/b/static/hello.html") 46 | resp, err = s.httpClient.Get(s.routerApp.HttpURI().ResolveReference(subUri).String()) 47 | if err != nil { 48 | t.Errorf("failed to send request, err: %s", err.Error()) 49 | return 50 | } 51 | if resp.StatusCode != 503 { 52 | t.Errorf("statusCode err, expected 503, got: %d", resp.StatusCode) 53 | return 54 | } 55 | 56 | } 57 | 58 | type CrankerWithAllExtensionSingleService struct { 59 | serviceA *ContextualizedWebserver 60 | testProxyInterceptorStat *testStat 61 | testSocketStat *testStat 62 | testConnectorPluginStat *testStat 63 | routerHttpPort int 64 | connectorHealthPort int 65 | routerApp *RouterApp 66 | connectorA *ConnectorApp 67 | httpClient *http.Client 68 | } 69 | 70 | func NewCrankerWithAllExtensionSingleService() *CrankerWithAllExtensionSingleService { 71 | testConnectorPluginStat := newTestStat() 72 | dataPublishHandlers := []util.DataPublishHandler{ 73 | util.DataPublishHandlerFunc(func(key string, value int) { fmt.Printf("mykey: %s, myvalue: %d", key, value) }), 74 | } 75 | connectorHealthPort, _ := util.GetFreePort() 76 | routerHealthPort, _ := util.GetFreePort() 77 | serviceAHttpPort, _ := util.GetFreePort() 78 | routerHttpPort := 9000 79 | serviceA := NewContextualizedWebserver(serviceAHttpPort, "/a") 80 | routerApp := CreateRouterApp(routerHttpPort, 9070, routerHealthPort) 81 | connectorApp := NewConnectorApp2([]*url.URL{routerApp.RegisterURI(), routerApp.RegisterURI()}, serviceA.Uri, 82 | "a", "service-a", connectorHealthPort, 3, dataPublishHandlers, []plugin.ConnectorPlugin{newTestConnectorPlugin(testConnectorPluginStat)}) 83 | 84 | t := &CrankerWithAllExtensionSingleService{ 85 | serviceA: serviceA, 86 | testProxyInterceptorStat: newTestStat(), 87 | testSocketStat: newTestStat(), 88 | testConnectorPluginStat: testConnectorPluginStat, 89 | routerHttpPort: routerHttpPort, 90 | connectorHealthPort: connectorHealthPort, 91 | routerApp: routerApp, 92 | connectorA: connectorApp, 93 | httpClient: connector.GetHttpClient(), 94 | } 95 | return t 96 | } 97 | 98 | func (s *CrankerWithAllExtensionSingleService) start() { 99 | util.LOG.Infof("service start is starting...") 100 | s.routerApp.Start() 101 | s.serviceA.Start() 102 | s.connectorA.Start() 103 | util.LOG.Infof("service start done") 104 | } 105 | 106 | func (s *CrankerWithAllExtensionSingleService) stop() { 107 | s.serviceA.ShutDown() 108 | s.connectorA.ShutDown() 109 | s.routerApp.Shutdown() 110 | } 111 | 112 | func CreateRouterApp(httpPort, registerPort, healthPort int) *RouterApp { 113 | connMonitor := util.NewConnectionMonitor([]util.DataPublishHandler{ 114 | util.DataPublishHandlerFunc(func(key string, value int) { fmt.Printf("key: %s, value: %d", key, value) }), 115 | }) 116 | 117 | routerConfig := router.NewRouterConfig("localhost", "localhost", 118 | registerPort, httpPort, GetTestTLSConfig(), GetTestTLSConfig()) 119 | 120 | routerConfig.SetIsShutDownHookAdded(false). 121 | SetConnMonitor(connMonitor). 122 | ConfigDarkLaunch("abc", "dark-mode-service") 123 | return NewRouterApp2(routerConfig, healthPort) 124 | } 125 | 126 | type testStat struct { 127 | states []string 128 | } 129 | 130 | func newTestStat() *testStat { 131 | return &testStat{states: make([]string, 0, 8)} 132 | } 133 | func (s *testStat) UpdateState(state string) { 134 | s.states = append(s.states, state) 135 | } 136 | func (s *testStat) CurrentState() string { 137 | if l := len(s.states); l > 0 { 138 | return s.states[l-1] 139 | } else { 140 | return "" 141 | } 142 | } 143 | 144 | type testConnectorPlugin struct { 145 | testStat *testStat 146 | } 147 | 148 | func newTestConnectorPlugin(testStat *testStat) *testConnectorPlugin { 149 | return &testConnectorPlugin{testStat: testStat} 150 | } 151 | 152 | func (t *testConnectorPlugin) HandleBeforeRequestSent(req *protocol.CrankerProtocolRequest, carrier plugin.ConnectorPluginStatCarrier) (plugin.ConnectorPluginStatCarrier, error) { 153 | util.LOG.Infof("handleBeforeRequestSent %s", req.String()) 154 | t.testStat.UpdateState(req.ToProtocolMessage()) 155 | return carrier, nil 156 | } 157 | 158 | func (t *testConnectorPlugin) HandleAfterResponseReceived(resp *protocol.CrankerProtocolResponse, carrier plugin.ConnectorPluginStatCarrier) (plugin.ConnectorPluginStatCarrier, error) { 159 | util.LOG.Infof("HandleAfterResponseReceived %s", resp.String()) 160 | t.testStat.UpdateState(resp.ToProtocolMessage()) 161 | return carrier, nil 162 | } 163 | -------------------------------------------------------------------------------- /test/e2etest/dryrun_router_test.go: -------------------------------------------------------------------------------- 1 | package e2etest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestDryRunRouter(t *testing.T) { 9 | app := CreateRouterApp(9000, 9070, 12439) 10 | app.Start() 11 | time.Sleep(60 * time.Minute) 12 | } 13 | -------------------------------------------------------------------------------- /test/init.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "path" 7 | "runtime" 8 | ) 9 | 10 | var TestDir = "" 11 | 12 | func getTestDir() { 13 | TestDir = path.Dir(getCurrentFile()) 14 | 15 | if TestDir == "" { 16 | panic(errors.New("can not get current file info")) 17 | } else { 18 | fmt.Println("test dir is " + TestDir) 19 | 20 | } 21 | } 22 | 23 | func getCurrentFile() string { 24 | _, file, _, ok := runtime.Caller(1) 25 | if !ok { 26 | panic(errors.New("can not get current file info")) 27 | } 28 | return file 29 | } 30 | 31 | func init() { 32 | getTestDir() 33 | } 34 | -------------------------------------------------------------------------------- /test/scaffolding/connector_app.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "time" 7 | 8 | ct "github.com/torchcc/crank4go/connector" 9 | "github.com/torchcc/crank4go/connector/plugin" 10 | "github.com/torchcc/crank4go/util" 11 | ) 12 | 13 | type ConnectorApp struct { 14 | healthURI *url.URL 15 | healthServer *RestfulServer 16 | connector *ct.Connector 17 | connectorHealthService *ConnectorHealthService 18 | } 19 | 20 | func NewConnectorApp(routerURIs []*url.URL, targetURI *url.URL, targetServiceName string, healthPort, websocketPoolSize int, dataPublishHandlers []util.DataPublishHandler) *ConnectorApp { 21 | return NewConnectorApp2(routerURIs, targetURI, targetServiceName, "testService", healthPort, 22 | websocketPoolSize, dataPublishHandlers, nil) 23 | } 24 | 25 | func NewConnectorApp2(routerURIs []*url.URL, targetURI *url.URL, targetServiceName, componentName string, healthPort, websocketPoolSize int, dataPublishHandlers []util.DataPublishHandler, connectorPlugins []plugin.ConnectorPlugin) *ConnectorApp { 26 | connectorConfig := ct.NewConnectorConfig2(targetURI, targetServiceName, routerURIs, componentName, connectorPlugins). 27 | SetSlidingWindowSize(websocketPoolSize). 28 | SetDataPublishHandlers(dataPublishHandlers). 29 | SetIsShutDownHookAdded(false) 30 | 31 | connector := ct.CreateAndStartConnector(connectorConfig) 32 | connectorHealthService := NewConnectorHealthService(connector.ConnMonitor()) 33 | healthServer := NewRestfulServer(healthPort, NewHealthServiceResource(connectorHealthService)) 34 | healthURI, _ := url.Parse(fmt.Sprintf("http://localhost:%d/health", healthPort)) 35 | return &ConnectorApp{ 36 | healthURI: healthURI, 37 | healthServer: healthServer, 38 | connector: connector, 39 | connectorHealthService: connectorHealthService, 40 | } 41 | } 42 | 43 | func (c *ConnectorApp) Start() { 44 | c.healthServer.Start() 45 | c.connectorHealthService.ScheduleHealthCheck() 46 | } 47 | 48 | func (c *ConnectorApp) ShutDownSuccessfully(timeout time.Duration) (succeeded bool) { 49 | succeeded = c.connector.ShutDownAfterTimeout(timeout) 50 | c.healthServer.ShutDown() 51 | return 52 | } 53 | 54 | func (c *ConnectorApp) ShutDown() { 55 | c.ShutDownSuccessfully(20 * time.Microsecond) 56 | } 57 | 58 | func (c *ConnectorApp) IdleWebsocketFarmInfoMap() map[string]int { 59 | return c.connector.IdleWebsocketFarmInfo() 60 | } 61 | -------------------------------------------------------------------------------- /test/scaffolding/connector_health_service.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/julienschmidt/httprouter" 9 | ptc "github.com/torchcc/crank4go/protocol" 10 | "github.com/torchcc/crank4go/router/api" 11 | "github.com/torchcc/crank4go/router/handler" 12 | "github.com/torchcc/crank4go/util" 13 | ) 14 | 15 | type HealthService interface { 16 | CreateHealthReport() map[string]interface{} 17 | GetVersion() string 18 | GetAvailable() bool 19 | } 20 | 21 | type HealthServiceResource struct { 22 | basePath string 23 | healthService HealthService 24 | } 25 | 26 | func NewHealthServiceResource(healthService HealthService) *HealthServiceResource { 27 | return &HealthServiceResource{ 28 | healthService: healthService, 29 | basePath: "/health", 30 | } 31 | } 32 | 33 | // @Path("/health") 34 | func (h *HealthServiceResource) GetHealthInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) bool { 35 | api.RespJsonOk(w, h.healthService.CreateHealthReport()) 36 | return true 37 | } 38 | 39 | func (h *HealthServiceResource) RegisterResourceToHttpRouter(server *httprouter.Router, rootPath string) { 40 | basePath := rootPath + h.basePath 41 | server.GET(basePath, handler.NewXHttpHandler(handler.XHandlerFunc(h.GetHealthInfo)).ServeXHTTP) 42 | } 43 | 44 | type ConnectorHealthService struct { 45 | version string 46 | cancelHealthCheck context.CancelFunc 47 | connectorToRouterConnMonitor *util.ConnectionMonitor 48 | isAvailable bool 49 | } 50 | 51 | func NewConnectorHealthService(connectorToRouterConnMonitor *util.ConnectionMonitor) *ConnectorHealthService { 52 | return &ConnectorHealthService{ 53 | version: "N/A", 54 | connectorToRouterConnMonitor: connectorToRouterConnMonitor, 55 | isAvailable: false, 56 | } 57 | } 58 | 59 | func (c *ConnectorHealthService) CreateHealthReport() map[string]interface{} { 60 | return map[string]interface{}{ 61 | "component": "crank4go-connector", 62 | "description": "open connections to crank4go-router, and pass tunneled requests to target service", 63 | "version": c.version, 64 | "git-url": "https://github.com", 65 | "isAvailable": c.isAvailable, 66 | "CrankerProtocolVersion": ptc.CrankerProtocolVersion10, 67 | "activeConnections": c.connectorToRouterConnMonitor.ConnectionCount(), 68 | "openFiles": c.connectorToRouterConnMonitor.OpenFiles(), 69 | } 70 | } 71 | 72 | func (c *ConnectorHealthService) GetVersion() string { 73 | return c.version 74 | } 75 | 76 | func (c *ConnectorHealthService) GetAvailable() bool { 77 | return c.isAvailable 78 | } 79 | 80 | func (c *ConnectorHealthService) ScheduleHealthCheck() { 81 | ctx, cancel := context.WithCancel(context.Background()) 82 | c.cancelHealthCheck = cancel 83 | util.LOG.Infof("Health Check scheduler started with 1 minute period") 84 | go func(ctx context.Context) { 85 | LOOP: 86 | for { 87 | c.updateHealth() 88 | select { 89 | case <-ctx.Done(): 90 | break LOOP 91 | case <-time.After(time.Minute): 92 | } 93 | } 94 | }(ctx) 95 | } 96 | 97 | func (c *ConnectorHealthService) updateHealth() { 98 | c.isAvailable = true 99 | } 100 | -------------------------------------------------------------------------------- /test/scaffolding/contextualized_webserver.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "strconv" 11 | "testing" 12 | "time" 13 | 14 | "github.com/torchcc/crank4go/test" 15 | "github.com/torchcc/crank4go/util" 16 | ) 17 | 18 | type ContextualizedWebserver struct { 19 | Uri *url.URL 20 | server *http.Server 21 | staticContext string 22 | } 23 | 24 | func NewContextualizedWebserver(port int, ctx string) *ContextualizedWebserver { 25 | svr := &ContextualizedWebserver{} 26 | svr.Uri, _ = url.Parse("http://localhost:" + strconv.Itoa(port)) 27 | svr.staticContext = ctx + "/static/" 28 | util.LOG.Infof("test dir is %s", test.TestDir) 29 | mux := http.NewServeMux() 30 | mux.Handle(svr.staticContext, http.StripPrefix(ctx, http.FileServer(http.Dir(test.TestDir)))) 31 | mux.Handle(ctx+"/say_hi", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | w.WriteHeader(200) 33 | w.Write([]byte("say hi")) 34 | })) 35 | mux.Handle(ctx+"/upload", http.HandlerFunc(UploadImage)) 36 | 37 | svr.server = &http.Server{ 38 | Addr: ":" + strconv.Itoa(port), 39 | Handler: mux, 40 | } 41 | return svr 42 | } 43 | 44 | func (s *ContextualizedWebserver) Start() { 45 | go func() { 46 | util.LOG.Infof("target server is started at %s", s.Uri.String()+s.staticContext+"hello.html") 47 | if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 48 | panic("failed to start httpServer, uri: " + s.Uri.String() + " err: " + err.Error()) 49 | } 50 | }() 51 | } 52 | func (s *ContextualizedWebserver) ShutDown() { 53 | timeout, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) 54 | defer cancelFunc() 55 | _ = s.server.Shutdown(timeout) 56 | } 57 | 58 | func UploadImage(w http.ResponseWriter, r *http.Request) { 59 | err := r.ParseMultipartForm(1024 * 1024) // max size of file that allowed to upload 60 | if err != nil { 61 | util.LOG.Errorf("failed to upload: err: %s", err.Error()) 62 | return 63 | } 64 | 65 | img := r.MultipartForm.File["image"][0] 66 | name := img.Filename 67 | 68 | fmt.Println("uploaded: ", name) 69 | 70 | file, err := img.Open() 71 | if err == nil { 72 | data, err := ioutil.ReadAll(file) 73 | if err == nil { 74 | filename := name 75 | // 创建这个文件 76 | newFile, err := os.Create(test.TestDir + "/static/img/" + filename) 77 | if err != nil { 78 | fmt.Errorf("failed to create, err: %s", err.Error()) 79 | } 80 | defer newFile.Close() 81 | // 将上传文件的二进制字节信息写入新建的文件 82 | size, err := newFile.Write(data) 83 | if err == nil { 84 | fmt.Fprintf(w, "uploaded, img size: %d 字节\n", size/1000) 85 | } 86 | } 87 | } 88 | } 89 | 90 | func TestContextualizedWebserver(t *testing.T) { 91 | NewContextualizedWebserver(10086, "/a").Start() 92 | time.Sleep(60 * time.Second) 93 | } 94 | -------------------------------------------------------------------------------- /test/scaffolding/restfull_server.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | "github.com/torchcc/crank4go/router/api" 11 | "github.com/torchcc/crank4go/util" 12 | ) 13 | 14 | type RestfulServer struct { 15 | server *http.Server 16 | port int 17 | resources []api.Resource 18 | } 19 | 20 | func NewRestfulServer(port int, resources ...api.Resource) *RestfulServer { 21 | return &RestfulServer{port: port, resources: resources} 22 | } 23 | 24 | func (s *RestfulServer) Port() int { 25 | return s.port 26 | } 27 | 28 | func (s *RestfulServer) Start() { 29 | s.createHttpServer() 30 | go func() { 31 | util.LOG.Infof("going to start RESTFul health server at http://0.0.0.0:%d/health", s.port) 32 | if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 33 | panic("failed to start httpServer, err: " + err.Error()) 34 | } 35 | }() 36 | } 37 | 38 | func (s *RestfulServer) createHttpServer() { 39 | httpRouter := httprouter.New() 40 | for _, resource := range s.resources { 41 | resource.RegisterResourceToHttpRouter(httpRouter, "") 42 | } 43 | 44 | s.server = &http.Server{ 45 | Addr: ":" + strconv.Itoa(s.port), 46 | Handler: httpRouter, 47 | } 48 | } 49 | 50 | func (s *RestfulServer) ShutDown() { 51 | timeout, cancelFunc := context.WithTimeout(context.Background(), 10*time.Millisecond) 52 | defer cancelFunc() 53 | _ = s.server.Shutdown(timeout) 54 | } 55 | -------------------------------------------------------------------------------- /test/scaffolding/router_app.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/url" 7 | 8 | router "github.com/torchcc/crank4go/router" 9 | "github.com/torchcc/crank4go/router/api" 10 | "github.com/torchcc/crank4go/router/darklaunch_manager" 11 | "github.com/torchcc/crank4go/router/router_socket" 12 | "github.com/torchcc/crank4go/util" 13 | ) 14 | 15 | type RouterApp struct { 16 | healthURI *url.URL 17 | router *router.Router 18 | routerHealthService *RouterHealthService 19 | healthServer *RestfulServer 20 | darkLaunchManager *darklaunch_manager.DarkLaunchManager 21 | } 22 | 23 | func NewRouterApp2(routerConfig *router.RouterConfig, healthPort int) *RouterApp { 24 | routerConfig.SetupDarkLaunchManagerWithPath("/fake-path") // TODO to configure properly 25 | startedRouter := router.CreateAndStartRouter(routerConfig) 26 | routerHealthService := NewRouterHealthService(startedRouter.RouterAvailability()) 27 | healthServer := NewRestfulServer(healthPort, api.NewHealthServiceResource2(routerHealthService)) 28 | healthURI, _ := url.Parse(fmt.Sprintf("http://%s:%d/health", routerConfig.WebserverInterface(), healthPort)) 29 | 30 | return &RouterApp{ 31 | healthURI: healthURI, 32 | router: startedRouter, 33 | routerHealthService: routerHealthService, 34 | healthServer: healthServer, 35 | darkLaunchManager: routerConfig.DarkLaunchManager(), 36 | } 37 | } 38 | 39 | func NewRouterApp(httpPort, registerPort, healthPort int, 40 | webserverTLSConfig, websocketTLSConfig *tls.Config, 41 | webserverInterface, websocketInterface string, 42 | dataPublishHandlers []util.DataPublishHandler) *RouterApp { 43 | 44 | connMonitor := util.NewConnectionMonitor(dataPublishHandlers) 45 | 46 | routerConfig := router.NewRouterConfig(websocketInterface, webserverInterface, registerPort, httpPort, websocketTLSConfig, webserverTLSConfig) 47 | routerConfig.SetReqComponentHeader("amazon-AI-component"). 48 | SetCheckOrigin(func(s string) bool { 49 | return true 50 | }). 51 | SetIsShutDownHookAdded(true). 52 | SetConnMonitor(connMonitor). 53 | ConfigDarkLaunch("aaa", "dark-mode-service"). // TODO to config properly 54 | SetupDarkLaunchManagerWithPath("/fake-path") 55 | 56 | healthURI, _ := url.Parse(fmt.Sprintf("http://%s:%d/health", webserverInterface, healthPort)) 57 | startedRouter := router.CreateAndStartRouter(routerConfig) 58 | routerHealthService := NewRouterHealthService(startedRouter.RouterAvailability()) 59 | healthServer := NewRestfulServer(healthPort, api.NewHealthServiceResource2(routerHealthService)) 60 | 61 | return &RouterApp{ 62 | healthURI: healthURI, 63 | router: startedRouter, 64 | routerHealthService: routerHealthService, 65 | darkLaunchManager: routerConfig.DarkLaunchManager(), 66 | healthServer: healthServer, 67 | } 68 | } 69 | 70 | func (r *RouterApp) RegisterURI() *url.URL { 71 | return r.router.RegisterURI 72 | } 73 | 74 | func (r *RouterApp) WebsocketFarm() *router_socket.WebsocketFarm { 75 | return r.router.WebsocketFarm() 76 | } 77 | 78 | func (r *RouterApp) HttpURI() *url.URL { 79 | return r.router.HttpURI 80 | } 81 | 82 | func (r *RouterApp) RouterHealthService() *RouterHealthService { 83 | return r.routerHealthService 84 | } 85 | 86 | func (r *RouterApp) DarkLaunchManager() *darklaunch_manager.DarkLaunchManager { 87 | return r.darkLaunchManager 88 | } 89 | 90 | func (r *RouterApp) Start() { 91 | r.healthServer.Start() 92 | r.routerHealthService.ScheduleHealthCheck() 93 | } 94 | func (r *RouterApp) Shutdown() { 95 | r.healthServer.ShutDown() 96 | r.router.Shutdown() 97 | } 98 | -------------------------------------------------------------------------------- /test/scaffolding/router_health_service.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | router "github.com/torchcc/crank4go/router" 8 | "github.com/torchcc/crank4go/util" 9 | ) 10 | 11 | type RouterHealthService struct { 12 | version string 13 | available string 14 | isAvailable bool 15 | cancelHealthCheck context.CancelFunc 16 | routerAvailability *router.RouterAvailability 17 | } 18 | 19 | func NewRouterHealthService(routerAvailability *router.RouterAvailability) *RouterHealthService { 20 | return &RouterHealthService{ 21 | version: "N/A", 22 | available: "isAvailable", 23 | routerAvailability: routerAvailability, 24 | } 25 | } 26 | 27 | func (r *RouterHealthService) CreateHealthReport() map[string]interface{} { 28 | embeddedRouterStatus := make(map[string]interface{}) 29 | embeddedRouterStatus["router"] = r.routerAvailability.Status() 30 | dependencies := make([]interface{}, 0, 8) 31 | dependencies = append(dependencies, embeddedRouterStatus) 32 | 33 | return map[string]interface{}{ 34 | "component": "crank4go-router", 35 | "description": "Proxy end uses https calls and websocket calls", 36 | "version": r.version, 37 | "service": "/health/connectors", 38 | "git-url": "https://github.com/", 39 | r.available: r.isAvailable, 40 | "dependencies": dependencies, 41 | } 42 | } 43 | 44 | func (r *RouterHealthService) GetVersion() string { 45 | return r.version 46 | } 47 | 48 | func (r *RouterHealthService) GetAvailable() bool { 49 | return r.isAvailable 50 | } 51 | 52 | func (r *RouterHealthService) CreateConnectorsReport() map[string]interface{} { 53 | return map[string]interface{}{ 54 | "services": r.routerAvailability.Services(), 55 | } 56 | } 57 | 58 | func (r *RouterHealthService) CreateCategorizedConnectorsReport() map[string]interface{} { 59 | m := make(map[string]interface{}) 60 | m["services"] = r.routerAvailability.ServicesCategorizedDetail() 61 | return m 62 | } 63 | 64 | func (r *RouterHealthService) ScheduleHealthCheck() { 65 | ctx, cancel := context.WithCancel(context.Background()) 66 | r.cancelHealthCheck = cancel 67 | util.LOG.Infof("Health Check scheduler started with 1 minute period") 68 | go func(ctx context.Context) { 69 | LOOP: 70 | for { 71 | r.updateHealth() 72 | select { 73 | case <-ctx.Done(): 74 | break LOOP 75 | case <-time.After(time.Minute): 76 | } 77 | } 78 | }(ctx) 79 | } 80 | 81 | func (r *RouterHealthService) updateHealth() { 82 | embeddedRouterStatus := r.routerAvailability.Status() 83 | r.isAvailable = embeddedRouterStatus[r.available].(bool) 84 | } 85 | -------------------------------------------------------------------------------- /test/scaffolding/util.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path" 9 | 10 | "github.com/torchcc/crank4go/test" 11 | "github.com/torchcc/crank4go/util" 12 | ) 13 | 14 | func GetTestTLSConfig() *tls.Config { 15 | // return nil 16 | cert, err := tls.LoadX509KeyPair(path.Join(test.TestDir, "static/cert/server.pem"), path.Join(test.TestDir, "static/cert/server.key")) 17 | if err != nil { 18 | util.LOG.Errorf("failed to load ssl certificate: err: %s", err) 19 | return nil 20 | } 21 | return &tls.Config{Certificates: []tls.Certificate{cert}} 22 | } 23 | 24 | func HelloHtmlContents() string { 25 | 26 | fullPath := test.TestDir + "/static/hello.html" 27 | return FindFileAsString(fullPath) 28 | } 29 | 30 | func FindFileAsString(path string) string { 31 | if file, e := os.Open(path); e != nil { 32 | fmt.Printf("failed to open path: %s, error: %s", path, e.Error()) 33 | return "" 34 | } else { 35 | if bytes, e := io.ReadAll(file); e != nil { 36 | fmt.Printf("failed to read file: %s, error: %s", path, e.Error()) 37 | return "" 38 | } else { 39 | return string(bytes) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/scaffolding/util_test.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // it blocks. open it to manually run 8 | // func TestManualGetTestTLSConfig(t *testing.T) { 9 | // config := GetTestTLSConfig() 10 | // if config == nil { 11 | // t.Errorf("failed to get cert, err:") 12 | // return 13 | // } 14 | // 15 | // mux := http.NewServeMux() 16 | // mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | // w.WriteHeader(200) 18 | // w.Write([]byte("hello")) 19 | // })) 20 | // server := &http.Server{ 21 | // Addr: ":8443", 22 | // Handler: mux, 23 | // TLSConfig: config, 24 | // } 25 | // server.ListenAndServeTLS("", "") 26 | // } 27 | 28 | func TestFindFileAsString(t *testing.T) { 29 | contents := HelloHtmlContents() 30 | t.Log(contents) 31 | } 32 | -------------------------------------------------------------------------------- /test/static/cert/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCeVxfXCJ/E+rOd 3 | tgW41+QVGwfyiWNpO+SZYQZI7M2BrUL9ibfIKpsMF4qqtP0C1k7spVYJK+KXLBms 4 | M7NGkIIc9SZqv907GbHPX/LH6uMg6S4ukS6kpZ9oYQ6abogXvAxUzPIjuRM31QFe 5 | tguHzztOnIregKpqpnSH694vRNEsKddjGxMbhofoIhcmLM9mt02kEgjTxrF2csUw 6 | aTWkRau35o96ZSVpjVKc7f9NMMB5dgxe9y8KoqS6mZiWsjvR0HkfW/YudIssfhUA 7 | d2nWKwSc+kovsISY0P2/2Qkj+tJu5RUdD+XRcujoWgG90p9Q3dmznicN/SmzYYYo 8 | hrZguGFnAgMBAAECggEAShwVam386aM1gnF5iCRz+nTmaVxojQ6dVjSVTniXT0Sb 9 | ADP/Ms7ONwClxHRln3hTBGv2MuC5c2wOsAyaskJcw9TyIDChCVJjaN5Nsch8eiDp 10 | np4RKLrkO2SCA0IMrJ81XlN2WcX7+rvVolCuYOhbp9WZIb8zBCvYiu2Y1qLtDC/k 11 | pOby9CUFN6QgqCqQq22JOFgZ0SxNGfT4yWL1F+dQ2ZfVklPt6h/anWUh8MMp+Egu 12 | TLXYe6abrEsMI+fjQIDlh2U2Op5zMI/qBz9XLRH20G0n7Rpq7IxzS/ywZmCrVCrW 13 | 2M2KzY+xWxfPylqpaHv5q8fm1chu+Qdt2oAwnOLXcQKBgQDRaOYZe3+R/es22M0o 14 | bjrjfgV+uMxqUDrV0Pw2cHsV0qpYspW1OExTnjCPURbps9kqcdb9tDPh9c8RCNFP 15 | T1rBgPDbaN8lVP+gp0xuW9/p2nOwtIePuDf70oWMFaR40Y4EzOpzwBp+8/LDKwYZ 16 | eepQWzKuE8Fv6JaCJ7/zyQ8THQKBgQDBkXyiM6YBCu97xBMUoon2fjyX6lqWJhWY 17 | Fs1v8oHA5vtshrvS0q5NuB6jdnVR4KbtEaETZaExJKU0DfhdsUdY+YPpWWY2S6Rb 18 | N7kbuPpNGUGCgt5gw6BNljIeXzucyqPNsxueEw96+UA2QnVLTyLsPtoMt859PULW 19 | c7tEWCu7UwKBgE+UJoUeim8naGByaRxpL1XOSTZL8dqg3IrunTnu6sdzRCrqyruQ 20 | RmksX1XHQgbTwr4/fqzw8xp2eBcIG8qg5GNOpbkrlEkykOYzTXdO0LukuXw4Tp/O 21 | KPA4o2mFu/fx4p9uY9ZS9X6zI9kJG4tI6kNZNNw0Q7lMUQ3rHyX8jQ79AoGADtFt 22 | YhlkFuZYaPgcodLDjvwg9Vw6bQ4jTw5H0c8Vwces7aTu0ffQ4iA7MDZMSuVQwgs8 23 | cniwO0vb3b1ICxwcIyKOx5lRasylm1oLsKSbfLV3P193WJ8BMY9S7OJLdPhKS9/v 24 | OE2rPLXCBaVWx1oaiU1ScfDMPOgoqrQXXOcHCgMCgYEArX/ykwaig+0mgxGPFtZH 25 | Z2VqvRS0z8NbwomBMuaQRMh3gzKQ7+uReu0kPEQ7Qwk8LNquBtVFqN7+fJ7LubFn 26 | xIKEdopJbNnlsLv/1anWp/BR72Kk7crToC7yC91BQn21nlXBeD5DEd/HmyDhIV+g 27 | FUY2srhE2h3/FDQTcqidtJY= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/static/cert/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDljCCAn4CCQCz+CLD9I1KxTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMC 3 | REUxDDAKBgNVBAgMA05SVzEOMAwGA1UEBwwFRWFydGgxFzAVBgNVBAoMDlJhbmRv 4 | bSBDb21wYW55MQswCQYDVQQLDAJJVDEXMBUGA1UEAwwOd3d3LnJhbmRvbS5jb20x 5 | IDAeBgkqhkiG9w0BCQEWETE1NTM3NjU1MjZAcXEuY29tMB4XDTIxMDQyNTAyMjUx 6 | OVoXDTMxMDQyMzAyMjUxOVowgYwxCzAJBgNVBAYTAkRFMQwwCgYDVQQIDANOUlcx 7 | DjAMBgNVBAcMBUVhcnRoMRcwFQYDVQQKDA5SYW5kb20gQ29tcGFueTELMAkGA1UE 8 | CwwCSVQxFzAVBgNVBAMMDnd3dy5yYW5kb20uY29tMSAwHgYJKoZIhvcNAQkBFhEx 9 | NTUzNzY1NTI2QHFxLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 10 | AJ5XF9cIn8T6s522BbjX5BUbB/KJY2k75JlhBkjszYGtQv2Jt8gqmwwXiqq0/QLW 11 | TuylVgkr4pcsGawzs0aQghz1Jmq/3TsZsc9f8sfq4yDpLi6RLqSln2hhDppuiBe8 12 | DFTM8iO5EzfVAV62C4fPO06cit6AqmqmdIfr3i9E0Swp12MbExuGh+giFyYsz2a3 13 | TaQSCNPGsXZyxTBpNaRFq7fmj3plJWmNUpzt/00wwHl2DF73LwqipLqZmJayO9HQ 14 | eR9b9i50iyx+FQB3adYrBJz6Si+whJjQ/b/ZCSP60m7lFR0P5dFy6OhaAb3Sn1Dd 15 | 2bOeJw39KbNhhiiGtmC4YWcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAXGTm9Der 16 | N/34KM++mmuEQkE+bDuciMSrnqTCbz7PLfqFCAOxmz0+fCNhG3O2Qsaw+NgzypdZ 17 | YGe+ZkT36x6baSL4iV2XveCxuv9rAkwHcRKBrz0MgyYMg1Al/ZkQ/kTngf3XKYyU 18 | tGm8ABuIX3Z1SX9p2piL3RSXu65deaD93ND29TyIrudbOt9JE1W2WmTcXNN60H+x 19 | tgkd9j4DFxKwwImjVz4Ad3Tzn3NMQk4x6s6C43auq2UOV88HYB+fgsLryilkgNlo 20 | ZJTLHmQasPOESltaatHNhLnGgbWnyGg6e7iNBN+vmp5mXWoKuLF8ONlW3dvLZZZy 21 | tt7nj2+WZ1BqRA== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /test/static/cert/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuq4IAbnquGs+1 3 | Ino02zoF3tHRrji8YgrMIrdeoZAHF5IVGgy16quJ1v3odOyj3xoETB517LnaDctf 4 | TElA1d3+dXPHhCsb7t0A8Kx7x89AJqZrpBkcsmEHINc+sYQqvgi9IW9AskbideUb 5 | XNWqeVlunpOGSQX0aj232hh2r18ad8e7ixtLrRqtQ3z+CpKbabk5pmisXtANwO/C 6 | +zqIqdejoxKM/hlmLq3c6Eaqm9ghtlGUVPFHjRjZdc1DguZaYsKz+Q46Ziwj8IA4 7 | HL2qA3AZO6u68PjzNliNqjxLmGfG6ie+pU7vc2o/8W/4qrQk0Q8N4dxcLz+Ymivh 8 | D8wSHYfZAgMBAAECggEAVSipKSy0A05vFhDJJBv+Hf6UrXYTk0T5nq0OWcTLQ6nq 9 | Pv+EUu0m1P1MrZjUBtEvDglOkI/pzLYNAVlgHYwnv3fkCtIVcnjypxKwBWlVUiGF 10 | 4dUPqT4OsDsCtj42AeDEwfEbHJ/Oj3qLSNvqEEM5pwUJR98yWAe0L37VjofSBkSx 11 | hGspqOHSnCIJQ0QsAZ+OxlpNyjMuqphGCnhhIe4ALzFl190C2hJ7z0dZAsuRSkLw 12 | +5vfaT4+wl5daNMgfI4encdjLd1bkxggMD2cv61JUheINvsdWKf41pQGkIRu9H+G 13 | ll3mHajLb63f9wZsQg2PWLzf/tnqq5iEWoliCROyQQKBgQDZrwH5SiN2NPzJa3f1 14 | 49yT2lVicd+zXyvZeJY+goafRt+Mbez6JP4r5cCKCbTQqzNVuZZ5cjLkhnN6Hc/I 15 | T/uYCAKwpxKMt4vHwMKqV2yX9NN1JN/XBVyecknQWerWW5K50A6wC/51n9g3oyAz 16 | a7v3OiFgzDeBluvroTHAIlDufwKBgQDNakVVopVYFWcXQ3+lQmtMzuY0+4vcOBO8 17 | Z+QvuybrudGVr8XqYfbGJk3/+fWcP1rl0SegLEkXOD1sV/Jepu/bvPa+D+zddwD1 18 | 7nXKBLI0kubprsgHPCIkxkg3WUwlaOsEo49XhV9YrPipfRnsdvwVmPEgMg0zxWee 19 | D9A6tQqNpwKBgCeGu5kEeUPxgyUfunyPj3HZZz+k5bWwRkoKt11KXh91wwnAvBL/ 20 | vJdD9J4b/RUWwQ+Dz2rl7Y/JShaWazA4Nbr5WWOyMpASk/MFcVN05GcDMZJHy81D 21 | T/oFTpnied3Kau4KdWBKDT5Wc/BGUoaDvXG6wGzPKBUDznrOnjYBBSkJAoGAdU13 22 | lKSIpklqmpVYIlZgkfcg7SaswrBfTNsDKuK0Ii716YX7/pG864DaUBA82uIvFUgw 23 | Wb6QaqsaIHHEnZq1JIWvXfAYMowPx2FKcHfoEC/Hn7DI16DWWqvEd58N5dsZQofo 24 | hnFKdogoZBlloWx9HhQ9tkX+1g6n0lJzdeMGyMcCgYB9wV1DVzF8PEtsAHRWoNwj 25 | mOUEPwDhGy/lGsmivSIRXZ/4evoPsXnT/da+KcL+IT0oMRfq2i0XjJIgJKj5hHvh 26 | +3k752ncViB5xo+g7vYTvWxzARmrHSmIvIsJrhTQ2o3sRL3saHK0Ei00aytL3yXD 27 | bsPfWf3XpXjKyEOKCX57rQ== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/static/cert/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDljCCAn4CCQDuqr95JdXErjANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMC 3 | REUxDDAKBgNVBAgMA05SVzEOMAwGA1UEBwwFRWFydGgxFzAVBgNVBAoMDlJhbmRv 4 | bSBDb21wYW55MQswCQYDVQQLDAJJVDEXMBUGA1UEAwwOd3d3LnJhbmRvbS5jb20x 5 | IDAeBgkqhkiG9w0BCQEWETE1NTM3NjU1MjZAcXEuY29tMB4XDTIxMDQyNTAyMjQ1 6 | NFoXDTMxMDQyMzAyMjQ1NFowgYwxCzAJBgNVBAYTAkRFMQwwCgYDVQQIDANOUlcx 7 | DjAMBgNVBAcMBUVhcnRoMRcwFQYDVQQKDA5SYW5kb20gQ29tcGFueTELMAkGA1UE 8 | CwwCSVQxFzAVBgNVBAMMDnd3dy5yYW5kb20uY29tMSAwHgYJKoZIhvcNAQkBFhEx 9 | NTUzNzY1NTI2QHFxLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 10 | AK6rggBueq4az7UiejTbOgXe0dGuOLxiCswit16hkAcXkhUaDLXqq4nW/eh07KPf 11 | GgRMHnXsudoNy19MSUDV3f51c8eEKxvu3QDwrHvHz0AmpmukGRyyYQcg1z6xhCq+ 12 | CL0hb0CyRuJ15Rtc1ap5WW6ek4ZJBfRqPbfaGHavXxp3x7uLG0utGq1DfP4Kkptp 13 | uTmmaKxe0A3A78L7Ooip16OjEoz+GWYurdzoRqqb2CG2UZRU8UeNGNl1zUOC5lpi 14 | wrP5DjpmLCPwgDgcvaoDcBk7q7rw+PM2WI2qPEuYZ8bqJ76lTu9zaj/xb/iqtCTR 15 | Dw3h3FwvP5iaK+EPzBIdh9kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEATfxmm0sa 16 | 6oqDD9PwZSFaBlWttasSYQwseES1EjVTfmzuOZeV65f+dUa2l6x4r+ux197GP9yj 17 | +oQVYlrpr6rFlcKTkq2kIgrruRIDuwrG95AIRM1u0eIPMq0/0WJUE4QBNLeBOqWk 18 | qmagLB6PeDhLADI301BlBbm5PlbbT0enq6mwVoyrNFC/2WK5g6aU0xeBbiu5/xgB 19 | 9Z83O2fnSvFLTmlb9x3qauRZvuiQku5LmFzAEEN94aEXtUlY3PRZF6QF7HBke1dP 20 | QxjMXn6qfKQU/42QE5bzGBrm4FXGKhD+h6a/q2qCltwzfsDq2s+N5vwq5nbxqBYg 21 | uowUuDEZuC3xFQ== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /test/static/hello.html: -------------------------------------------------------------------------------- 1 |

Hello Cranker4go

-------------------------------------------------------------------------------- /test/static/img/nana.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchcc/crank4go/b961b8b3bbf7434f36b20e229d72e90ea4629a55/test/static/img/nana.jpg -------------------------------------------------------------------------------- /util/catchall.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "net" 4 | 5 | func IsNotLocalEnv() bool { 6 | return true 7 | } 8 | 9 | func GetFreePort() (int, error) { 10 | addr, err := net.ResolveTCPAddr("tcp", "localhost:0") 11 | if err != nil { 12 | return 0, err 13 | } 14 | 15 | l, err := net.ListenTCP("tcp", addr) 16 | if err != nil { 17 | return 0, err 18 | } 19 | defer l.Close() 20 | return l.Addr().(*net.TCPAddr).Port, nil 21 | } 22 | -------------------------------------------------------------------------------- /util/catchall_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetFreePort(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | want int 11 | wantErr bool 12 | }{ 13 | {name: "getPortCase1", want: 0, wantErr: false}, 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | got, err := GetFreePort() 18 | t.Logf("got port %d", got) 19 | if (err != nil) != tt.wantErr { 20 | t.Errorf("GetFreePort() error = %v, wantErr %v", err, tt.wantErr) 21 | return 22 | } 23 | if got <= tt.want { 24 | t.Errorf("GetFreePort() got = %v, want %v", got, tt.want) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /util/connection_monitor.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "runtime" 8 | "strings" 9 | "sync/atomic" 10 | ) 11 | 12 | type DataPublishHandler interface { 13 | PublishData(key string, value int) 14 | } 15 | 16 | type DataPublishHandlerFunc func(key string, value int) 17 | 18 | func (f DataPublishHandlerFunc) PublishData(key string, value int) { 19 | f(key, value) 20 | } 21 | 22 | type ConnectionMonitor struct { 23 | requestNum int32 24 | availableConns int32 25 | openFiles int64 26 | dataPublishHandlers []DataPublishHandler 27 | } 28 | 29 | func NewConnectionMonitor(handlers []DataPublishHandler) *ConnectionMonitor { 30 | return &ConnectionMonitor{ 31 | dataPublishHandlers: handlers, 32 | } 33 | } 34 | 35 | func (m *ConnectionMonitor) OnConnectionStarted() { 36 | m.reportConnCount(atomic.AddInt32(&m.requestNum, 1)) 37 | m.reportOpenFilesCount() 38 | LOG.Infof("activeConnections=%d, openFiles=%d", m.requestNum, m.openFiles) 39 | } 40 | 41 | func (m *ConnectionMonitor) OnConnectionStarted2(path, routerSocketID string) { 42 | m.reportConnCount(atomic.AddInt32(&m.requestNum, 1)) 43 | m.reportOpenFilesCount() 44 | if path == "" { 45 | path = "default" 46 | } 47 | LOG.Infof("activeConnections=%d, openFiles=%d, routerName=%s, routerSocketID=%s", m.requestNum, m.openFiles, path, routerSocketID) 48 | } 49 | 50 | // OnConnectionEnded called by connector socket 51 | func (m *ConnectionMonitor) OnConnectionEnded() { 52 | m.reportConnCount(atomic.AddInt32(&m.requestNum, -1)) 53 | m.reportOpenFilesCount() 54 | LOG.Infof("activeConnections=%d, openFiles=%d", m.requestNum, m.openFiles) 55 | } 56 | 57 | func (m *ConnectionMonitor) OnConnectionEnded2(path, routerSocketID string) { 58 | m.reportConnCount(atomic.AddInt32(&m.requestNum, -1)) 59 | m.reportOpenFilesCount() 60 | LOG.Infof("activeConnections=%d, openFiles=%d", m.requestNum, m.openFiles) 61 | if path == "" { 62 | path = "default" 63 | } 64 | LOG.Infof("activeConnections=%d, openFiles=%d, routerName=%s, routerSocketID=%s", m.requestNum, m.openFiles, path, routerSocketID) 65 | } 66 | 67 | // OnConnectionEnded3 called by router socket 68 | func (m *ConnectionMonitor) OnConnectionEnded3(routerSocketID, path, reqComponentName string, respStatus int, reqDuration, reqBytes, respBytes int64) { 69 | activeRequest := atomic.AddInt32(&m.requestNum, -1) 70 | if path == "" { 71 | path = "default" 72 | } 73 | for _, handler := range m.dataPublishHandlers { 74 | m.reportConnCount2(handler, activeRequest) 75 | m.reportOpenFilesCount2(handler) 76 | handler.PublishData("request.requestBytes,path="+path, int(reqBytes)) 77 | handler.PublishData("request.duration,path="+path, int(reqDuration)) 78 | handler.PublishData("request.responseStatus,path="+path, respStatus) 79 | handler.PublishData("request.responseBytes,path="+path, int(respBytes)) 80 | } 81 | LOG.Infof("activeConnection=%d, "+ 82 | "openFiles=%d, "+ 83 | "routerName=%s, "+ 84 | "routerSocketID=%s, "+ 85 | "request_%s_requestBytes=%d, "+ 86 | "request_%s_duration=%d, "+ 87 | "request_%s_responseStatus=%d, "+ 88 | "request_%s_responseBytes=%d, "+ 89 | "request_%s_requestComponentName=%s", 90 | m.requestNum, m.openFiles, path, routerSocketID, 91 | path, reqBytes, 92 | path, reqDuration, 93 | path, respStatus, 94 | path, respBytes, 95 | path, reqComponentName) 96 | } 97 | 98 | func (m *ConnectionMonitor) OnConnectionAvailable() { 99 | m.reportActiveConnCount(atomic.AddInt32(&m.availableConns, 1)) 100 | } 101 | 102 | // OnConnectionConsumed called by connector 103 | func (m *ConnectionMonitor) OnConnectionConsumed() { 104 | m.reportActiveConnCount(atomic.AddInt32(&m.availableConns, -1)) 105 | } 106 | 107 | func (m *ConnectionMonitor) ReportWebsocketPoolSize(size int) { 108 | for _, handler := range m.dataPublishHandlers { 109 | handler.PublishData("websocket.pool.size", size) 110 | } 111 | } 112 | 113 | func (m *ConnectionMonitor) ConnectionCount() int { 114 | return int(atomic.LoadInt32(&m.requestNum)) 115 | } 116 | 117 | func (m *ConnectionMonitor) AvailableConns() int { 118 | return int(m.availableConns) 119 | } 120 | 121 | func (m *ConnectionMonitor) OpenFiles() int { 122 | return int(m.openFiles) 123 | } 124 | 125 | func (m *ConnectionMonitor) reportConnCount(newInflightReqCount int32) { 126 | for _, handler := range m.dataPublishHandlers { 127 | m.reportConnCount2(handler, newInflightReqCount) 128 | m.reportOpenFilesCount2(handler) 129 | } 130 | } 131 | 132 | func (m *ConnectionMonitor) reportConnCount2(handler DataPublishHandler, newInflightReqCount int32) { 133 | handler.PublishData("connections", int(newInflightReqCount)) 134 | 135 | } 136 | 137 | func (m *ConnectionMonitor) reportOpenFilesCount() { 138 | for _, handler := range m.dataPublishHandlers { 139 | m.reportOpenFilesCount2(handler) 140 | } 141 | } 142 | 143 | // disable for better performance 144 | func (m *ConnectionMonitor) reportOpenFilesCount2(handler DataPublishHandler) { 145 | return 146 | // if os is Unix. 147 | if runtime.GOOS == "linux" || runtime.GOOS == "openbsd" || runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" { 148 | go func() { 149 | out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("lsof -p %v", os.Getpid())).Output() 150 | if err != nil { 151 | fmt.Println(err.Error()) 152 | } 153 | lines := strings.Split(string(out), "\n") 154 | handler.PublishData("openFiles", len(lines)-1) 155 | }() 156 | } 157 | 158 | } 159 | 160 | func (m *ConnectionMonitor) reportActiveConnCount(newActiveConnCount int32) { 161 | for _, handler := range m.dataPublishHandlers { 162 | handler.PublishData("availableConnections", int(newActiveConnCount)) 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /util/exception.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type CrankerErr struct { 4 | Msg string 5 | Code int 6 | } 7 | 8 | func (c *CrankerErr) Error() string { 9 | return c.Msg 10 | } 11 | func (c *CrankerErr) ErrCode() int { 12 | return c.Code 13 | } 14 | 15 | type CancelErr struct { 16 | Msg string 17 | } 18 | 19 | func (c CancelErr) Error() string { 20 | return "cancel error, detail: " + c.Msg 21 | } 22 | 23 | // HttpClientPolicyErr An error is returned if caused by client policy (such as 24 | // CheckRedirect), or failure to speak HTTP (such as a network 25 | // connectivity problem). A non-2xx status code doesn't cause an 26 | // error. 27 | type HttpClientPolicyErr struct { 28 | Msg string 29 | } 30 | 31 | func (pe HttpClientPolicyErr) Error() string { 32 | return "err caused by client policy, (such as checkRedirect) or failure to speak http (such as a network connectivity problem), detail: " + pe.Msg 33 | } 34 | 35 | type NoRouteErr struct { 36 | Msg string 37 | } 38 | 39 | func (err NoRouteErr) Error() string { 40 | return err.Msg 41 | } 42 | 43 | type TimeoutErr struct { 44 | Msg string 45 | } 46 | 47 | func (err TimeoutErr) Error() string { 48 | return err.Msg 49 | } 50 | -------------------------------------------------------------------------------- /util/log.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/op/go-logging" 7 | ) 8 | 9 | var LOG = logging.MustGetLogger("crank4go") 10 | 11 | // Example format string. Everything except the message has a custom color 12 | // which is dependent on the log level. Many fields have a custom output 13 | // formatting too, eg. the time returns the hour down to the milli second. 14 | var format = logging.MustStringFormatter( 15 | `%{color}%{shortfile} %{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`, 16 | ) 17 | 18 | // Password is just an example type implementing the Redactor interface. Any 19 | // time this is logged, the Redacted() function will be called. 20 | type Password string 21 | 22 | func (p Password) Redacted() interface{} { 23 | return logging.Redact(string(p)) 24 | } 25 | 26 | func init() { 27 | // For demo purposes, create two backend for os.Stderr. 28 | backend1 := logging.NewLogBackend(os.Stderr, "", 0) 29 | backend2 := logging.NewLogBackend(os.Stderr, "", 0) 30 | 31 | // For messages written to backend2 we want to add some additional 32 | // information to the output, including the used log level and the name of 33 | // the function. 34 | backend2Formatter := logging.NewBackendFormatter(backend2, format) 35 | 36 | // Only errors and more severe messages should be sent to backend1 37 | backend1Leveled := logging.AddModuleLevel(backend1) 38 | backend1Leveled.SetLevel(logging.ERROR, "") 39 | 40 | // Set the backends to be used. 41 | logging.SetBackend(backend1Leveled, backend2Formatter) 42 | } 43 | -------------------------------------------------------------------------------- /util/shutdown.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | ) 9 | 10 | // ExitProgram a helper function to execute some hooks before exiting the program. 11 | // usage: 12 | // 1. wrapper all things that you want to do in a func for example `exitFunc ()` 13 | // 2. call `ExitProgram(ExitFunc)` inside a `init` func of your program 14 | func ExitProgram(exitFunc func()) { 15 | c := make(chan os.Signal) 16 | // listen to specified signals: ctrl+c kill 17 | signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2) 18 | go func() { 19 | for s := range c { 20 | switch s { 21 | case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: 22 | fmt.Println("executing shutdownHooks before exit...") 23 | exitFunc() 24 | fmt.Println("all hooks are executed. shutting down the program...") 25 | os.Exit(0) 26 | case syscall.SIGUSR1: 27 | fmt.Println("usr1", s) 28 | case syscall.SIGUSR2: 29 | fmt.Println("usr2", s) 30 | default: 31 | fmt.Println("other", s) 32 | } 33 | } 34 | }() 35 | } 36 | -------------------------------------------------------------------------------- /util/toggle.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | const ( 4 | UUI31223NotSendingHostHeader = false 5 | UUI51288AllowFixedLengthResponses = false 6 | UUI51292EnableResponseTimeouts = false 7 | ) 8 | --------------------------------------------------------------------------------