package internal import ( "context" "fmt" "io" "net/http" "strconv" "strings" "time" "cloud.google.com/go/storage" "github.com/go-logr/logr" ) const ( ADDR = "addr" BYTES = "bytes" METHOD = "method" RANGE = "range" STATUS = "status" URL = "url" ) type BucketServer struct { ctx context.Context router *http.ServeMux bucketHandle *storage.BucketHandle logger logr.Logger } func NewBucketServer(ctx context.Context, client *storage.Client, bucket string, logger logr.Logger) (*BucketServer, error) { bucketHandle := client.Bucket(bucket) _, err := bucketHandle.Attrs(ctx) if err != nil { return nil, fmt.Errorf("connection to the bucket %s could not be established", bucket) } logger.Info("bucket connection successful", "bucket", bucket) bucketServer := &BucketServer{ ctx: ctx, bucketHandle: bucketHandle, logger: logger, } router := http.NewServeMux() router.HandleFunc("/{object...}", bucketServer.getObject) router.HandleFunc("/health", bucketServer.health) return &BucketServer{ ctx: ctx, router: router, bucketHandle: bucketHandle, logger: logger, }, nil } func (bs *BucketServer) Run(addr string) error { server := &http.Server{ Addr: addr, Handler: bs.router, ReadTimeout: time.Second * 10, } bs.logger.Info("[apk-mirror] started", "bind", addr) return server.ListenAndServe() } func (bs *BucketServer) handleError(w http.ResponseWriter, r *http.Request, err error) { switch err { case storage.ErrObjectNotExist: http.Error(w, err.Error(), http.StatusNotFound) bs.logger.Error(err, "object does not exist", STATUS, strconv.Itoa(http.StatusNotFound), ADDR, getEntityAddr(r), METHOD, r.Method, URL, r.URL.String(), ) default: http.Error(w, err.Error(), http.StatusInternalServerError) bs.logger.Error(err, "error in proxy", STATUS, strconv.Itoa(http.StatusInternalServerError), ADDR, getEntityAddr(r), METHOD, r.Method, URL, r.URL.String(), ) } } func (bs *BucketServer) getObjReader(w http.ResponseWriter, r *http.Request, obj *storage.ObjectHandle) (*storage.Reader, error) { var objr *storage.Reader ctx := r.Context() attrs, err := obj.Attrs(ctx) if err != nil { return nil, err } setStrHeader(w, "Content-Type", attrs.ContentType) setStrHeader(w, "Content-Language", attrs.ContentLanguage) setStrHeader(w, "Cache-Control", attrs.CacheControl) setStrHeader(w, "Content-Encoding", attrs.ContentEncoding) setStrHeader(w, "Content-Disposition", attrs.ContentDisposition) if r.Header.Get("Range") != "" { //nolint byteRange := strings.Split(r.Header.Values("Range")[0], "=")[1] rangeStart, err := strconv.Atoi(strings.Split(byteRange, "-")[0]) if err != nil { return nil, err } rangeEnd, err := strconv.Atoi(strings.Split(byteRange, "-")[1]) if err != nil { return nil, err } rangeLength := rangeEnd - rangeStart + 1 objr, err = obj.NewRangeReader(ctx, int64(rangeStart), int64(rangeLength)) if err != nil { return nil, err } } else { objr, err = obj.NewReader(ctx) if err != nil { return nil, err } } return objr, nil } func (bs *BucketServer) health(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) } func (bs *BucketServer) getObject(w http.ResponseWriter, r *http.Request) { objName := r.PathValue("object") obj := bs.bucketHandle.Object(objName) objr, err := bs.getObjReader(w, r, obj) if err != nil { bs.handleError(w, r, err) return } bytesWritten, err := io.Copy(w, objr) objr.Close() setIntHeader(w, "Content-Length", bytesWritten) if err != nil { bs.handleError(w, r, err) return } logPairs := []any{ STATUS, strconv.Itoa(http.StatusOK), ADDR, getEntityAddr(r), METHOD, r.Method, URL, r.URL.String(), } if rangeVal := r.Header.Get("Range"); rangeVal != "" { logPairs = append( logPairs, RANGE, rangeVal, BYTES, strconv.FormatInt(bytesWritten, 10), ) } bs.logger.Info(fmt.Sprintf("got object %s", objName), logPairs..., ) } func setStrHeader(w http.ResponseWriter, key string, value string) { if value != "" { w.Header().Add(key, value) } } func setIntHeader(w http.ResponseWriter, key string, value int64) { if value > 0 { w.Header().Add(key, strconv.FormatInt(value, 10)) } } func getEntityAddr(r *http.Request) string { if xForwardedFor := r.Header.Get("X-Forwarded-For"); xForwardedFor != "" { return xForwardedFor } return r.RemoteAddr }