├── .gitignore ├── README.md ├── cmd ├── client │ └── client.go ├── server │ └── server.go └── stress_testing │ └── client.go ├── conn.go ├── epoll.go ├── go.mod ├── go.sum ├── jsclient ├── index.html └── main.js ├── message.go ├── server.go └── tls ├── cert.pem └── key.pem /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Makefile -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crab 2 | 3 | golang写的websocket服务,支持超过连接数(实测12w+稳定连接,程序自带压测工具,感兴趣可以测试下)。 4 | 5 | 受[1m-go-websockets](https://github.com/eranyanay/1m-go-websockets)启发 6 | 7 | ## usage 8 | 9 | run 10 | 11 | ```bash 12 | $ run cmd/server/server.go 13 | #open a new terminal 14 | $ run cmd/client/client.go 15 | #open a new terminal 16 | $ curl -X POST -d "sn=no123456&msg=msg1" http://localhost:9333/send_msg 17 | $ curl -X POST -d "channel=channel1&msg=broadcastinchannel" http://localhost:9333/broadcastinchannel 18 | $ curl -X POST -d "msg=broadcastmsg" http://localhost:9333/broadcast 19 | ``` -------------------------------------------------------------------------------- /cmd/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/url" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/widaT/crab" 12 | ) 13 | 14 | var addr = flag.String("addr", "localhost:8888", "http service address") 15 | var isTLS = flag.Bool("tls", false, "use websocket over tls") 16 | 17 | func main() { 18 | flag.Parse() 19 | log.SetFlags(0) 20 | 21 | interrupt := make(chan os.Signal, 1) 22 | signal.Notify(interrupt, os.Interrupt) 23 | 24 | Scheme := "ws" 25 | if *isTLS { 26 | Scheme = "wss" 27 | } 28 | u := url.URL{Scheme: Scheme, Host: *addr, Path: "/", RawQuery: "token=abcd"} 29 | log.Printf("connecting to %s", u.String()) 30 | 31 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 32 | if err != nil { 33 | log.Fatal("dial:", err) 34 | } 35 | defer c.Close() 36 | 37 | done := make(chan struct{}) 38 | 39 | go func() { 40 | defer close(done) 41 | for { 42 | _, message, err := c.ReadMessage() 43 | if err != nil { 44 | log.Println("read:", err) 45 | return 46 | } 47 | log.Printf("recv: %s", message) 48 | } 49 | }() 50 | 51 | //第一条消息注册下设备sn 52 | message := crab.Message{ 53 | Type: crab.TJoin, 54 | Payload: "no123456", 55 | Channel: "channel1", 56 | } 57 | c.WriteMessage(websocket.TextMessage, message.ToJSON()) 58 | 59 | for { 60 | select { 61 | case <-done: 62 | return 63 | case <-interrupt: 64 | log.Println("interrupt") 65 | err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 66 | if err != nil { 67 | log.Println("write close:", err) 68 | return 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cmd/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/widaT/crab" 5 | ) 6 | 7 | func main() { 8 | crab.Run() 9 | } 10 | -------------------------------------------------------------------------------- /cmd/stress_testing/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "time" 12 | 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | var ( 17 | ip = flag.String("ip", "172.30.80.27", "server IP") 18 | connections = flag.Int("conn", 20000, "number of websocket connections") 19 | ) 20 | 21 | func main() { 22 | flag.Usage = func() { 23 | io.WriteString(os.Stderr, `Websockets client generator 24 | Example usage: ./client -ip=172.30.80.27 -conn=10 25 | `) 26 | flag.PrintDefaults() 27 | } 28 | flag.Parse() 29 | 30 | u := url.URL{Scheme: "ws", Host: *ip + ":8888", Path: "/", RawQuery: "token=abcd"} 31 | log.Printf("Connecting to %s", u.String()) 32 | // message := crab.Message{ 33 | // Type: crab.TJoin, 34 | // } 35 | var conns []*websocket.Conn 36 | header := make(http.Header) 37 | header.Add("Sec-WebSocket-Protocol", "rust-websocket") 38 | 39 | for i := 0; i < *connections; i++ { 40 | c, _, err := websocket.DefaultDialer.Dial(u.String(), header) 41 | if err != nil { 42 | fmt.Println("Failed to connect", i, err) 43 | break 44 | } 45 | 46 | conns = append(conns, c) 47 | defer func() { 48 | c.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second)) 49 | time.Sleep(time.Second) 50 | c.Close() 51 | }() 52 | } 53 | 54 | log.Printf("Finished initializing %d connections", len(conns)) 55 | tts := time.Second 56 | if *connections > 100 { 57 | tts = time.Millisecond * 5 58 | } 59 | for { 60 | for i := 0; i < len(conns); i++ { 61 | time.Sleep(tts) 62 | conn := conns[i] 63 | log.Printf("Conn %d sending message", i) 64 | if err := conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(time.Second*5)); err != nil { 65 | fmt.Printf("Failed to receive pong: %v", err) 66 | } 67 | //conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Hello from conn %v", i))) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package crab 2 | 3 | import ( 4 | "log" 5 | "net" 6 | ) 7 | 8 | type Conn struct { 9 | Sn string 10 | Channel string 11 | C net.Conn 12 | S *Server 13 | } 14 | 15 | func (c *Conn) Close() error { 16 | c.S.Epoller.Remove(c) 17 | if len(c.Sn) > 0 { 18 | c.S.Deregister(c.Sn) 19 | log.Printf("sn:%s closed", c.Sn) 20 | } 21 | 22 | if len(c.Channel) > 0 { 23 | c.S.RemoveFromChannels(c.Channel, c.Sn) 24 | log.Printf("remove sn:%s from %s Channels", c.Sn, c.Channel) 25 | } 26 | 27 | if c.C != nil { 28 | c.C.Close() 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /epoll.go: -------------------------------------------------------------------------------- 1 | package crab 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "sync" 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | type epoll struct { 13 | fd int 14 | connections map[int]*Conn 15 | lock *sync.RWMutex 16 | } 17 | 18 | func MkEpoll() (*epoll, error) { 19 | fd, err := unix.EpollCreate1(0) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &epoll{ 24 | fd: fd, 25 | lock: &sync.RWMutex{}, 26 | connections: make(map[int]*Conn), 27 | }, nil 28 | } 29 | 30 | func (e *epoll) Add(conn *Conn) error { 31 | fd := websocketFD(conn.C) 32 | err := unix.EpollCtl(e.fd, syscall.EPOLL_CTL_ADD, fd, &unix.EpollEvent{Events: unix.POLLIN | unix.POLLHUP, Fd: int32(fd)}) 33 | if err != nil { 34 | return err 35 | } 36 | e.lock.Lock() 37 | defer e.lock.Unlock() 38 | e.connections[fd] = conn 39 | return nil 40 | } 41 | 42 | func (e *epoll) Remove(conn *Conn) error { 43 | fd := websocketFD(conn.C) 44 | err := unix.EpollCtl(e.fd, syscall.EPOLL_CTL_DEL, fd, nil) 45 | if err != nil { 46 | return err 47 | } 48 | e.lock.Lock() 49 | defer e.lock.Unlock() 50 | delete(e.connections, fd) 51 | return nil 52 | } 53 | 54 | func (e *epoll) Wait() ([]*Conn, error) { 55 | events := make([]unix.EpollEvent, 100) 56 | n, err := unix.EpollWait(e.fd, events, -1) 57 | if err != nil { 58 | return nil, err 59 | } 60 | e.lock.RLock() 61 | defer e.lock.RUnlock() 62 | var connections []*Conn 63 | for i := 0; i < n; i++ { 64 | conn := e.connections[int(events[i].Fd)] 65 | connections = append(connections, conn) 66 | } 67 | return connections, nil 68 | } 69 | 70 | func websocketFD(conn net.Conn) int { 71 | tcpConn := reflect.Indirect(reflect.ValueOf(conn)).FieldByName("conn") 72 | fdVal := tcpConn.FieldByName("fd") 73 | pfdVal := reflect.Indirect(fdVal).FieldByName("pfd") 74 | 75 | return int(pfdVal.FieldByName("Sysfd").Int()) 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/widaT/crab 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gobwas/ws v1.1.0 7 | github.com/gorilla/websocket v1.5.0 8 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d 9 | ) 10 | 11 | require ( 12 | github.com/gobwas/httphead v0.1.0 // indirect 13 | github.com/gobwas/pool v0.2.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 2 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 3 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 4 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 5 | github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= 6 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= 7 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 8 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 9 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d h1:MiWWjyhUzZ+jvhZvloX6ZrUsdEghn8a64Upd8EMHglE= 10 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | -------------------------------------------------------------------------------- /jsclient/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | crab js client demo 7 | 8 | 9 | 10 | 11 |
12 | 13 |

curl -X POST -d "sn=no123456&msg=msg1" http://localhost:9333/send_msg

14 |

curl -X POST -d "channel=channel1&msg=broadcastinchannel" http://localhost:9333/broadcastinchannel

15 |

curl -X POST -d "msg=broadcastmsg" http://localhost:9333/broadcast

16 |
17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /jsclient/main.js: -------------------------------------------------------------------------------- 1 | // Create WebSocket connection. 2 | const socket = new WebSocket('ws://localhost:8888/?token=abcd'); 3 | 4 | // Connection opened 5 | socket.addEventListener('open', function (event) { 6 | //认证 7 | a ={type:0,payload:"no123456",channel:"channel1"} 8 | console.log(JSON.stringify(a)) 9 | socket.send(JSON.stringify(a)); 10 | }); 11 | 12 | // Listen for messages 13 | socket.addEventListener('message', function (event) { 14 | const e = document.getElementById("messages"); 15 | e.innerHTML = e.innerHTML + 'Message from server '+ event.data +"
"; 16 | 17 | console.log('Message from server ', event.data) 18 | }); -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package crab 2 | 3 | import "encoding/json" 4 | 5 | type MessageType uint8 6 | 7 | const ( 8 | TJoin MessageType = iota 9 | ) 10 | 11 | type Message struct { 12 | Type MessageType `json:"type"` 13 | Payload string `json:"payload"` 14 | Channel string `json:"channel"` 15 | } 16 | 17 | func (m *Message) ToJSON() []byte { 18 | b, _ := json.Marshal(m) 19 | return b 20 | } 21 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package crab 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "sync" 11 | 12 | "github.com/gobwas/ws" 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | type Server struct { 17 | Sn2Conn sync.Map 18 | Channels sync.Map 19 | 20 | Handler func(c *Conn, in []byte) error 21 | Epoller *epoll 22 | } 23 | 24 | var server *Server 25 | 26 | func wsHandler(w http.ResponseWriter, r *http.Request) { 27 | // Upgrade connection 28 | c, _, _, err := ws.UpgradeHTTP(r, w) 29 | if err != nil { 30 | return 31 | } 32 | 33 | // 认证 34 | // if r.URL.Query().Get("token") != "abcd" { 35 | // w.Write([]byte("forbidden")) 36 | // return 37 | // } 38 | 39 | conn := &Conn{ 40 | C: c, 41 | S: server, 42 | } 43 | if err := server.Epoller.Add(conn); err != nil { 44 | log.Printf("Failed to add connection %v", err) 45 | conn.Close() 46 | } 47 | } 48 | func runApiServer() { 49 | http.HandleFunc("/send_msg", func(rw http.ResponseWriter, r *http.Request) { 50 | r.ParseForm() 51 | var sn, message string 52 | if len(r.PostForm["sn"]) > 0 { 53 | sn = r.PostForm["sn"][0] 54 | } 55 | if len(r.PostForm["msg"]) > 0 { 56 | message = r.PostForm["msg"][0] 57 | } 58 | 59 | log.Printf("post data sn:%q msg:%q", sn, message) 60 | sendMessage(sn, []byte(message)) 61 | }) 62 | http.HandleFunc("/broadcastinchannel", func(rw http.ResponseWriter, r *http.Request) { 63 | r.ParseForm() 64 | var channel, message string 65 | if len(r.PostForm["channel"]) > 0 { 66 | channel = r.PostForm["channel"][0] 67 | } 68 | if len(r.PostForm["msg"]) > 0 { 69 | message = r.PostForm["msg"][0] 70 | } 71 | 72 | log.Printf("post data channel:%q msg:%q", channel, message) 73 | broadcastinchannel(channel, []byte(message)) 74 | }) 75 | http.HandleFunc("/broadcast", func(rw http.ResponseWriter, r *http.Request) { 76 | r.ParseForm() 77 | var message string 78 | 79 | if len(r.PostForm["msg"]) > 0 { 80 | message = r.PostForm["msg"][0] 81 | } 82 | 83 | log.Printf("post data msg:%q", message) 84 | broadcast([]byte(message)) 85 | }) 86 | 87 | http.ListenAndServe(":9333", nil) 88 | } 89 | 90 | func sendMessage(sn string, message []byte) error { 91 | c := server.GetConnBySn(sn) 92 | if c != nil { 93 | frame := ws.NewTextFrame(message) 94 | ws.WriteFrame(c.C, frame) 95 | return nil 96 | } 97 | return nil 98 | //return errors.New("sn2connection error") 99 | } 100 | func broadcastinchannel(channel string, message []byte) error { 101 | if c, ok := server.Channels.Load(channel); ok { 102 | frame := ws.NewTextFrame(message) 103 | c.(*sync.Map).Range(func(_, c any) bool { 104 | ws.WriteFrame(c.(*Conn).C, frame) 105 | return true 106 | }) 107 | } 108 | return nil 109 | } 110 | 111 | func broadcast(message []byte) error { 112 | frame := ws.NewTextFrame(message) 113 | server.Sn2Conn.Range(func(_, c any) bool { 114 | ws.WriteFrame(c.(*Conn).C, frame) 115 | return true 116 | }) 117 | return nil 118 | } 119 | 120 | func Run() { 121 | go runApiServer() 122 | 123 | epoller, err := MkEpoll() 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | server = &Server{ 129 | Sn2Conn: sync.Map{}, 130 | Channels: sync.Map{}, 131 | Handler: HandleMsg, 132 | Epoller: epoller, 133 | } 134 | go server.Start() 135 | 136 | http.HandleFunc("/", wsHandler) 137 | if err := http.ListenAndServe("0.0.0.0:8888", nil); err != nil { 138 | log.Fatal(err) 139 | } 140 | } 141 | 142 | func (s *Server) Start() error { 143 | for { 144 | connections, err := s.Epoller.Wait() 145 | if err != nil && err != unix.EINTR { 146 | log.Printf("Failed to epoll wait %v", err) 147 | continue 148 | } 149 | for _, conn := range connections { 150 | if conn == nil { 151 | break 152 | } 153 | header, err := ws.ReadHeader(conn.C) 154 | if err != nil { 155 | conn.Close() 156 | continue 157 | } 158 | 159 | switch header.OpCode { 160 | case ws.OpPing: 161 | ws.WriteFrame(conn.C, ws.NewPongFrame([]byte(""))) 162 | continue 163 | case ws.OpContinuation | ws.OpPong: 164 | continue 165 | case ws.OpClose: 166 | log.Printf("got close message") 167 | conn.Close() 168 | continue 169 | default: 170 | } 171 | 172 | payload := make([]byte, header.Length) 173 | _, err = io.ReadFull(conn.C, payload) 174 | if err != nil { 175 | return err 176 | } 177 | if header.Masked { 178 | ws.Cipher(payload, header.Mask, 0) 179 | } 180 | err = s.Handler(conn, payload) 181 | if err != nil { 182 | log.Printf("[err] handler error %s", err) 183 | conn.Close() 184 | } 185 | } 186 | } 187 | } 188 | 189 | func (s *Server) StoreConn(sn string, conn *Conn) { 190 | if c, ok := s.Sn2Conn.Load(sn); ok { 191 | c.(*Conn).Close() 192 | } 193 | s.Sn2Conn.Store(sn, conn) 194 | } 195 | 196 | func (s *Server) GetConnBySn(sn string) *Conn { 197 | if c, ok := s.Sn2Conn.Load(sn); ok { 198 | return c.(*Conn) 199 | } 200 | return nil 201 | } 202 | 203 | func (s *Server) Deregister(sn string) { 204 | s.Sn2Conn.Delete(sn) 205 | } 206 | 207 | func (s *Server) StoreChannel(channel, sn string, conn *Conn) { 208 | c, _ := s.Channels.LoadOrStore(channel, &sync.Map{}) 209 | c.(*sync.Map).Store(sn, conn) 210 | } 211 | 212 | func (s *Server) RemoveFromChannels(channel, sn string) { 213 | if c, ok := s.Channels.Load(channel); ok { 214 | c.(*sync.Map).Delete(sn) 215 | } 216 | } 217 | 218 | func HandleMsg(c *Conn, in []byte) error { 219 | var message = Message{} 220 | err := json.Unmarshal(in, &message) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | switch message.Type { 226 | case TJoin: 227 | if c.Sn == "" { 228 | c.Sn = message.Payload 229 | c.S.StoreConn(c.Sn, c) 230 | } 231 | if c.Channel == "" { 232 | c.Channel = message.Channel 233 | c.S.StoreChannel(c.Channel, c.Sn, c) 234 | } 235 | default: 236 | return errors.New("unreachable") 237 | } 238 | 239 | frame := ws.NewTextFrame([]byte("welcome: " + c.Sn)) 240 | ws.WriteFrame(c.C, frame) 241 | return nil 242 | } 243 | -------------------------------------------------------------------------------- /tls/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIECTCCAnGgAwIBAgIRAJIQqfq9rrtvT5I+rY2ZacAwDQYJKoZIhvcNAQELBQAw 3 | TzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMRIwEAYDVQQLDAl3aWRh 4 | QHdpZGExGTAXBgNVBAMMEG1rY2VydCB3aWRhQHdpZGEwHhcNMTkwNjAxMDAwMDAw 5 | WhcNMjkxMjEyMTA0NzI2WjA9MScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQg 6 | Y2VydGlmaWNhdGUxEjAQBgNVBAsMCXdpZGFAd2lkYTCCASIwDQYJKoZIhvcNAQEB 7 | BQADggEPADCCAQoCggEBANOxb49q6ZDzpANZkhUyHrlYE/YpE6o0b6GYqKikzocF 8 | rnjBudv2kxogk883chabj9W0JwUgsOKnWrXEmNrBkn/UDWVarD2WRNBdOZlMeBVp 9 | IXFlTJHdKvhvgy36zCrSWlscGjIc81pUDjzQDB3Bw3lUT40Qx3XikS0DxxAZ4S1X 10 | 6bMrB95x56RBD/2zrEboh+xO8dOrELIdJYO4yJ5LlPFTr2pF8tHavWQSLgQv1kGQ 11 | rRjTszz0bAaHVR7y/olSurLwrLwDor+AJqq0mVXXRheAljT1+uZ4LrcpkADmKX+T 12 | jcAGQFJeQWqUZcvDSnloXEi77Jmso2+Ni91YCMFuwnECAwEAAaNyMHAwDgYDVR0P 13 | AQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHwYD 14 | VR0jBBgwFoAU7rg1P8UTlk3BkAjdZn/yLaK3bR0wGgYDVR0RBBMwEYIJbG9jYWxo 15 | b3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBgQCQPrtIcGmv7AhKRN3E6u/gxdb9 16 | DhBshBO/DMe4HS7woWfaJMOsWtYk3m0joHQ8+EMSo+ue2M899VcB2cJGxYm+cMbZ 17 | jdi5eBSFEqL68s9HZjDTHN07k/FLk9x8emghf3svXreMiyz/U+l/fctfBHRORiI9 18 | B+XnOfDZsE6QW26kDrCT6aq7vVITlWRdyWuVr3VkCO89NwCrc0RstBKgn8pKN9Et 19 | JcueX6UdHt5hGRvWsL0zmg2rRRL2LeXQ7fJsZUaMfEqNiLf9rrfrHRgEvDWGqHeB 20 | vnkLQMPjirFpqDIMR0JSwteYBUQ7GJqYBncJuSxdy2ihsMgMau6NKyQrdnkRDFjH 21 | KGtykoKSUYWjrl9KqfSMflb5UA+pc2mgWf7sQBY8yVz/ukJRtTDTldbvoCPOoMJr 22 | CWnCpsGeuFvIUFxtTBXOCQetuEZEVH3TXTdl+QNchuucqgFJJy7gS25sVdhsXSLF 23 | I9LFXMD5HXpwC4B8/LixrfbY/I+S3Nmbo09mZdA= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /tls/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTsW+PaumQ86QD 3 | WZIVMh65WBP2KROqNG+hmKiopM6HBa54wbnb9pMaIJPPN3IWm4/VtCcFILDip1q1 4 | xJjawZJ/1A1lWqw9lkTQXTmZTHgVaSFxZUyR3Sr4b4Mt+swq0lpbHBoyHPNaVA48 5 | 0AwdwcN5VE+NEMd14pEtA8cQGeEtV+mzKwfeceekQQ/9s6xG6IfsTvHTqxCyHSWD 6 | uMieS5TxU69qRfLR2r1kEi4EL9ZBkK0Y07M89GwGh1Ue8v6JUrqy8Ky8A6K/gCaq 7 | tJlV10YXgJY09frmeC63KZAA5il/k43ABkBSXkFqlGXLw0p5aFxIu+yZrKNvjYvd 8 | WAjBbsJxAgMBAAECggEATM2GPRE71iQe8Dwx/NCnRvVGoLt8b/cAPS+mmTDZ4TDV 9 | gOhAJybJbeQ93CMwmDUuNQlcUsbuhfzTszi1gEBUh14G0ivYa6u2IaaA/DY7JwFy 10 | abKn2g/UxDeo7yibpDmXIJEMi/vm6nGujK+qha1D3yQQjIx24TUnGJ7TPOs2v/0I 11 | 4vSjy4vFaV1qFGaeJXpi6usCyCETubF5pjtt0+9sttAVr2biCpYi1PjAsC/KX1MF 12 | AnG5h5BMlFP3eofEg/Aq/hUKGiB4ThBDVV3ChYRGt5QW5xv5wfm1NxGFtsEEBexf 13 | lUml/8QQeM7kIg2o3zxZzuJPIdRq5AXKrzEN86N5QQKBgQD6lmniyk2CuuLAFxde 14 | zS2RdE+UPwzyYgmbiWm6uBSynlfWHxAeKshu2gwiD1dKIBASAL0TT8mU+eX7Derz 15 | e4EYAAkiTI06amxoGxMO+qdpn34A4wVWCYvoC0rKyR/QaZvEuQr2cbhARjDT2tB3 16 | euU1BPMrzqDCVV/LuXgQ/sFdSQKBgQDYQ/YOHH20WM5CccYEinH+FppHuP02Jxta 17 | FzHuAR4f2nPhp38SYkU1EGAEWFM6XgSPGxLYi6Iyvh0S+rhbuKhbUtq0cf9P9ibX 18 | GjVTUxeyvRSiRTGMEVoP504kv3WybbkbV6dvM4Tk70td/hCKhky0ppFFpx9sg/9R 19 | H/KlgZED6QKBgQDzer67i5GwmmHD/yvIA1hpYjgIdRLhV/y50dSXhlEmFNCHznrc 20 | tPk3LEpptbo83dZ/h5QvdXEKfZ3GSAmoNrKCXVaCqxr2TX2z0cq83TNZX40Q04TV 21 | ykTUXFR5oN52pIajqKBMQMakPQ8oag3wacrXr02PnR96c3W2/yj4am/7cQKBgBU3 22 | DYMy3k37zDgOayt03VvcocPZbNWMHo3G6rn7WBYt2uJPCRMnmrxq39BtTbTTFkVn 23 | lYCyqfWqWrXK7Jzqz3Et/pBnAnG4pvTA+Gw1IYUCMWz9tq+T4C7hNPQ75/LZ+qep 24 | AJ7TAf1nUBBnZP4B0LKECIo5q8Z8aGZv6OC9hhlJAoGABRlddITwBv22xTCvMT12 25 | PdovWyjwXVR2H9Hu9Utvl2rcKNnb+kh/fLqDC0kG+XoJdYFFqAX4/PD8ALlPGudV 26 | g+DO/Dj0KdD20Y2ap7CREfJTrR7L0va1OSziBjtOFE6lhf53vI5vtR53djQ4xlkG 27 | y3GtsOu3v0pvCtASBxAG7Ec= 28 | -----END PRIVATE KEY----- 29 | --------------------------------------------------------------------------------