github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/internal/apiserver/server.go (about) 1 // Copyright © 2021 Kaleido, Inc. 2 // 3 // SPDX-License-Identifier: Apache-2.0 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 package apiserver 18 19 import ( 20 "context" 21 "crypto/tls" 22 "crypto/x509" 23 "encoding/json" 24 "fmt" 25 "io/ioutil" 26 "mime/multipart" 27 "net" 28 "net/http" 29 "reflect" 30 "regexp" 31 "strings" 32 "time" 33 34 "github.com/ghodss/yaml" 35 "github.com/gorilla/mux" 36 "github.com/kaleido-io/firefly/internal/config" 37 "github.com/kaleido-io/firefly/internal/events/eifactory" 38 "github.com/kaleido-io/firefly/internal/events/websockets" 39 "github.com/kaleido-io/firefly/internal/i18n" 40 "github.com/kaleido-io/firefly/internal/log" 41 "github.com/kaleido-io/firefly/internal/oapispec" 42 "github.com/kaleido-io/firefly/internal/orchestrator" 43 "github.com/kaleido-io/firefly/pkg/database" 44 "github.com/kaleido-io/firefly/pkg/fftypes" 45 ) 46 47 var ffcodeExtractor = regexp.MustCompile(`^(FF\d+):`) 48 49 // Serve is the main entry point for the API Server 50 func Serve(ctx context.Context, o orchestrator.Orchestrator) error { 51 r := createMuxRouter(o) 52 l, err := createListener(ctx) 53 if err == nil { 54 var s *http.Server 55 s, err = createServer(ctx, r) 56 if err == nil { 57 err = serveHTTP(ctx, l, s) 58 } 59 } 60 return err 61 } 62 63 func createListener(ctx context.Context) (net.Listener, error) { 64 listenAddr := fmt.Sprintf("%s:%d", config.GetString(config.HTTPAddress), config.GetUint(config.HTTPPort)) 65 listener, err := net.Listen("tcp", listenAddr) 66 if err != nil { 67 return nil, i18n.WrapError(ctx, err, i18n.MsgAPIServerStartFailed, listenAddr) 68 } 69 log.L(ctx).Infof("Listening on HTTP %s", listener.Addr()) 70 return listener, err 71 } 72 73 func createServer(ctx context.Context, r *mux.Router) (srv *http.Server, err error) { 74 75 defaultFilterLimit = uint64(config.GetUint(config.APIDefaultFilterLimit)) 76 maxFilterLimit = uint64(config.GetUint(config.APIMaxFilterLimit)) 77 maxFilterSkip = uint64(config.GetUint(config.APIMaxFilterSkip)) 78 79 // Support client auth 80 clientAuth := tls.NoClientCert 81 if config.GetBool(config.HTTPTLSClientAuth) { 82 clientAuth = tls.RequireAndVerifyClientCert 83 } 84 85 // Support custom CA file 86 var rootCAs *x509.CertPool 87 caFile := config.GetString(config.HTTPTLSCAFile) 88 if caFile != "" { 89 rootCAs = x509.NewCertPool() 90 var caBytes []byte 91 caBytes, err = ioutil.ReadFile(caFile) 92 if err == nil { 93 ok := rootCAs.AppendCertsFromPEM(caBytes) 94 if !ok { 95 err = i18n.NewError(ctx, i18n.MsgInvalidCAFile) 96 } 97 } 98 } else { 99 rootCAs, err = x509.SystemCertPool() 100 } 101 102 if err != nil { 103 return nil, i18n.WrapError(ctx, err, i18n.MsgTLSConfigFailed) 104 } 105 106 srv = &http.Server{ 107 Handler: wrapCorsIfEnabled(ctx, r), 108 WriteTimeout: config.GetDuration(config.HTTPWriteTimeout), 109 ReadTimeout: config.GetDuration(config.HTTPReadTimeout), 110 TLSConfig: &tls.Config{ 111 MinVersion: tls.VersionTLS12, 112 ClientAuth: clientAuth, 113 ClientCAs: rootCAs, 114 RootCAs: rootCAs, 115 VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 116 cert := verifiedChains[0][0] 117 log.L(ctx).Debugf("Client certificate provided Subject=%s Issuer=%s Expiry=%s", cert.Subject, cert.Issuer, cert.NotAfter) 118 return nil 119 }, 120 }, 121 ConnContext: func(newCtx context.Context, c net.Conn) context.Context { 122 l := log.L(ctx).WithField("req", fftypes.ShortID()) 123 newCtx = log.WithLogger(newCtx, l) 124 l.Debugf("New HTTP connection: remote=%s local=%s", c.RemoteAddr().String(), c.LocalAddr().String()) 125 return newCtx 126 }, 127 } 128 return srv, nil 129 } 130 131 func serveHTTP(ctx context.Context, listener net.Listener, srv *http.Server) (err error) { 132 serverEnded := make(chan struct{}) 133 go func() { 134 select { 135 case <-ctx.Done(): 136 log.L(ctx).Infof("API server context cancelled - shutting down") 137 srv.Close() 138 case <-serverEnded: 139 return 140 } 141 }() 142 143 if config.GetBool(config.HTTPTLSEnabled) { 144 err = srv.ServeTLS(listener, config.GetString(config.HTTPTLSCertFile), config.GetString(config.HTTPTLSKeyFile)) 145 } else { 146 err = srv.Serve(listener) 147 } 148 if err == http.ErrServerClosed { 149 err = nil 150 } 151 close(serverEnded) 152 log.L(ctx).Infof("API server complete") 153 154 return err 155 } 156 157 func getFirstFilePart(req *http.Request) (*multipart.Part, error) { 158 159 ctx := req.Context() 160 l := log.L(ctx) 161 mpr, err := req.MultipartReader() 162 if err != nil { 163 return nil, i18n.WrapError(ctx, err, i18n.MsgMultiPartFormReadError) 164 } 165 for { 166 part, err := mpr.NextPart() 167 if err != nil { 168 return nil, i18n.WrapError(ctx, err, i18n.MsgMultiPartFormReadError) 169 } 170 if part.FileName() == "" { 171 l.Debugf("Ignoring form field in multi-part upload: %s", part.FormName()) 172 } else { 173 l.Debugf("Processing multi-part upload. Field='%s' Filename='%s'", part.FormName(), part.FileName()) 174 return part, nil 175 } 176 } 177 } 178 179 func routeHandler(o orchestrator.Orchestrator, route *oapispec.Route) http.HandlerFunc { 180 // Check the mandatory parts are ok at startup time 181 return apiWrapper(func(res http.ResponseWriter, req *http.Request) (int, error) { 182 183 var jsonInput interface{} 184 if route.JSONInputValue != nil { 185 jsonInput = route.JSONInputValue() 186 } 187 var part *multipart.Part 188 contentType := req.Header.Get("Content-Type") 189 var err error 190 if req.Method != http.MethodGet && req.Method != http.MethodDelete { 191 switch { 192 case strings.HasPrefix(strings.ToLower(contentType), "multipart/form-data") && route.FormUploadHandler != nil: 193 part, err = getFirstFilePart(req) 194 if err != nil { 195 return 400, err 196 } 197 defer part.Close() 198 case strings.HasPrefix(strings.ToLower(contentType), "application/json"): 199 if jsonInput != nil { 200 err = json.NewDecoder(req.Body).Decode(&jsonInput) 201 } 202 default: 203 return 415, i18n.NewError(req.Context(), i18n.MsgInvalidContentType) 204 } 205 } 206 207 queryParams := make(map[string]string) 208 pathParams := make(map[string]string) 209 var filter database.AndFilter 210 var status = 400 // if fail parsing input 211 var output interface{} 212 if err == nil { 213 if len(route.PathParams) > 0 { 214 v := mux.Vars(req) 215 for _, pp := range route.PathParams { 216 pathParams[pp.Name] = v[pp.Name] 217 } 218 } 219 for _, qp := range route.QueryParams { 220 val, exists := req.URL.Query()[qp.Name] 221 if qp.IsBool { 222 if exists && (len(val) == 0 || val[0] == "" || strings.EqualFold(val[0], "true")) { 223 val = []string{"true"} 224 } else { 225 val = []string{"false"} 226 } 227 } 228 if exists && len(val) > 0 { 229 queryParams[qp.Name] = val[0] 230 } 231 } 232 if route.FilterFactory != nil { 233 filter, err = buildFilter(req, route.FilterFactory) 234 } 235 } 236 237 if err == nil { 238 status = route.JSONOutputCode 239 req := oapispec.APIRequest{ 240 Ctx: req.Context(), 241 Or: o, 242 Req: req, 243 PP: pathParams, 244 QP: queryParams, 245 Filter: filter, 246 Input: jsonInput, 247 FReader: part, 248 } 249 if part != nil { 250 output, err = route.FormUploadHandler(req) 251 } else { 252 output, err = route.JSONHandler(req) 253 } 254 } 255 if err == nil { 256 isNil := output == nil || reflect.ValueOf(output).IsNil() 257 if isNil && status != 204 { 258 err = i18n.NewError(req.Context(), i18n.Msg404NoResult) 259 status = 404 260 } 261 res.Header().Add("Content-Type", "application/json") 262 res.WriteHeader(status) 263 if !isNil { 264 err = json.NewEncoder(res).Encode(output) 265 if err != nil { 266 err = i18n.WrapError(req.Context(), err, i18n.MsgResponseMarshalError) 267 log.L(req.Context()).Errorf(err.Error()) 268 } 269 } 270 } 271 return status, err 272 }) 273 } 274 275 func apiWrapper(handler func(res http.ResponseWriter, req *http.Request) (status int, err error)) http.HandlerFunc { 276 apiTimeout := config.GetDuration(config.APIRequestTimeout) // Query once at startup when wrapping 277 return func(res http.ResponseWriter, req *http.Request) { 278 279 // Configure a server-side timeout on each request, to try and avoid cases where the API requester 280 // times out, and we continue to churn indefinitely processing the request. 281 // Long-running processes should be dispatched asynchronously (API returns 202 Accepted asap), 282 // and the caller can either listen on the websocket for updates, or poll the status of the affected object. 283 // This is dependent on the context being passed down through to all blocking operations down the stack 284 // (while avoiding passing the context to asynchronous tasks that are dispatched as a result of the request) 285 ctx, cancel := context.WithTimeout(req.Context(), apiTimeout) 286 req = req.WithContext(ctx) 287 defer cancel() 288 289 // Wrap the request itself in a log wrapper, that gives minimal request/response and timing info 290 l := log.L(ctx) 291 l.Infof("--> %s %s", req.Method, req.URL.Path) 292 startTime := time.Now() 293 status, err := handler(res, req) 294 durationMS := float64(time.Since(startTime)) / float64(time.Millisecond) 295 if err != nil { 296 // Routers don't need to tweak the status code when sending errors. 297 // .. either the FF12345 error they raise is mapped to a status hint 298 ffcodeExtract := ffcodeExtractor.FindStringSubmatch(err.Error()) 299 if len(ffcodeExtract) >= 2 { 300 if statusHint, ok := i18n.GetStatusHint(ffcodeExtract[1]); ok { 301 status = statusHint 302 } 303 } 304 // ... or we default to 500 305 if status < 300 { 306 status = 500 307 } 308 l.Infof("<-- %s %s [%d] (%.2fms): %s", req.Method, req.URL.Path, status, durationMS, err) 309 res.Header().Add("Content-Type", "application/json") 310 res.WriteHeader(status) 311 _ = json.NewEncoder(res).Encode(&fftypes.RESTError{ 312 Error: err.Error(), 313 }) 314 } else { 315 l.Infof("<-- %s %s [%d] (%.2fms)", req.Method, req.URL.Path, status, durationMS) 316 } 317 } 318 } 319 320 func notFoundHandler(res http.ResponseWriter, req *http.Request) (status int, err error) { 321 res.Header().Add("Content-Type", "application/json") 322 return 404, i18n.NewError(req.Context(), i18n.Msg404NotFound) 323 } 324 325 func swaggerUIHandler(res http.ResponseWriter, req *http.Request) (status int, err error) { 326 res.Header().Add("Content-Type", "text/html") 327 _, _ = res.Write(oapispec.SwaggerUIHTML(req.Context())) 328 return 200, nil 329 } 330 331 func swaggerHandler(res http.ResponseWriter, req *http.Request) (status int, err error) { 332 vars := mux.Vars(req) 333 if vars["ext"] == ".json" { 334 res.Header().Add("Content-Type", "application/json") 335 doc := oapispec.SwaggerGen(req.Context(), routes) 336 b, _ := json.Marshal(&doc) 337 _, _ = res.Write(b) 338 } else { 339 res.Header().Add("Content-Type", "application/x-yaml") 340 doc := oapispec.SwaggerGen(req.Context(), routes) 341 b, _ := yaml.Marshal(&doc) 342 _, _ = res.Write(b) 343 } 344 return 200, nil 345 } 346 347 func createMuxRouter(o orchestrator.Orchestrator) *mux.Router { 348 r := mux.NewRouter() 349 for _, route := range routes { 350 if route.JSONHandler != nil { 351 r.HandleFunc(fmt.Sprintf("/api/v1/%s", route.Path), routeHandler(o, route)). 352 Methods(route.Method) 353 } 354 } 355 ws, _ := eifactory.GetPlugin(context.TODO(), "websockets") 356 r.HandleFunc(`/api/swagger{ext:\.yaml|\.json|}`, apiWrapper(swaggerHandler)) 357 r.HandleFunc(`/api`, apiWrapper(swaggerUIHandler)) 358 r.HandleFunc(`/favicon{any:.*}.png`, favIcons) 359 360 r.HandleFunc(`/ws`, ws.(*websockets.WebSockets).ServeHTTP) 361 362 uiPath := config.GetString(config.UIPath) 363 if uiPath != "" { 364 r.PathPrefix(`/ui`).Handler(newStaticHandler(uiPath, "index.html", `/ui`)) 365 } 366 367 r.NotFoundHandler = apiWrapper(notFoundHandler) 368 return r 369 }