github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/api/api.go (about)

     1  // Package api implements a JSON-API for interacting with a qri node
     2  package api
     3  
     4  import (
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  
    11  	"github.com/gorilla/mux"
    12  	golog "github.com/ipfs/go-log"
    13  	apiutil "github.com/qri-io/qri/api/util"
    14  	"github.com/qri-io/qri/auth/token"
    15  	"github.com/qri-io/qri/lib"
    16  	qhttp "github.com/qri-io/qri/lib/http"
    17  	"github.com/qri-io/qri/lib/websocket"
    18  	"github.com/qri-io/qri/version"
    19  )
    20  
    21  var (
    22  	log = golog.Logger("qriapi")
    23  	// APIVersion is the version string that is written in API responses
    24  	APIVersion = version.Version
    25  )
    26  
    27  const (
    28  	// DefaultTemplateHash is the hash of the default render template
    29  	DefaultTemplateHash = "/ipfs/QmeqeRTf2Cvkqdx4xUdWi1nJB2TgCyxmemsL3H4f1eTBaw"
    30  	// TemplateUpdateAddress is the URI for the template update
    31  	TemplateUpdateAddress = "/ipns/defaulttmpl.qri.io"
    32  )
    33  
    34  func init() {
    35  	golog.SetLogLevel("qriapi", "info")
    36  }
    37  
    38  // Server wraps a qri p2p node, providing traditional access via http
    39  // Create one with New, start it up with Serve
    40  type Server struct {
    41  	*lib.Instance
    42  	Mux       *mux.Router
    43  	websocket websocket.Handler
    44  }
    45  
    46  // New creates a new qri server from a p2p node & configuration
    47  func New(inst *lib.Instance) Server {
    48  	return Server{
    49  		Instance: inst,
    50  	}
    51  }
    52  
    53  // Serve starts the server. It will block while the server is running
    54  func (s Server) Serve(ctx context.Context) (err error) {
    55  	node := s.Node()
    56  	cfg := s.GetConfig()
    57  
    58  	node.LocalStreams.Print(fmt.Sprintf("qri version v%s\nconnecting...\n", APIVersion))
    59  
    60  	ws, err := websocket.NewHandler(ctx, s.Instance.Bus(), s.Instance.KeyStore())
    61  	if err != nil {
    62  		return err
    63  	}
    64  	s.websocket = ws
    65  	s.Mux = NewServerRoutes(s)
    66  
    67  	p2pConnected := true
    68  	if err := s.Instance.ConnectP2P(ctx); err != nil {
    69  		if !errors.Is(err, lib.ErrP2PDisabled) {
    70  			return err
    71  		}
    72  		p2pConnected = false
    73  	}
    74  
    75  	server := &http.Server{
    76  		Handler: s.Mux,
    77  	}
    78  
    79  	// TODO(ramfox): check config to see if automation is active
    80  	automationRunning := true
    81  	if err := s.Instance.AutomationListen(ctx); err != nil {
    82  		automationRunning = false
    83  		if !errors.Is(lib.ErrAutomationDisabled, err) {
    84  			return err
    85  		}
    86  	}
    87  
    88  	info := "qri is ready.\n"
    89  	if !automationRunning {
    90  		info += "automation is diabled. workflow triggers will not execute\n"
    91  	}
    92  	if !p2pConnected {
    93  		info += "running with no p2p connection\n"
    94  	}
    95  	info += cfg.SummaryString()
    96  	if p2pConnected {
    97  		info += "IPFS Addresses:"
    98  		for _, a := range node.EncapsulatedAddresses() {
    99  			info = fmt.Sprintf("%s\n  %s", info, a.String())
   100  		}
   101  	}
   102  	info += "\n"
   103  
   104  	node.LocalStreams.Print(info)
   105  
   106  	go func() {
   107  		<-ctx.Done()
   108  		log.Info("shutting down")
   109  		server.Close()
   110  	}()
   111  
   112  	// http.ListenAndServe will not return unless there's an error
   113  	return StartServer(cfg.API, server)
   114  }
   115  
   116  // HandleIPFSPath responds to IPFS Hash requests with raw data
   117  func (s *Server) HandleIPFSPath(w http.ResponseWriter, r *http.Request) {
   118  	file, err := s.Node().Repo.Filesystem().Get(r.Context(), r.URL.Path)
   119  	if err != nil {
   120  		apiutil.WriteErrResponse(w, http.StatusInternalServerError, err)
   121  		return
   122  	}
   123  
   124  	io.Copy(w, file)
   125  }
   126  
   127  // helper function
   128  func readOnlyResponse(w http.ResponseWriter, endpoint string) {
   129  	apiutil.WriteErrResponse(w, http.StatusForbidden, fmt.Errorf("qri server is in read-only mode, access to '%s' endpoint is forbidden", endpoint))
   130  }
   131  
   132  // HomeHandler responds with a health check on the empty path, 404 for
   133  // everything else
   134  func (s *Server) HomeHandler(w http.ResponseWriter, r *http.Request) {
   135  	upgrade := r.Header.Get("Upgrade")
   136  	if upgrade == "websocket" {
   137  		s.websocket.ConnectionHandler(w, r)
   138  	} else {
   139  		if r.URL.Path == "" || r.URL.Path == "/" {
   140  			HealthCheckHandler(w, r)
   141  			return
   142  		}
   143  
   144  		apiutil.NotFoundHandler(w, r)
   145  	}
   146  }
   147  
   148  // HealthCheckHandler is a basic ok response for load balancers & co
   149  // returns the version of qri this node is running, pulled from the lib package
   150  func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
   151  	w.WriteHeader(http.StatusOK)
   152  	w.Write([]byte(`{ "meta": { "code": 200, "status": "ok", "version":"` + APIVersion + `" }, "data": [] }`))
   153  }
   154  
   155  // refRouteParams carry a config for a ref based route
   156  type refRouteParams struct {
   157  	Endpoint qhttp.APIEndpoint
   158  	ShortRef bool
   159  	Selector bool
   160  	Methods  []string
   161  }
   162  
   163  // newrefRouteParams is a shorthand to generate refRouteParams
   164  func newrefRouteParams(e qhttp.APIEndpoint, sr bool, sel bool, methods ...string) refRouteParams {
   165  	return refRouteParams{
   166  		Endpoint: e,
   167  		ShortRef: sr,
   168  		Selector: sel,
   169  		Methods:  methods,
   170  	}
   171  }
   172  
   173  func handleRefRoute(m *mux.Router, p refRouteParams, f http.HandlerFunc) {
   174  	routes := []string{
   175  		p.Endpoint.String(),
   176  		fmt.Sprintf("%s/%s", p.Endpoint, "{username}/{name}"),
   177  	}
   178  	if p.Selector {
   179  		routes = append(routes, fmt.Sprintf("%s/%s", p.Endpoint, "{username}/{name}/{selector}"))
   180  	}
   181  	if !p.ShortRef {
   182  		routes = append(routes, fmt.Sprintf("%s/%s", p.Endpoint, "{username}/{name}/at/{fs}/{hash}"))
   183  		if p.Selector {
   184  			routes = append(routes, fmt.Sprintf("%s/%s", p.Endpoint, "{username}/{name}/at/{fs}/{hash}/{selector}"))
   185  		}
   186  	}
   187  
   188  	if p.Methods == nil {
   189  		p.Methods = []string{}
   190  	}
   191  
   192  	for _, route := range routes {
   193  		if len(p.Methods) > 0 {
   194  			hasOptions := false
   195  			for _, o := range p.Methods {
   196  				if o == http.MethodOptions {
   197  					hasOptions = true
   198  					break
   199  				}
   200  			}
   201  			if !hasOptions {
   202  				p.Methods = append(p.Methods, http.MethodOptions)
   203  			}
   204  			// TODO(b5): this is a band-aid that lets us punt on teaching lib about how to
   205  			// switch on HTTP verbs. I think we should use tricks like this that leverage
   206  			// the gorilla/mux package until we get a better sense of how our API uses
   207  			// HTTP verbs
   208  			m.Handle(route, f).Methods(p.Methods...)
   209  		} else {
   210  			m.Handle(route, f)
   211  		}
   212  	}
   213  }
   214  
   215  // NewServerRoutes returns a Muxer that has all API routes
   216  func NewServerRoutes(s Server) *mux.Router {
   217  	cfg := s.GetConfig()
   218  
   219  	m := s.Instance.GiveAPIServer(s.Middleware, []string{})
   220  	m.Use(corsMiddleware(cfg.API.AllowedOrigins))
   221  	m.Use(muxVarsToQueryParamMiddleware)
   222  	m.Use(refStringMiddleware)
   223  	m.Use(token.OAuthTokenMiddleware)
   224  
   225  	var routeParams refRouteParams
   226  
   227  	// misc endpoints
   228  	m.Handle(AEHome.String(), s.NoLogMiddleware(s.HomeHandler))
   229  	m.Handle(AEHealth.String(), s.NoLogMiddleware(HealthCheckHandler))
   230  	m.Handle(AEIPFS.String(), s.Middleware(s.HandleIPFSPath))
   231  	if cfg.API.Webui {
   232  		m.Handle(AEWebUI.String(), s.Middleware(WebuiHandler))
   233  	}
   234  
   235  	// auth endpoints
   236  	m.Handle(AEToken.String(), s.Middleware(TokenHandler(s.Instance))).Methods(http.MethodPost, http.MethodOptions)
   237  
   238  	// non POST/json dataset endpoints
   239  	m.Handle(AEGetCSVFullRef.String(), s.Middleware(GetBodyCSVHandler(s.Instance))).Methods(http.MethodGet)
   240  	m.Handle(AEGetCSVShortRef.String(), s.Middleware(GetBodyCSVHandler(s.Instance))).Methods(http.MethodGet)
   241  	routeParams = newrefRouteParams(qhttp.AEGet, false, true, http.MethodGet)
   242  	handleRefRoute(m, routeParams, s.Middleware(GetHandler(s.Instance, qhttp.AEGet.String())))
   243  	m.Handle(AEUnpack.String(), s.Middleware(UnpackHandler(AEUnpack.NoTrailingSlash())))
   244  	m.Handle(AESaveByUpload.String(), s.Middleware(SaveByUploadHandler(s.Instance, AESaveByUpload.NoTrailingSlash())))
   245  
   246  	// sync/protocol endpoints
   247  	if cfg.RemoteServer != nil && cfg.RemoteServer.Enabled {
   248  		log.Info("running in `remote` mode")
   249  
   250  		m.Handle(qhttp.AERemoteDSync.String(), s.Middleware(s.Instance.RemoteServer().DsyncHTTPHandler()))
   251  		m.Handle(qhttp.AERemoteLogSync.String(), s.Middleware(s.Instance.RemoteServer().LogsyncHTTPHandler()))
   252  		m.Handle(qhttp.AERemoteRefs.String(), s.Middleware(s.Instance.RemoteServer().RefsHTTPHandler()))
   253  	}
   254  
   255  	return m
   256  }