-
-
Notifications
You must be signed in to change notification settings - Fork 30
/
goproxy.go
113 lines (104 loc) · 3.16 KB
/
goproxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Package goproxy is a LoadBalancer based on httputil.ReverseProxy.
//
// ExtractNameVersion and LoadBalance can be overridden in order to customize
// the behavior.
package goproxy
import (
"errors"
"fmt"
"math/rand"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/creack/goproxy/registry"
)
// Common errors
var (
ErrInvalidService = errors.New("invalid service/version")
)
// ExtractNameVersion is called to lookup the service name / version from
// the requested URL. It should update the URL's Path to reflect the target
// expectation.
var ExtractNameVersion = extractNameVersion
// LoadBalance is the default balancer which will use a random endpoint
// for the given service name/version.
var LoadBalance = loadBalance
// extractNameVersion lookup the target path and extract the name and version.
// It updates the target Path trimming version and name.
// Expected format: `/<name>/<version>/...`
func extractNameVersion(target *url.URL) (name, version string, err error) {
path := target.Path
if len(path) > 1 && path[0] == '/' {
path = path[1:]
}
tmp := strings.Split(path, "/")
if len(tmp) < 2 {
return "", "", fmt.Errorf("Invalid path")
}
name, version = tmp[0], tmp[1]
target.Path = "/" + strings.Join(tmp[2:], "/")
return name, version, nil
}
// loadBalance is a basic loadBalancer which randomly
// tries to connect to one of the endpoints and try again
// in case of failure.
func loadBalance(network, serviceName, serviceVersion string, reg registry.Registry) (net.Conn, error) {
endpoints, err := reg.Lookup(serviceName, serviceVersion)
if err != nil {
return nil, err
}
for {
// No more endpoint, stop
if len(endpoints) == 0 {
break
}
// Select a random endpoint
i := rand.Int() % len(endpoints)
endpoint := endpoints[i]
// Try to connect
conn, err := net.Dial(network, endpoint)
if err != nil {
reg.Failure(serviceName, serviceVersion, endpoint, err)
// Failure: remove the endpoint from the current list and try again.
endpoints = append(endpoints[:i], endpoints[i+1:]...)
continue
}
// Success: return the connection.
return conn, nil
}
// No available endpoint.
return nil, fmt.Errorf("No endpoint available for %s/%s", serviceName, serviceVersion)
}
// NewMultipleHostReverseProxy creates a reverse proxy handler
// that will randomly select a host from the passed `targets`
func NewMultipleHostReverseProxy(reg registry.Registry) http.HandlerFunc {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: func(network, addr string) (net.Conn, error) {
addr = strings.Split(addr, ":")[0]
tmp := strings.Split(addr, "/")
if len(tmp) != 2 {
return nil, ErrInvalidService
}
return LoadBalance(network, tmp[0], tmp[1], reg)
},
TLSHandshakeTimeout: 10 * time.Second,
}
return func(w http.ResponseWriter, req *http.Request) {
name, version, err := ExtractNameVersion(req.URL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
(&httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = name + "/" + version
},
Transport: transport,
}).ServeHTTP(w, req)
}
}