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  }