b835a656279f259a90754d5deec03abb259ed6e5
[src/xds/xds-agent.git] / lib / webserver / server.go
1 package webserver
2
3 import (
4         "fmt"
5         "net/http"
6         "strings"
7
8         "github.com/Sirupsen/logrus"
9         "github.com/gin-gonic/gin"
10         "github.com/googollee/go-socket.io"
11         "github.com/iotbzh/xds-agent/lib/apiv1"
12         "github.com/iotbzh/xds-agent/lib/session"
13         "github.com/iotbzh/xds-agent/lib/xdsconfig"
14 )
15
16 // ServerService .
17 type ServerService struct {
18         router    *gin.Engine
19         api       *apiv1.APIService
20         sIOServer *socketio.Server
21         webApp    *gin.RouterGroup
22         cfg       *xdsconfig.Config
23         sessions  *session.Sessions
24         log       *logrus.Logger
25         stop      chan struct{} // signals intentional stop
26 }
27
28 const indexFilename = "index.html"
29 const cookieMaxAge = "3600"
30
31 // New creates an instance of ServerService
32 func New(conf *xdsconfig.Config, log *logrus.Logger) *ServerService {
33
34         // Setup logging for gin router
35         if log.Level == logrus.DebugLevel {
36                 gin.SetMode(gin.DebugMode)
37         } else {
38                 gin.SetMode(gin.ReleaseMode)
39         }
40
41         // TODO
42         //  - try to bind gin DefaultWriter & DefaultErrorWriter to logrus logger
43         //  - try to fix pb about isTerminal=false when out is in VSC Debug Console
44         //gin.DefaultWriter = ??
45         //gin.DefaultErrorWriter = ??
46
47         // Creates gin router
48         r := gin.New()
49
50         svr := &ServerService{
51                 router:    r,
52                 api:       nil,
53                 sIOServer: nil,
54                 webApp:    nil,
55                 cfg:       conf,
56                 log:       log,
57                 sessions:  nil,
58                 stop:      make(chan struct{}),
59         }
60
61         return svr
62 }
63
64 // Serve starts a new instance of the Web Server
65 func (s *ServerService) Serve() error {
66         var err error
67
68         // Setup middlewares
69         s.router.Use(gin.Logger())
70         s.router.Use(gin.Recovery())
71         s.router.Use(s.middlewareCORS())
72         s.router.Use(s.middlewareXDSDetails())
73         s.router.Use(s.middlewareCSRF())
74
75         // Sessions manager
76         s.sessions = session.NewClientSessions(s.router, s.log, cookieMaxAge)
77
78         s.router.GET("", s.slashHandler)
79
80         // Create REST API
81         s.api = apiv1.New(s.sessions, s.cfg, s.log, s.router)
82
83         // Websocket routes
84         s.sIOServer, err = socketio.NewServer(nil)
85         if err != nil {
86                 s.log.Fatalln(err)
87         }
88
89         s.router.GET("/socket.io/", s.socketHandler)
90         s.router.POST("/socket.io/", s.socketHandler)
91         /* TODO: do we want to support ws://...  ?
92         s.router.Handle("WS", "/socket.io/", s.socketHandler)
93         s.router.Handle("WSS", "/socket.io/", s.socketHandler)
94         */
95
96         // Serve in the background
97         serveError := make(chan error, 1)
98         go func() {
99                 fmt.Printf("Web Server running on localhost:%s ...\n", s.cfg.HTTPPort)
100                 serveError <- http.ListenAndServe(":"+s.cfg.HTTPPort, s.router)
101         }()
102
103         fmt.Printf("XDS agent running...\n")
104
105         // Wait for stop, restart or error signals
106         select {
107         case <-s.stop:
108                 // Shutting down permanently
109                 s.sessions.Stop()
110                 s.log.Infoln("shutting down (stop)")
111         case err = <-serveError:
112                 // Error due to listen/serve failure
113                 s.log.Errorln(err)
114         }
115
116         return nil
117 }
118
119 // Stop web server
120 func (s *ServerService) Stop() {
121         close(s.stop)
122 }
123
124 // serveSlash provides response to GET "/"
125 func (s *ServerService) slashHandler(c *gin.Context) {
126         c.String(200, "Hello from XDS agent!")
127 }
128
129 // Add details in Header
130 func (s *ServerService) middlewareXDSDetails() gin.HandlerFunc {
131         return func(c *gin.Context) {
132                 c.Header("XDS-Agent-Version", s.cfg.Version)
133                 c.Header("XDS-API-Version", s.cfg.APIVersion)
134                 c.Next()
135         }
136 }
137
138 func (s *ServerService) isValidAPIKey(key string) bool {
139         return (key == s.cfg.FileConf.XDSAPIKey && key != "")
140 }
141
142 func (s *ServerService) middlewareCSRF() gin.HandlerFunc {
143         return func(c *gin.Context) {
144                 // Allow requests carrying a valid API key
145                 if s.isValidAPIKey(c.Request.Header.Get("X-API-Key")) {
146                         // Set the access-control-allow-origin header for CORS requests
147                         // since a valid API key has been provided
148                         c.Header("Access-Control-Allow-Origin", "*")
149                         c.Next()
150                         return
151                 }
152
153                 // Allow io.socket request
154                 if strings.HasPrefix(c.Request.URL.Path, "/socket.io") {
155                         c.Next()
156                         return
157                 }
158
159                 /* FIXME Add really CSRF support
160
161                 // Allow requests for anything not under the protected path prefix,
162                 // and set a CSRF cookie if there isn't already a valid one.
163                 if !strings.HasPrefix(c.Request.URL.Path, prefix) {
164                         cookie, err := c.Cookie("CSRF-Token-" + unique)
165                         if err != nil || !validCsrfToken(cookie.Value) {
166                                 s.log.Debugln("new CSRF cookie in response to request for", c.Request.URL)
167                                 c.SetCookie("CSRF-Token-"+unique, newCsrfToken(), 600, "/", "", false, false)
168                         }
169                         c.Next()
170                         return
171                 }
172
173                 // Verify the CSRF token
174                 token := c.Request.Header.Get("X-CSRF-Token-" + unique)
175                 if !validCsrfToken(token) {
176                         c.AbortWithError(403, "CSRF Error")
177                         return
178                 }
179
180                 c.Next()
181                 */
182                 c.AbortWithError(403, fmt.Errorf("Not valid API key"))
183         }
184 }
185
186 // CORS middleware
187 func (s *ServerService) middlewareCORS() gin.HandlerFunc {
188         return func(c *gin.Context) {
189                 if c.Request.Method == "OPTIONS" {
190                         c.Header("Access-Control-Allow-Origin", "*")
191                         c.Header("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
192                         c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE")
193                         c.Header("Access-Control-Max-Age", cookieMaxAge)
194                         c.AbortWithStatus(204)
195                         return
196                 }
197                 c.Next()
198         }
199 }
200
201 // socketHandler is the handler for the "main" websocket connection
202 func (s *ServerService) socketHandler(c *gin.Context) {
203
204         // Retrieve user session
205         sess := s.sessions.Get(c)
206         if sess == nil {
207                 c.JSON(500, gin.H{"error": "Cannot retrieve session"})
208                 return
209         }
210
211         s.sIOServer.On("connection", func(so socketio.Socket) {
212                 s.log.Debugf("WS Connected (SID=%v)", so.Id())
213                 s.sessions.UpdateIOSocket(sess.ID, &so)
214
215                 so.On("disconnection", func() {
216                         s.log.Debugf("WS disconnected (SID=%v)", so.Id())
217                         s.sessions.UpdateIOSocket(sess.ID, nil)
218                 })
219         })
220
221         s.sIOServer.On("error", func(so socketio.Socket, err error) {
222                 s.log.Errorf("WS SID=%v Error : %v", so.Id(), err.Error())
223         })
224
225         s.sIOServer.ServeHTTP(c.Writer, c.Request)
226 }