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