Use go module as dependency tool instead of glide
[src/xds/xds-agent.git] / lib / agent / webserver.go
1 /*
2  * Copyright (C) 2017-2018 "IoT.bzh"
3  * Author Sebastien Douheret <sebastien@iot.bzh>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package agent
19
20 import (
21         "fmt"
22         "log"
23         "net/http"
24         "os"
25         "path"
26
27         socketio "github.com/googollee/go-socket.io"
28
29         "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
30         "github.com/Sirupsen/logrus"
31         "github.com/gin-contrib/static"
32         "github.com/gin-gonic/gin"
33 )
34
35 // WebServer .
36 type WebServer struct {
37         *Context
38         router    *gin.Engine
39         api       *APIService
40         sIOServer *socketio.Server
41         webApp    *gin.RouterGroup
42         stop      chan struct{} // signals intentional stop
43 }
44
45 const indexFilename = "index.html"
46
47 // NewWebServer creates an instance of WebServer
48 func NewWebServer(ctx *Context) *WebServer {
49
50         // Setup logging for gin router
51         if ctx.Log.Level == logrus.DebugLevel {
52                 gin.SetMode(gin.DebugMode)
53         } else {
54                 gin.SetMode(gin.ReleaseMode)
55         }
56
57         // Redirect gin logs into another logger (LogVerboseOut may be stderr or a file)
58         gin.DefaultWriter = ctx.Config.LogVerboseOut
59         gin.DefaultErrorWriter = ctx.Config.LogVerboseOut
60         log.SetOutput(ctx.Config.LogVerboseOut)
61
62         // Creates gin router
63         r := gin.New()
64
65         svr := &WebServer{
66                 Context:   ctx,
67                 router:    r,
68                 api:       nil,
69                 sIOServer: nil,
70                 webApp:    nil,
71                 stop:      make(chan struct{}),
72         }
73
74         return svr
75 }
76
77 // Serve starts a new instance of the Web Server
78 func (s *WebServer) Serve() error {
79         var err error
80
81         // Setup middlewares
82         s.router.Use(gin.Logger())
83         s.router.Use(gin.Recovery())
84         s.router.Use(s.middlewareCORS())
85         s.router.Use(s.middlewareXDSDetails())
86         s.router.Use(s.middlewareCSRF())
87
88         // Create REST API
89         s.api = NewAPIV1(s.Context)
90
91         // Create connections to XDS Servers
92         // XXX - not sure there is no side effect to do it in background !
93         go func() {
94                 for _, svrCfg := range s.Config.FileConf.ServersConf {
95                         if svr, err := s.api.AddXdsServer(svrCfg); err != nil {
96                                 // Just log error, don't consider as critical
97                                 s.Log.Errorf("Cannot connect to XDS Server url=%s: %v", svr.BaseURL, err.Error())
98                         }
99                 }
100         }()
101
102         // Websocket routes
103         s.sIOServer, err = socketio.NewServer(nil)
104         if err != nil {
105                 s.Log.Fatalln(err)
106         }
107
108         s.router.GET("/socket.io/", s.socketHandler)
109         s.router.POST("/socket.io/", s.socketHandler)
110         /* TODO: do we want to support ws://...  ?
111         s.router.Handle("WS", "/socket.io/", s.socketHandler)
112         s.router.Handle("WSS", "/socket.io/", s.socketHandler)
113         */
114
115         // Web Application (serve on / )
116         idxFile := path.Join(s.Config.FileConf.WebAppDir, indexFilename)
117         if _, err := os.Stat(idxFile); err != nil {
118                 s.Log.Fatalln("Web app directory not found, check/use webAppDir setting in config file: ", idxFile)
119         }
120         s.Log.Infof("Serve WEB app dir: %s", s.Config.FileConf.WebAppDir)
121         s.router.Use(static.Serve("/", static.LocalFile(s.Config.FileConf.WebAppDir, true)))
122         s.webApp = s.router.Group("/", s.serveIndexFile)
123         {
124                 s.webApp.GET("/")
125         }
126
127         // Serve in the background
128         serveError := make(chan error, 1)
129         go func() {
130                 fmt.Printf("Web Server running on localhost:%s ...\n", s.Config.FileConf.HTTPPort)
131                 serveError <- http.ListenAndServe(":"+s.Config.FileConf.HTTPPort, s.router)
132         }()
133
134         fmt.Printf("XDS agent running...\n")
135
136         // Wait for stop, restart or error signals
137         select {
138         case <-s.stop:
139                 // Shutting down permanently
140                 s.sessions.Stop()
141                 s.Log.Infoln("shutting down (stop)")
142         case err = <-serveError:
143                 // Error due to listen/serve failure
144                 s.Log.Errorln(err)
145         }
146
147         return nil
148 }
149
150 // Stop web server
151 func (s *WebServer) Stop() {
152         s.api.Stop()
153         close(s.stop)
154 }
155
156 // serveIndexFile provides initial file (eg. index.html) of webapp
157 func (s *WebServer) serveIndexFile(c *gin.Context) {
158         c.HTML(200, indexFilename, gin.H{})
159 }
160
161 // Add details in Header
162 func (s *WebServer) middlewareXDSDetails() gin.HandlerFunc {
163         return func(c *gin.Context) {
164                 c.Header("XDS-Agent-Version", s.Config.Version)
165                 c.Header("XDS-API-Version", s.Config.APIVersion)
166                 c.Next()
167         }
168 }
169
170 func (s *WebServer) isValidAPIKey(key string) bool {
171         return (s.Config.FileConf.XDSAPIKey != "" && key == s.Config.FileConf.XDSAPIKey)
172 }
173
174 func (s *WebServer) middlewareCSRF() gin.HandlerFunc {
175         return func(c *gin.Context) {
176                 // XXX - not used for now
177                 c.Next()
178                 return
179                 /*
180                         // Allow requests carrying a valid API key
181                         if s.isValidAPIKey(c.Request.Header.Get("X-API-Key")) {
182                                 // Set the access-control-allow-origin header for CORS requests
183                                 // since a valid API key has been provided
184                                 c.Header("Access-Control-Allow-Origin", "*")
185                                 c.Next()
186                                 return
187                         }
188
189                         // Allow io.socket request
190                         if strings.HasPrefix(c.Request.URL.Path, "/socket.io") {
191                                 c.Next()
192                                 return
193                         }
194
195                         // FIXME Add really CSRF support
196
197                         // Allow requests for anything not under the protected path prefix,
198                         // and set a CSRF cookie if there isn't already a valid one.
199                         //if !strings.HasPrefix(c.Request.URL.Path, prefix) {
200                         //      cookie, err := c.Cookie("CSRF-Token-" + unique)
201                         //      if err != nil || !validCsrfToken(cookie.Value) {
202                         //              s.Log.Debugln("new CSRF cookie in response to request for", c.Request.URL)
203                         //              c.SetCookie("CSRF-Token-"+unique, newCsrfToken(), 600, "/", "", false, false)
204                         //      }
205                         //      c.Next()
206                         //      return
207                         //}
208
209                         // Verify the CSRF token
210                         //token := c.Request.Header.Get("X-CSRF-Token-" + unique)
211                         //if !validCsrfToken(token) {
212                         //      c.AbortWithError(403, "CSRF Error")
213                         //      return
214                         //}
215
216                         //c.Next()
217
218                         c.AbortWithError(403, fmt.Errorf("Not valid API key"))
219                 */
220         }
221 }
222
223 // CORS middleware
224 func (s *WebServer) middlewareCORS() gin.HandlerFunc {
225         return func(c *gin.Context) {
226                 if c.Request.Method == "OPTIONS" {
227                         c.Header("Access-Control-Allow-Origin", "*")
228                         c.Header("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
229                         c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE")
230                         c.Header("Access-Control-Max-Age", cookieMaxAge)
231                         c.AbortWithStatus(204)
232                         return
233                 }
234                 c.Next()
235         }
236 }
237
238 // socketHandler is the handler for the "main" websocket connection
239 func (s *WebServer) socketHandler(c *gin.Context) {
240
241         // Retrieve user session
242         sess := s.sessions.Get(c)
243         if sess == nil {
244                 c.JSON(500, gin.H{"error": "Cannot retrieve session"})
245                 return
246         }
247
248         s.sIOServer.On("connection", func(so socketio.Socket) {
249                 sessID := sess.ID
250
251                 s.Log.Debugf("WS Connected (sessID=%s, WS SID=%s)", sessID, so.Id())
252                 s.sessions.UpdateIOSocket(sessID, &so)
253
254                 so.On("disconnection", func() {
255                         s.Log.Debugf("WS disconnected (sessID=%s, WS SID=%s)", sessID, so.Id())
256                         s.events.UnRegister(xaapiv1.EVTAll, sessID)
257                         s.sessions.UpdateIOSocket(sessID, nil)
258                 })
259         })
260
261         s.sIOServer.On("error", func(so socketio.Socket, err error) {
262                 s.Log.Errorf("WS SID=%v Error : %v", so.Id(), err.Error())
263         })
264
265         s.sIOServer.ServeHTTP(c.Writer, c.Request)
266 }