379 lines
13 KiB
Go
379 lines
13 KiB
Go
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/<name>/manifests/<reference> end-3
|
|
//
|
|
// <name> refers to the namespace of the repository.
|
|
// <reference> MUST be either (a) the digest of the manifest or (b) a tag.
|
|
// The <reference> MUST NOT be in any other format.
|
|
//
|
|
// Throughout this document, <name> MUST match the following regular expression:
|
|
//
|
|
// [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*
|
|
//
|
|
// Throughout this document, <reference> 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/<name>/manifests/<reference>
|
|
|
|
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 manifests
|
|
//
|
|
// To pull a manifest, perform a GET request to a URL in the following form:
|
|
// /v2/<name>/manifests/<reference> end-3
|
|
//
|
|
// <name> refers to the namespace of the repository. <reference> MUST be either
|
|
// (a) the digest of the manifest or (b) a tag. The <reference> MUST NOT be in
|
|
// any other format. Throughout this document, <name> MUST match the following
|
|
// regular expression:
|
|
//
|
|
// [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*
|
|
//
|
|
// Implementers note: Many clients impose a limit of 255 characters on the length of
|
|
// the concatenation of the registry hostname (and optional port), /, and <name> value.
|
|
// If the registry name is registry.example.org:5000, those clients would be limited
|
|
// to a <name> of 229 characters (255 minus 25 for the registry hostname and port
|
|
// and minus 1 for a / separator). For compatibility with those clients, registries should
|
|
// avoid values of <name> that would cause this limit to be exceeded.
|
|
//
|
|
// Throughout this document, <reference> 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}
|
|
//
|
|
// The client SHOULD include an Accept header indicating which manifest content types
|
|
// it supports. In a successful response, the Content-Type header will indicate
|
|
// the type of the returned manifest. The registry SHOULD NOT include parameters
|
|
// on the Content-Type header. The client SHOULD ignore parameters on the Content-Type
|
|
// header.
|
|
// The Content-Type header SHOULD match what the client pushed as the manifest's
|
|
// Content-Type. If the manifest has a mediaType field, clients SHOULD reject
|
|
// unless the mediaType field's value matches the type specified by the Content-Type header.
|
|
//
|
|
// For more information on the use of Accept headers and content negotiation,
|
|
// please see Content Negotiation and RFC7231.
|
|
//
|
|
// A GET request to an existing manifest URL MUST provide the expected manifest,
|
|
// 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.
|
|
//
|
|
// The Docker-Content-Digest header, if present on the response,
|
|
// returns the canonical digest of the uploaded blob which MAY differ
|
|
// from the provided digest. If the digest does differ,
|
|
// it MAY be the case that the hashing algorithms used do not match.
|
|
//
|
|
// See Content Digests apdx-3 for information on how to detect the hashing
|
|
// algorithm in use. Most clients MAY ignore the value, but if it is used,
|
|
// the client MUST verify the value matches the returned manifest.
|
|
//
|
|
// If the <reference> part of a manifest request is a digest, clients SHOULD verify
|
|
// the returned manifest matches this digest.
|
|
//
|
|
// If the manifest is not found in the repository, the response code
|
|
// MUST be 404 Not Found.
|
|
|
|
svc.rout.Get(`/v2/{name}/manifests/{reference}`, svc.hand.GetManifest)
|
|
|
|
// Pulling blobs
|
|
//
|
|
// To pull a blob, perform a GET request to a URL in the following form:
|
|
// /v2/<name>/blobs/<digest>
|
|
//
|
|
// <name> is the namespace of the repository, and <digest> 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/<name>/blobs/uploads/?digest=<digest>
|
|
//
|
|
// Content-Length: <length>
|
|
// Content-Type: application/octet-stream
|
|
//
|
|
// <upload byte stream>
|
|
//
|
|
// Here, <name> is the repository's namespace, <digest> is the blob's digest,
|
|
// and <length> is the size (in bytes) of the blob.
|
|
//
|
|
// The Content-Length header MUST match the blob's actual content length.
|
|
// Likewise, the <digest> 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: <blob-location>
|
|
//
|
|
// Here, <blob-location> 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 <size> is the size in bytes
|
|
// (see the blob PATCH definition for usage):
|
|
//
|
|
// OCI-Chunk-Min-Length: <size>
|
|
//
|
|
// Please reference the above section for restrictions on the <location>.
|
|
//
|
|
// 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: <location>
|
|
//
|
|
// Content-Type: application/octet-stream
|
|
// Content-Range: <range>
|
|
// Content-Length: <length>
|
|
//
|
|
// <upload byte stream of chunk>
|
|
//
|
|
// The <location> 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):
|
|
//
|
|
// <location>?digest=<digest>
|
|
//
|
|
// Content-Length: <length of chunk, if present>
|
|
// Content-Range: <range of chunk, if present>
|
|
// Content-Type: application/octet-stream <if chunk provided>
|
|
//
|
|
// OPTIONAL: <final chunk byte stream>
|
|
//
|
|
// The closing PUT request MUST include the <digest> 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: <blob-location>
|
|
//
|
|
// Here, <blob-location> 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/<name>/blobs/<digest> end-2
|
|
//
|
|
// <name> is the namespace of the repository, and <digest> 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/<name>/blobs/<digest>
|
|
//
|
|
// <name> is the namespace of the repository, and <digest> 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)
|
|
}
|
|
}
|
|
}
|