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 }