harbor/src/chartserver/reverse_proxy.go

248 lines
7.2 KiB
Go

package chartserver
import (
"bytes"
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/api/event/metadata"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/api"
commonhttp "github.com/goharbor/harbor/src/common/http"
hlog "github.com/goharbor/harbor/src/common/utils/log"
n_event "github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/replication"
rep_event "github.com/goharbor/harbor/src/replication/event"
)
const (
agentHarbor = "HARBOR"
contentLengthHeader = "Content-Length"
)
var (
defaultRepo = "library"
chartRepoAPIPrefix = fmt.Sprintf("/api/%s/chartrepo", api.APIVersion)
rootUploadingEndpoint = fmt.Sprintf("/api/%s/chartrepo/charts", api.APIVersion)
chartRepoHealthEndpoint = fmt.Sprintf("/api/%s/chartrepo/health", api.APIVersion)
chartRepoPrefix = "/chartrepo"
)
// ProxyEngine is used to proxy the related traffics
type ProxyEngine struct {
// The backend target server the traffic will be forwarded to
// Just in case we'll use it
backend *url.URL
// Use go reverse proxy as engine
engine http.Handler
}
// NewProxyEngine is constructor of NewProxyEngine
func NewProxyEngine(target *url.URL, cred *Credential, middlewares ...func(http.Handler) http.Handler) *ProxyEngine {
var engine http.Handler
engine = &httputil.ReverseProxy{
ErrorLog: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile),
Director: func(req *http.Request) {
director(target, cred, req)
},
ModifyResponse: modifyResponse,
Transport: commonhttp.GetHTTPTransport(commonhttp.InternalTransport),
}
if len(middlewares) > 0 {
hlog.Info("New chart server traffic proxy with middlewares")
for i := len(middlewares) - 1; i >= 0; i-- {
engine = middlewares[i](engine)
}
}
return &ProxyEngine{
backend: target,
engine: engine,
}
}
// ServeHTTP serves the incoming http requests
func (pe *ProxyEngine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
pe.engine.ServeHTTP(w, req)
}
// Overwrite the http requests
func director(target *url.URL, cred *Credential, req *http.Request) {
// Closure
targetQuery := target.RawQuery
// Overwrite the request URL to the target path
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
rewriteURLPath(req)
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", agentHarbor)
}
// Add authentication header if it is existing
if cred != nil {
req.SetBasicAuth(cred.Username, cred.Password)
}
}
// Modify the http response
func modifyResponse(res *http.Response) error {
// Upload chart success, then to the notification to replication handler
if res.StatusCode == http.StatusCreated {
// 201 and has chart_upload_event context
// means this response is for uploading chart success.
chartUploadEvent := res.Request.Context().Value(common.ChartUploadCtxKey)
e, ok := chartUploadEvent.(*rep_event.Event)
if !ok {
hlog.Error("failed to convert chart upload context into replication event.")
} else {
// Todo: it used as the replacement of webhook, will be removed when webhook to be introduced.
go func() {
if err := replication.EventHandler.Handle(e); err != nil {
hlog.Errorf("failed to handle event: %v", err)
}
}()
// Trigger harbor webhook
if e != nil && e.Resource != nil && e.Resource.Metadata != nil && len(e.Resource.Metadata.Vtags) > 0 &&
len(e.Resource.ExtendedInfo) > 0 {
event := &n_event.Event{}
metaData := &metadata.ChartUploadMetaData{
ChartMetaData: metadata.ChartMetaData{
ProjectName: e.Resource.ExtendedInfo["projectName"].(string),
ChartName: e.Resource.ExtendedInfo["chartName"].(string),
Versions: e.Resource.Metadata.Vtags,
OccurAt: time.Now(),
Operator: e.Resource.ExtendedInfo["operator"].(string),
},
}
if err := event.Build(metaData); err == nil {
if err := event.Publish(); err != nil {
hlog.Errorf("failed to publish chart upload event: %v", err)
}
} else {
hlog.Errorf("failed to build chart upload event metadata: %v", err)
}
}
}
}
// Process downloading chart success webhook event
if res.StatusCode == http.StatusOK {
chartDownloadEvent := res.Request.Context().Value(common.ChartDownloadCtxKey)
eventMetaData, ok := chartDownloadEvent.(*metadata.ChartDownloadMetaData)
if ok && eventMetaData != nil {
// Trigger harbor webhook
event := &n_event.Event{}
if err := event.Build(eventMetaData); err == nil {
if err := event.Publish(); err != nil {
hlog.Errorf("failed to publish chart download event: %v", err)
}
} else {
hlog.Errorf("failed to build chart download event metadata: %v", err)
}
}
}
// Accept cases
// Success or redirect
if res.StatusCode >= http.StatusOK && res.StatusCode <= http.StatusTemporaryRedirect {
return nil
}
// Detect the 401 code, if it is,overwrite it to 500.
// We also re-write the error content to structural error object
errorObj := make(map[string]string)
if res.StatusCode == http.StatusUnauthorized {
errorObj["error"] = "operation request from unauthorized source is rejected"
res.StatusCode = http.StatusInternalServerError
} else {
// Extract the error and wrap it into the error object
data, err := ioutil.ReadAll(res.Body)
if err != nil {
errorObj["error"] = fmt.Sprintf("%s: %s", res.Status, err.Error())
} else {
if err := json.Unmarshal(data, &errorObj); err != nil {
errorObj["error"] = string(data)
}
}
}
content, err := json.Marshal(errorObj)
if err != nil {
return err
}
size := len(content)
body := ioutil.NopCloser(bytes.NewReader(content))
res.Body = body
res.ContentLength = int64(size)
res.Header.Set(contentLengthHeader, strconv.Itoa(size))
return nil
}
// Join the path
// Copy from the go reverse proxy
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
// Rewrite the incoming URL with the right backend URL pattern
// Remove 'chartrepo' from the endpoints of manipulation API
// Remove 'chartrepo' from the endpoints of repository services
func rewriteURLPath(req *http.Request) {
incomingURLPath := req.URL.Path
// Health check endpoint
if incomingURLPath == chartRepoHealthEndpoint {
req.URL.Path = "/health"
return
}
// Root uploading endpoint
if incomingURLPath == rootUploadingEndpoint {
req.URL.Path = strings.Replace(incomingURLPath, fmt.Sprintf("%s/chartrepo", api.APIVersion), defaultRepo, 1)
return
}
// Repository endpoints
if strings.HasPrefix(incomingURLPath, chartRepoPrefix) {
req.URL.Path = strings.TrimPrefix(incomingURLPath, "/chartrepo")
return
}
// API endpoints
if strings.HasPrefix(incomingURLPath, chartRepoAPIPrefix) {
req.URL.Path = strings.Replace(incomingURLPath, fmt.Sprintf("/%s/chartrepo", api.APIVersion), "", 1)
return
}
}