mirror of
https://github.com/kubevirt/containerized-data-importer.git
synced 2025-06-03 06:30:22 +00:00

* Set htpp(s)_proxy to lower case env variable CURL used by nbdkit doesn't read upper case http(s)_proxy environment variables, and thus was not using the proxy. Changed the variable to be lower case. Added a significant number of tests to test many more variations of using a proxy. Also added https + auth endpoint to the file-host container, so we can test https + auth with the proxy. Added https endpoint to proxy, so we can test an https proxy. Cleaned up some of the error handling in the import controller for the proxy, in particular if a trustedCAProxy is defined. Fixed some of the cluster wide proxy configuration so it works properly inside an openshift cluster. Signed-off-by: Alexander Wels <awels@redhat.com> * Add https proxy support to registry import. Added extra functional tests to test all registry import combinations Signed-off-by: Alexander Wels <awels@redhat.com> * Fixed some tests to work better in Open Shift. Signed-off-by: Alexander Wels <awels@redhat.com>
233 lines
6.3 KiB
Go
233 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"k8s.io/klog/v2"
|
|
|
|
"github.com/pkg/errors"
|
|
"kubevirt.io/containerized-data-importer/pkg/util"
|
|
"kubevirt.io/containerized-data-importer/tests/utils"
|
|
)
|
|
|
|
const (
|
|
contentTypeKey = "Content-Type"
|
|
contentTypeText = "text/plain"
|
|
proxyHeaderKey = "X-GoProxy"
|
|
proxyHeaderValue = "import-proxy-test"
|
|
username = "foo"
|
|
password = "bar"
|
|
httpPort = "8080"
|
|
httpPortWithAuth = "8081"
|
|
httpsPort = "443"
|
|
httpsPortWithAuth = "444"
|
|
withBasicAuth = true
|
|
noBasicAuth = false
|
|
serviceName = "cdi-test-proxy"
|
|
configMapName = serviceName + "-certs"
|
|
certFile = "tls.crt"
|
|
keyFile = "tls.key"
|
|
certDir = "/certs"
|
|
)
|
|
|
|
func startServer(port string, basicAuth bool, useTLS bool, wg *sync.WaitGroup) {
|
|
server := &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
appendForwardedForHeader(r)
|
|
start := time.Now()
|
|
if r.Method == http.MethodConnect {
|
|
handleTunneling(w, r, basicAuth)
|
|
} else {
|
|
handleHTTP(w, r, basicAuth)
|
|
}
|
|
klog.Infof("INFO: METHOD:%s URL:%s SRC IP:%s DURATION:%s USER AGENT:%s\n",
|
|
r.Method, r.URL, r.RemoteAddr, fmt.Sprint(time.Since(start)), r.UserAgent())
|
|
|
|
}),
|
|
// Disable HTTP/2.
|
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
|
}
|
|
if useTLS {
|
|
go func() {
|
|
klog.Infof("INFO: started proxy on port %s\n", port)
|
|
log.Fatal(server.ListenAndServeTLS(filepath.Join(certDir, certFile), filepath.Join(certDir, keyFile)))
|
|
wg.Done()
|
|
}()
|
|
} else {
|
|
go func() {
|
|
klog.Infof("INFO: started proxy on port %s\n", port)
|
|
log.Fatal(server.ListenAndServe())
|
|
wg.Done()
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Prepares the X-Forwarded-For header for another forwarding hop by appending the previous sender's
|
|
// IP address to the X-Forwarded-For chain.
|
|
func appendForwardedForHeader(req *http.Request) {
|
|
// Copied from net/http/httputil/reverseproxy.go:
|
|
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
|
// If we aren't the first proxy retain prior
|
|
// X-Forwarded-For information as a comma+space
|
|
// separated list and fold multiple headers into one.
|
|
if prior, ok := req.Header["X-Forwarded-For"]; ok {
|
|
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
|
}
|
|
req.Header.Set("X-Forwarded-For", clientIP)
|
|
}
|
|
}
|
|
|
|
func handleTunneling(w http.ResponseWriter, req *http.Request, withAuth bool) {
|
|
if withAuth {
|
|
if !isAuthorized(w, req) {
|
|
return
|
|
}
|
|
}
|
|
destConn, err := net.DialTimeout("tcp", req.Host, 300*time.Second)
|
|
if err != nil {
|
|
klog.Infof("ERROR: URL:%s %v\n", req.Host, err.Error())
|
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
hijacker, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
klog.Info("ERROR: Hijacking not supported\n")
|
|
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
clientConn, _, err := hijacker.Hijack()
|
|
if err != nil {
|
|
klog.Infof("ERROR: %v\n", err.Error())
|
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
go transfer(destConn, clientConn, req)
|
|
go transfer(clientConn, destConn, req)
|
|
}
|
|
|
|
func transfer(destination io.WriteCloser, source io.ReadCloser, r *http.Request) {
|
|
defer destination.Close()
|
|
defer source.Close()
|
|
n, err := io.Copy(destination, source)
|
|
if err == nil {
|
|
klog.Infof("INFO: URL:%s SRC IP:%s BYTES:%d USER AGENT:%s\n", r.URL, r.RemoteAddr, n, r.UserAgent())
|
|
} else {
|
|
klog.Infof("INFO: URL:%s SRC IP:%s BYTES:%d, ERROR: %v USER AGENT:%s\n", r.URL, r.RemoteAddr, n, err, r.UserAgent())
|
|
}
|
|
}
|
|
|
|
func handleHTTP(w http.ResponseWriter, req *http.Request, withAuth bool) {
|
|
if withAuth {
|
|
if !isAuthorized(w, req) {
|
|
return
|
|
}
|
|
}
|
|
for k, v := range req.Header {
|
|
klog.Infof("HTTP Request header %s[%v]\n", k, v)
|
|
}
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
for k, v := range resp.Header {
|
|
klog.Infof("HTTP Response header %s%v\n", k, v)
|
|
}
|
|
sender(w, resp)
|
|
}
|
|
|
|
func isAuthorized(w http.ResponseWriter, req *http.Request) bool {
|
|
//Basic auth for https connection
|
|
sEnc := strings.Replace(strings.Replace(req.Header.Get("Proxy-Authorization"), "Proxy-Authorization=Basic ", "", -1), "Basic ", "", -1)
|
|
sDec, _ := base64.StdEncoding.DecodeString(sEnc)
|
|
if auth := strings.Split(string(sDec), ":"); len(auth) > 1 {
|
|
user := auth[0]
|
|
pass := auth[1]
|
|
if username == user && password == pass {
|
|
klog.Infof("INFO: Proxy authorization succeeded\n")
|
|
return true
|
|
}
|
|
}
|
|
|
|
//Basic auth for http connection
|
|
user, pass, _ := req.BasicAuth()
|
|
if username == user && password == pass {
|
|
klog.Infof("INFO: Proxy authorization succeeded\n")
|
|
return true
|
|
}
|
|
|
|
//Not authorized
|
|
klog.Errorf("ERROR: Proxy authorization failed\n")
|
|
resp := newResponse(req,
|
|
contentTypeText, http.StatusUnauthorized,
|
|
"Basic auth wrong")
|
|
sender(w, resp)
|
|
return false
|
|
}
|
|
|
|
func newResponse(r *http.Request, contentType string, status int, body string) *http.Response {
|
|
resp := &http.Response{}
|
|
resp.Request = r
|
|
resp.TransferEncoding = r.TransferEncoding
|
|
resp.Header = make(http.Header)
|
|
resp.Header.Add(contentTypeKey, contentType)
|
|
resp.Header.Add(proxyHeaderKey, proxyHeaderValue)
|
|
resp.StatusCode = status
|
|
resp.Status = http.StatusText(status)
|
|
buf := bytes.NewBufferString(body)
|
|
resp.ContentLength = int64(buf.Len())
|
|
resp.Body = ioutil.NopCloser(buf)
|
|
return resp
|
|
}
|
|
|
|
func sender(w http.ResponseWriter, resp *http.Response) {
|
|
resp.Header.Set(proxyHeaderKey, proxyHeaderValue)
|
|
defer resp.Body.Close()
|
|
copyHeader(w.Header(), resp.Header)
|
|
w.WriteHeader(resp.StatusCode)
|
|
io.Copy(w, resp.Body)
|
|
}
|
|
|
|
func copyHeader(dst, src http.Header) {
|
|
for k, vv := range src {
|
|
for _, v := range vv {
|
|
dst.Add(k, v)
|
|
}
|
|
}
|
|
}
|
|
func main() {
|
|
if err := utils.CreateCertForTestService(util.GetNamespace(), serviceName, configMapName, certDir, certFile, keyFile); err != nil {
|
|
klog.Fatal(errors.Wrapf(err, "populate certificate directory %s' errored: ", certDir))
|
|
}
|
|
|
|
wg := new(sync.WaitGroup)
|
|
|
|
wg.Add(1)
|
|
startServer(httpPort, noBasicAuth, false, wg)
|
|
|
|
wg.Add(1)
|
|
startServer(httpPortWithAuth, withBasicAuth, false, wg)
|
|
|
|
wg.Add(1)
|
|
startServer(httpsPort, noBasicAuth, true, wg)
|
|
|
|
wg.Add(1)
|
|
startServer(httpsPortWithAuth, withBasicAuth, true, wg)
|
|
|
|
wg.Wait()
|
|
}
|