package service import ( "context" "crypto/tls" "fmt" "net" "net/http" "time" "mstore/app/handler" "mstore/app/logger" "mstore/app/router" ) const protocol = "tcp" type ServiceParams struct { Handler *handler.Handler X509Cert string X509Key string Portnum int64 Address string } type Service struct { hand *handler.Handler rout *router.Router logg *logger.Logger address string portnum int64 x509cert []byte x509key []byte listen net.Listener hsrv *http.Server } func NewService(params *ServiceParams) (*Service, error) { var err error svc := &Service{ hand: params.Handler, x509cert: []byte(params.X509Cert), x509key: []byte(params.X509Key), portnum: params.Portnum, address: params.Address, } svc.logg = logger.NewLogger("service") return svc, err } func (svc *Service) Build() error { var err error svc.logg.Infof("Service build ") svc.rout = router.NewRouter() svc.rout.Use(router.NewRecoveryMiddleware(svc.logg.Errorf)) svc.rout.Use(router.NewLoggingMiddleware(svc.logg.Infof)) svc.rout.Use(router.NewCorsMiddleware()) svc.rout.Get("/v3/api/service/hello", svc.hand.SendHello) svc.rout.Head("/v3/api/file/{filepath}", svc.hand.FileExists) svc.rout.Put("/v3/api/file/{filepath}", svc.hand.PutFile) svc.rout.Get("/v3/api/file/{filepath}", svc.hand.GetFile) svc.rout.Delete("/v3/api/file/{filepath}", svc.hand.DeleteFile) svc.rout.Get("/v3/api/files/{filepath}", svc.hand.ListFiles) svc.rout.Get("/v2/", svc.hand.GetVersion) // https://github.com/opencontainers/distribution-spec/blob/main/spec.md // // Pulling manifests // // To pull a manifest, perform a GET request to a URL in the following form: // /v2//manifests/ end-3 // // refers to the namespace of the repository. // MUST be either (a) the digest of the manifest or (b) a tag. // The MUST NOT be in any other format. // // Throughout this document, MUST match the following regular expression: // // [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)* // // Throughout this document, as a tag MUST be at most 128 characters // in length and MUST match the following regular expression: // // [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127} // Pushing Manifests // // To push a manifest, perform a PUT request to a path in the following format, // and with the following headers and body: /v2//manifests/ const reference = `{reference:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}}` svc.rout.Head(`/v2/{name}/manifests/{reference}`, svc.hand.ManifestExists) svc.rout.Put(`/v2/{name}/manifests/{reference}`, svc.hand.PutManifest) // Pulling blobs // // To pull a blob, perform a GET request to a URL in the following form: // /v2//blobs/ // // is the namespace of the repository, and is the blob's digest. // // A GET request to an existing blob URL MUST provide the expected blob, // with a response code that MUST be 200 OK. A successful response SHOULD contain // the digest of the uploaded blob in the header Docker-Content-Digest. // // If present, the value of this header MUST be a digest matching that of the // response body. Most clients MAY ignore the value, but if it is used, // the client MUST verify the value matches the returned response body. // Clients SHOULD verify that the response body matches the requested digest. // // If the blob is not found in the repository, the response code MUST be 404 Not Found. svc.rout.Head(`/v2/{name}/blobs/{digest}`, svc.hand.BlobExists) // Single POST // // Registries MAY support pushing blobs using a single POST request. // // To push a blob monolithically by using a single POST request, perform a POST request // to a URL in the following form, and with the following headers and body: // // /v2//blobs/uploads/?digest= // // Content-Length: // Content-Type: application/octet-stream // // // // Here, is the repository's namespace, is the blob's digest, // and is the size (in bytes) of the blob. // // The Content-Length header MUST match the blob's actual content length. // Likewise, the MUST match the blob's digest. // // Registries that do not support single request monolithic uploads SHOULD // return a 202 Accepted status code and Location header and clients SHOULD // proceed with a subsequent PUT request, as described by the POST then PUT upload method. // // Successful completion of the request MUST return a 201 Created and MUST include the following header: // // Location: // // Here, is a pullable blob URL. This location does not necessarily // have to be served by your registry, for example, in the case of a signed URL from // some cloud storage provider that your registry generates. svc.rout.Post(`/v2/{name}/blobs/uploads/`, svc.hand.PostUpload) // Pushing a blob in chunks // // A chunked blob upload is accomplished in three phases: // // - Obtain a session ID (upload URL) (POST) // - Upload the chunks (PATCH) // - Close the session (PUT) // // For information on obtaining a session ID, reference the above section on pushing // a blob monolithically via the POST/PUT method. The process remains unchanged for // chunked upload, except that the post request MUST include the following header: // // Content-Length: 0 // // If the registry has a minimum chunk size, the POST response SHOULD include // the following header, where is the size in bytes // (see the blob PATCH definition for usage): // // OCI-Chunk-Min-Length: // // Please reference the above section for restrictions on the . // // To upload a chunk, issue a PATCH request to a URL path in the following format, // and with the following headers and body: // // URL path: // // Content-Type: application/octet-stream // Content-Range: // Content-Length: // // // // The refers to the URL obtained from the preceding POST request. svc.rout.Patch(`/v2/{name}/blobs/uploads/{reference}`, svc.hand.PatchUpload) // To close the session, issue a PUT request to a url in the following format, // and with the following headers (and optional body, depending on whether or not // the final chunk was uploaded already via a PATCH request): // // ?digest= // // Content-Length: // Content-Range: // Content-Type: application/octet-stream // // OPTIONAL: // // The closing PUT request MUST include the of the whole blob // (not the final chunk) as a query parameter. // // The response to a successful closing of the session MUST be 201 Created, // and MUST contain the following header: // // Location: // // Here, is a pullable blob URL. svc.rout.Put(`/v2/{name}/uploads/{reference}`, svc.hand.PutUpload) // Pulling blobs // // To pull a blob, perform a GET request to a URL in the following form: // // /v2//blobs/ end-2 // // is the namespace of the repository, and is the blob's digest. // // A GET request to an existing blob URL MUST provide the expected blob, // with a response code that MUST be 200 OK. // A successful response SHOULD contain the digest of the uploaded blob in the header Docker-Content-Digest. // If present, the value of this header MUST be a digest matching that of the response body. // Most clients MAY ignore the value, but if it is used, the client MUST verify the value // matches the returned response body. Clients SHOULD verify that the response body // matches the requested digest. // // If the blob is not found in the repository, the response code MUST be 404 Not Found. // // A registry SHOULD support the Range request header in accordance with RFC 9110. svc.rout.Get(`/v2/{name}/blobs/{digest}`, svc.hand.GetBlob) // Deleting Blobs // // To delete a blob, perform a DELETE request to a path in the following format: // // /v2//blobs/ // // is the namespace of the repository, and is the digest // of the blob to be deleted. // // Upon success, the registry MUST respond with code 202 Accepted. // If the blob is not found, a 404 Not Found code MUST be returned. // If blob deletion is disabled, the registry MUST respond with either // a 400 Bad Request or a 405 Method Not Allowed. svc.rout.Delete(`/v2/{name}/blobs/{digest}`, svc.hand.DeleteBlob) svc.rout.NotFound(svc.hand.NotFound) selector := svc.rout.Selector() for _, item := range selector.Routes { svc.logg.Infof("%s\t%s", item.Method, item.RawPath) } const useTLS = true if useTLS { tlsCert, err := tls.X509KeyPair(svc.x509cert, svc.x509key) if err != nil { return err } tlsConfig := tls.Config{ Certificates: []tls.Certificate{tlsCert}, ClientAuth: tls.NoClientCert, InsecureSkipVerify: true, } listenAddress := fmt.Sprintf("%s:%d", svc.address, svc.portnum) svc.listen, err = tls.Listen(protocol, listenAddress, &tlsConfig) if err != nil { return err } } else { listenAddress := fmt.Sprintf("%s:%d", svc.address, svc.portnum) svc.listen, err = net.Listen(protocol, listenAddress) if err != nil { return err } } svc.logg.Infof("Service listening at %v", svc.listen.Addr()) svc.hsrv = &http.Server{ Handler: svc.rout, } return err } func (svc *Service) Run() error { var err error svc.logg.Infof("Service run") err = svc.hsrv.Serve(svc.listen) if err == http.ErrServerClosed { svc.logg.Warningf("Service Closed") err = nil } if err != nil { return err } return err } func (svc *Service) Stop() { if svc.hsrv != nil { svc.logg.Infof("Service stop") downWaiting := 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), downWaiting) defer cancel() err := svc.hsrv.Shutdown(ctx) if err != nil { svc.logg.Errorf("Error service shutdown: %v", err) } } }