├── .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 |
--------------------------------------------------------------------------------