2018-07-03 15:36:20 +02:00
|
|
|
package chartserver
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2018-07-19 17:50:25 +02:00
|
|
|
"fmt"
|
2019-04-15 04:44:56 +02:00
|
|
|
"github.com/goharbor/harbor/src/common"
|
|
|
|
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
|
|
|
"github.com/goharbor/harbor/src/replication"
|
|
|
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
|
|
|
"github.com/goharbor/harbor/src/replication/model"
|
2018-07-03 15:36:20 +02:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httputil"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
agentHarbor = "HARBOR"
|
|
|
|
contentLengthHeader = "Content-Length"
|
2018-09-19 08:45:52 +02:00
|
|
|
|
|
|
|
defaultRepo = "library"
|
|
|
|
rootUploadingEndpoint = "/api/chartrepo/charts"
|
|
|
|
rootIndexEndpoint = "/chartrepo/index.yaml"
|
|
|
|
chartRepoHealthEndpoint = "/api/chartrepo/health"
|
2018-07-03 15:36:20 +02:00
|
|
|
)
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// ProxyEngine is used to proxy the related traffics
|
2018-07-03 15:36:20 +02:00
|
|
|
type ProxyEngine struct {
|
2018-09-05 10:16:31 +02:00
|
|
|
// The backend target server the traffic will be forwarded to
|
|
|
|
// Just in case we'll use it
|
2018-07-03 15:36:20 +02:00
|
|
|
backend *url.URL
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Use go reverse proxy as engine
|
2018-07-03 15:36:20 +02:00
|
|
|
engine *httputil.ReverseProxy
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// NewProxyEngine is constructor of NewProxyEngine
|
2018-07-11 11:31:34 +02:00
|
|
|
func NewProxyEngine(target *url.URL, cred *Credential) *ProxyEngine {
|
2018-07-03 15:36:20 +02:00
|
|
|
return &ProxyEngine{
|
|
|
|
backend: target,
|
|
|
|
engine: &httputil.ReverseProxy{
|
|
|
|
ErrorLog: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile),
|
|
|
|
Director: func(req *http.Request) {
|
2018-07-11 11:31:34 +02:00
|
|
|
director(target, cred, req)
|
2018-07-03 15:36:20 +02:00
|
|
|
},
|
|
|
|
ModifyResponse: modifyResponse,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// ServeHTTP serves the incoming http requests
|
2018-07-03 15:36:20 +02:00
|
|
|
func (pe *ProxyEngine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
|
|
pe.engine.ServeHTTP(w, req)
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Overwrite the http requests
|
2018-07-11 11:31:34 +02:00
|
|
|
func director(target *url.URL, cred *Credential, req *http.Request) {
|
2018-09-05 10:16:31 +02:00
|
|
|
// Closure
|
2018-07-03 15:36:20 +02:00
|
|
|
targetQuery := target.RawQuery
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Overwrite the request URL to the target path
|
2018-07-03 15:36:20 +02:00
|
|
|
req.URL.Scheme = target.Scheme
|
|
|
|
req.URL.Host = target.Host
|
2018-09-19 08:45:52 +02:00
|
|
|
rewriteURLPath(req)
|
2018-07-03 15:36:20 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Add authentication header if it is existing
|
2018-07-11 11:31:34 +02:00
|
|
|
if cred != nil {
|
|
|
|
req.SetBasicAuth(cred.Username, cred.Password)
|
|
|
|
}
|
2018-07-03 15:36:20 +02:00
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Modify the http response
|
2018-07-03 15:36:20 +02:00
|
|
|
func modifyResponse(res *http.Response) error {
|
2019-04-15 04:44:56 +02:00
|
|
|
// Upload chart success, then to the notification to replication handler
|
|
|
|
if res.StatusCode == http.StatusCreated {
|
|
|
|
// 201 and has chart_upload(namespace-repository-version) context
|
|
|
|
// means this response is for uploading chart success.
|
|
|
|
chartUpload := res.Request.Context().Value(common.ChartUploadCtxKey).(string)
|
|
|
|
if chartUpload != "" {
|
|
|
|
chartUploadSplitted := strings.Split(chartUpload, ":")
|
|
|
|
if len(chartUploadSplitted) == 3 {
|
|
|
|
// Todo: it used as the replacement of webhook, will be removed when webhook to be introduced.
|
|
|
|
go func() {
|
|
|
|
e := &rep_event.Event{
|
|
|
|
Type: rep_event.EventTypeChartUpload,
|
|
|
|
Resource: &model.Resource{
|
|
|
|
Type: model.ResourceTypeChart,
|
|
|
|
Metadata: &model.ResourceMetadata{
|
|
|
|
Namespace: &model.Namespace{
|
|
|
|
Name: chartUploadSplitted[0],
|
|
|
|
},
|
|
|
|
Repository: &model.Repository{
|
|
|
|
Name: chartUploadSplitted[1],
|
|
|
|
},
|
|
|
|
Vtags: []string{chartUploadSplitted[2]},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if err := replication.EventHandler.Handle(e); err != nil {
|
|
|
|
hlog.Errorf("failed to handle event: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Accept cases
|
|
|
|
// Success or redirect
|
2018-07-19 17:50:25 +02:00
|
|
|
if res.StatusCode >= http.StatusOK && res.StatusCode <= http.StatusTemporaryRedirect {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Detect the 401 code, if it is,overwrite it to 500.
|
|
|
|
// We also re-write the error content to structural error object
|
2018-07-19 17:50:25 +02:00
|
|
|
errorObj := make(map[string]string)
|
2018-07-03 15:36:20 +02:00
|
|
|
if res.StatusCode == http.StatusUnauthorized {
|
2018-07-19 17:50:25 +02:00
|
|
|
errorObj["error"] = "operation request from unauthorized source is rejected"
|
|
|
|
res.StatusCode = http.StatusInternalServerError
|
|
|
|
} else {
|
2018-09-05 10:16:31 +02:00
|
|
|
// Extract the error and wrap it into the error object
|
2018-07-19 17:50:25 +02:00
|
|
|
data, err := ioutil.ReadAll(res.Body)
|
2018-07-03 15:36:20 +02:00
|
|
|
if err != nil {
|
2018-07-19 17:50:25 +02:00
|
|
|
errorObj["error"] = fmt.Sprintf("%s: %s", res.Status, err.Error())
|
|
|
|
} else {
|
|
|
|
if err := json.Unmarshal(data, &errorObj); err != nil {
|
|
|
|
errorObj["error"] = string(data)
|
|
|
|
}
|
2018-07-03 15:36:20 +02:00
|
|
|
}
|
2018-07-19 17:50:25 +02:00
|
|
|
}
|
2018-07-03 15:36:20 +02:00
|
|
|
|
2018-07-19 17:50:25 +02:00
|
|
|
content, err := json.Marshal(errorObj)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2018-07-03 15:36:20 +02:00
|
|
|
}
|
|
|
|
|
2018-07-19 17:50:25 +02:00
|
|
|
size := len(content)
|
|
|
|
body := ioutil.NopCloser(bytes.NewReader(content))
|
|
|
|
res.Body = body
|
|
|
|
res.ContentLength = int64(size)
|
|
|
|
res.Header.Set(contentLengthHeader, strconv.Itoa(size))
|
|
|
|
|
2018-07-03 15:36:20 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Join the path
|
|
|
|
// Copy from the go reverse proxy
|
2018-07-03 15:36:20 +02:00
|
|
|
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
|
|
|
|
}
|
2018-09-19 08:45:52 +02:00
|
|
|
|
|
|
|
// 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) {
|
2018-09-20 17:30:36 +02:00
|
|
|
incomingURLPath := req.URL.Path
|
2018-09-19 08:45:52 +02:00
|
|
|
|
|
|
|
// Health check endpoint
|
|
|
|
if incomingURLPath == chartRepoHealthEndpoint {
|
|
|
|
req.URL.Path = "/health"
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Root uploading endpoint
|
|
|
|
if incomingURLPath == rootUploadingEndpoint {
|
|
|
|
req.URL.Path = strings.Replace(incomingURLPath, "chartrepo", defaultRepo, 1)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Repository endpoints
|
|
|
|
if strings.HasPrefix(incomingURLPath, "/chartrepo") {
|
|
|
|
req.URL.Path = strings.TrimPrefix(incomingURLPath, "/chartrepo")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// API endpoints
|
|
|
|
if strings.HasPrefix(incomingURLPath, "/api/chartrepo") {
|
|
|
|
req.URL.Path = strings.Replace(incomingURLPath, "/chartrepo", "", 1)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|