mirror of
https://github.com/umputun/reproxy.git
synced 2024-09-11 08:55:25 +03:00
Multiple static location (#36)
* add isStatic flag to mapper, implement for file and static providers * handle static match response as a special case * move assets conversion to load time * rename static to assets everywhere for consistency * don't overwride asset param in url mapper * add documentation about assets mode * add tests
This commit is contained in:
parent
33346c9f7a
commit
8cf4b9063d
15
README.md
15
README.md
@ -92,11 +92,22 @@ SSL mode (by default none) can be set to `auto` (ACME/LE certificates), `static`
|
||||
|
||||
By default no request log generated. This can be turned on by setting `--logger.enabled`. The log (auto-rotated) has [Apache Combined Log Format](http://httpd.apache.org/docs/2.2/logs.html#combined)
|
||||
|
||||
User can also turn stdout log on with `--logger.stdout`. It won't affect the file logging but will output some minimal info about processed requests, something like this:
|
||||
|
||||
```
|
||||
2021/04/16 01:17:25.601 [INFO] GET - /echo/image.png - xxx.xxx.xxx.xxx - 200 (155400) - 371.661251ms
|
||||
2021/04/16 01:18:18.959 [INFO] GET - /api/v1/params - xxx.xxx.xxx.xxx - 200 (74) - 1.217669m
|
||||
```
|
||||
|
||||
## Assets Server
|
||||
|
||||
User may turn assets server on (off by default) to serve static files. As long as `--assets.location` set it will treat every non-proxied request under `assets.root` as a request for static files.
|
||||
User may turn assets server on (off by default) to serve static files. As long as `--assets.location` set it will treat every non-proxied request under `assets.root` as a request for static files. Assets server can be used without any proxy providers. In this mode reproxy acts as a simple web server for a static context.
|
||||
|
||||
Assets server can be used without any proxy providers. In this mode reproxy acts as a simple web server for a static context.
|
||||
In addition to the common assets server multiple custom static servers supported. Each provider has a different way to define such static rule and some providers may not support it at all. For example, multiple static server make sense in case of static (command line provide), file provider and can be even useful with docker provider.
|
||||
|
||||
1. static provider - if source element prefixed by `assets:` it will be treated as file-server. For example `*,assets:/web,/var/www,` will serve all `/web/*` request with a file server on top of `/var/www` directory.
|
||||
2. file provider - setting optional field `assets: true`
|
||||
3. docker provider - `reproxy.assets=web-root:location`, i.e. `reproxy.assets=/web:/var/www`.
|
||||
|
||||
## More options
|
||||
|
||||
|
@ -32,6 +32,10 @@ type URLMapper struct {
|
||||
Dst string
|
||||
ProviderID ProviderID
|
||||
PingURL string
|
||||
MatchType MatchType
|
||||
|
||||
AssetsLocation string
|
||||
AssetsWebRoot string
|
||||
}
|
||||
|
||||
// Provider defines sources of mappers
|
||||
@ -50,6 +54,26 @@ const (
|
||||
PIFile ProviderID = "file"
|
||||
)
|
||||
|
||||
// MatchType defines the type of mapper (rule)
|
||||
type MatchType int
|
||||
|
||||
// enum of all match types
|
||||
const (
|
||||
MTProxy MatchType = iota
|
||||
MTStatic
|
||||
)
|
||||
|
||||
func (m MatchType) String() string {
|
||||
switch m {
|
||||
case MTProxy:
|
||||
return "proxy"
|
||||
case MTStatic:
|
||||
return "static"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// NewService makes service with given providers
|
||||
func NewService(providers []Provider, interval time.Duration) *Service {
|
||||
return &Service{providers: providers, interval: interval}
|
||||
@ -79,7 +103,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
evRecv = false
|
||||
lst := s.mergeLists()
|
||||
for _, m := range lst {
|
||||
log.Printf("[INFO] match for %s: %s %s -> %s", m.ProviderID, m.Server, m.SrcMatch.String(), m.Dst)
|
||||
log.Printf("[INFO] match for %s: %s %s -> %s (%s)", m.ProviderID, m.Server, m.SrcMatch.String(), m.Dst, m.MatchType)
|
||||
}
|
||||
s.lock.Lock()
|
||||
s.mappers = make(map[string][]URLMapper)
|
||||
@ -92,19 +116,39 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Match url to all mappers
|
||||
func (s *Service) Match(srv, src string) (string, bool) {
|
||||
func (s *Service) Match(srv, src string) (string, MatchType, bool) {
|
||||
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
var staticRules []URLMapper
|
||||
for _, srvName := range []string{srv, "*", ""} {
|
||||
for _, m := range s.mappers[srvName] {
|
||||
if m.MatchType == MTStatic { // collect static for
|
||||
staticRules = append(staticRules, m)
|
||||
continue
|
||||
}
|
||||
dest := m.SrcMatch.ReplaceAllString(src, m.Dst)
|
||||
if src != dest {
|
||||
return dest, true
|
||||
return dest, m.MatchType, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return src, false
|
||||
|
||||
// process static rules after all regular proxy rules as we want to prioritize regular rules
|
||||
// static rule returns a pair (separated by :) of assets location:assets web root
|
||||
for _, m := range staticRules {
|
||||
dest := m.SrcMatch.ReplaceAllString(src, m.Dst)
|
||||
if src == dest { // try to match with trialing / to match web root requests, i.e. /web (without trailing /)
|
||||
dest := m.SrcMatch.ReplaceAllString(src+"/", m.Dst)
|
||||
if src+"/" == dest {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return m.AssetsWebRoot + ":" + m.AssetsLocation, MTStatic, true
|
||||
}
|
||||
|
||||
return src, MTProxy, false
|
||||
}
|
||||
|
||||
// Servers return list of all servers, skips "*" (catch-all/default)
|
||||
@ -158,19 +202,30 @@ func (s *Service) mergeLists() (res []URLMapper) {
|
||||
func (s *Service) extendMapper(m URLMapper) URLMapper {
|
||||
|
||||
src := m.SrcMatch.String()
|
||||
m.Dst = strings.Replace(m.Dst, "@", "$", -1) // allow group defined as @n instead of $n (yaml friendly)
|
||||
|
||||
// TODO: Probably should be ok in practice but we better figure a nicer way to do it
|
||||
if strings.Contains(m.Dst, "$1") || strings.Contains(m.Dst, "@1") ||
|
||||
strings.Contains(src, "(") || !strings.HasSuffix(src, "/") {
|
||||
if m.MatchType == MTStatic && m.AssetsWebRoot == "" && m.AssetsLocation == "" {
|
||||
m.AssetsWebRoot = strings.TrimSuffix(src, "/")
|
||||
m.AssetsLocation = strings.TrimSuffix(m.Dst, "/") + "/"
|
||||
}
|
||||
|
||||
m.Dst = strings.Replace(m.Dst, "@", "$", -1) // allow group defined as @n instead of $n
|
||||
// don't extend src and dst with dst or src regex groups
|
||||
if strings.Contains(m.Dst, "$") || strings.Contains(m.Dst, "@") || strings.Contains(src, "(") {
|
||||
return m
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(src, "/") && m.MatchType == MTProxy {
|
||||
return m
|
||||
}
|
||||
|
||||
res := URLMapper{
|
||||
Server: m.Server,
|
||||
Dst: strings.TrimSuffix(m.Dst, "/") + "/$1",
|
||||
ProviderID: m.ProviderID,
|
||||
PingURL: m.PingURL,
|
||||
Server: m.Server,
|
||||
Dst: strings.TrimSuffix(m.Dst, "/") + "/$1",
|
||||
ProviderID: m.ProviderID,
|
||||
PingURL: m.PingURL,
|
||||
MatchType: m.MatchType,
|
||||
AssetsWebRoot: m.AssetsWebRoot,
|
||||
AssetsLocation: m.AssetsLocation,
|
||||
}
|
||||
|
||||
rx, err := regexp.Compile("^" + strings.TrimSuffix(src, "/") + "/(.*)")
|
||||
@ -212,3 +267,13 @@ func (s *Service) mergeEvents(ctx context.Context, chs ...<-chan ProviderID) <-c
|
||||
}()
|
||||
return out
|
||||
}
|
||||
|
||||
// Contains checks if the input string (e) in the given slice
|
||||
func Contains(e string, s []string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -81,6 +81,8 @@ func TestService_Match(t *testing.T) {
|
||||
ListFunc: func() ([]URLMapper, error) {
|
||||
return []URLMapper{
|
||||
{SrcMatch: *regexp.MustCompile("/api/svc3/xyz"), Dst: "http://127.0.0.3:8080/blah3/xyz", ProviderID: PIDocker},
|
||||
{SrcMatch: *regexp.MustCompile("/web"), Dst: "/var/web", ProviderID: PIDocker, MatchType: MTStatic},
|
||||
{SrcMatch: *regexp.MustCompile("/www/"), Dst: "/var/web", ProviderID: PIDocker, MatchType: MTStatic},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
@ -91,27 +93,35 @@ func TestService_Match(t *testing.T) {
|
||||
err := svc.Run(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, context.DeadlineExceeded, err)
|
||||
assert.Equal(t, 3, len(svc.Mappers()))
|
||||
assert.Equal(t, 5, len(svc.Mappers()))
|
||||
|
||||
tbl := []struct {
|
||||
server, src string
|
||||
dest string
|
||||
mt MatchType
|
||||
ok bool
|
||||
}{
|
||||
{"example.com", "/api/svc3/xyz/something", "http://127.0.0.3:8080/blah3/xyz/something", true},
|
||||
{"example.com", "/api/svc3/xyz", "http://127.0.0.3:8080/blah3/xyz", true},
|
||||
{"abc.example.com", "/api/svc1/1234", "http://127.0.0.1:8080/blah1/1234", true},
|
||||
{"zzz.example.com", "/aaa/api/svc1/1234", "/aaa/api/svc1/1234", false},
|
||||
{"m.example.com", "/api/svc2/1234", "http://127.0.0.2:8080/blah2/1234/abc", true},
|
||||
{"m1.example.com", "/api/svc2/1234", "/api/svc2/1234", false},
|
||||
{"example.com", "/api/svc3/xyz/something", "http://127.0.0.3:8080/blah3/xyz/something", MTProxy, true},
|
||||
{"example.com", "/api/svc3/xyz", "http://127.0.0.3:8080/blah3/xyz", MTProxy, true},
|
||||
{"abc.example.com", "/api/svc1/1234", "http://127.0.0.1:8080/blah1/1234", MTProxy, true},
|
||||
{"zzz.example.com", "/aaa/api/svc1/1234", "/aaa/api/svc1/1234", MTProxy, false},
|
||||
{"m.example.com", "/api/svc2/1234", "http://127.0.0.2:8080/blah2/1234/abc", MTProxy, true},
|
||||
{"m1.example.com", "/api/svc2/1234", "/api/svc2/1234", MTProxy, false},
|
||||
{"m1.example.com", "/web/index.html", "/web:/var/web/", MTStatic, true},
|
||||
{"m1.example.com", "/web/", "/web:/var/web/", MTStatic, true},
|
||||
{"m1.example.com", "/www", "/www:/var/web/", MTStatic, true},
|
||||
{"m1.example.com", "/www/something", "/www:/var/web/", MTStatic, true},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
res, ok := svc.Match(tt.server, tt.src)
|
||||
res, mt, ok := svc.Match(tt.server, tt.src)
|
||||
assert.Equal(t, tt.ok, ok)
|
||||
assert.Equal(t, tt.dest, res)
|
||||
if ok {
|
||||
assert.Equal(t, tt.mt, mt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ func (d *Docker) List() ([]discovery.URLMapper, error) {
|
||||
destURL := fmt.Sprintf("http://%s:%d/$1", c.IP, port)
|
||||
pingURL := fmt.Sprintf("http://%s:%d/ping", c.IP, port)
|
||||
server := "*"
|
||||
assetsWebRoot, assetsLocation := "", ""
|
||||
|
||||
// we don't care about value because disabled will be filtered before
|
||||
if _, ok := c.Labels["reproxy.enabled"]; ok {
|
||||
@ -117,6 +118,14 @@ func (d *Docker) List() ([]discovery.URLMapper, error) {
|
||||
pingURL = fmt.Sprintf("http://%s:%d%s", c.IP, port, v)
|
||||
}
|
||||
|
||||
if v, ok := c.Labels["reproxy.assets"]; ok {
|
||||
if ae := strings.Split(v, ":"); len(ae) == 2 {
|
||||
enabled = true
|
||||
assetsWebRoot = ae[0]
|
||||
assetsLocation = ae[1]
|
||||
}
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
log.Printf("[DEBUG] container %s disabled", c.Name)
|
||||
continue
|
||||
@ -129,8 +138,16 @@ func (d *Docker) List() ([]discovery.URLMapper, error) {
|
||||
|
||||
// docker server label may have multiple, comma separated servers
|
||||
for _, srv := range strings.Split(server, ",") {
|
||||
res = append(res, discovery.URLMapper{Server: strings.TrimSpace(srv), SrcMatch: *srcRegex, Dst: destURL,
|
||||
PingURL: pingURL, ProviderID: discovery.PIDocker})
|
||||
mp := discovery.URLMapper{Server: strings.TrimSpace(srv), SrcMatch: *srcRegex, Dst: destURL,
|
||||
PingURL: pingURL, ProviderID: discovery.PIDocker, MatchType: discovery.MTProxy}
|
||||
|
||||
if assetsWebRoot != "" {
|
||||
mp.MatchType = discovery.MTStatic
|
||||
mp.AssetsWebRoot = assetsWebRoot
|
||||
mp.AssetsLocation = assetsLocation
|
||||
}
|
||||
|
||||
res = append(res, mp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,7 +201,7 @@ func (d *Docker) events(ctx context.Context, client DockerClient, eventsCh chan
|
||||
log.Printf("[DEBUG] api event %+v", ev)
|
||||
containerName := strings.TrimPrefix(ev.Actor.Attributes["name"], "/")
|
||||
|
||||
if contains(containerName, d.Excludes) {
|
||||
if discovery.Contains(containerName, d.Excludes) {
|
||||
log.Printf("[DEBUG] container %s excluded", containerName)
|
||||
continue
|
||||
}
|
||||
@ -213,12 +230,12 @@ func (d *Docker) listContainers() (res []containerInfo, err error) {
|
||||
log.Printf("[DEBUG] total containers = %d", len(containers))
|
||||
|
||||
for _, c := range containers {
|
||||
if !contains(c.State, []string{"running"}) {
|
||||
if c.State != "running" {
|
||||
log.Printf("[DEBUG] skip container %s due to state %s", c.Names[0], c.State)
|
||||
continue
|
||||
}
|
||||
containerName := strings.TrimPrefix(c.Names[0], "/")
|
||||
if contains(containerName, d.Excludes) || strings.EqualFold(containerName, "reproxy") {
|
||||
if discovery.Contains(containerName, d.Excludes) || strings.EqualFold(containerName, "reproxy") {
|
||||
log.Printf("[DEBUG] container %s excluded", containerName)
|
||||
continue
|
||||
}
|
||||
@ -263,12 +280,3 @@ func (d *Docker) listContainers() (res []containerInfo, err error) {
|
||||
log.Print("[DEBUG] completed list")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func contains(e string, s []string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -70,9 +70,10 @@ func (d *File) Events(ctx context.Context) <-chan discovery.ProviderID {
|
||||
func (d *File) List() (res []discovery.URLMapper, err error) {
|
||||
|
||||
var fileConf map[string][]struct {
|
||||
SourceRoute string `yaml:"route"`
|
||||
Dest string `yaml:"dest"`
|
||||
Ping string `yaml:"ping"`
|
||||
SourceRoute string `yaml:"route"`
|
||||
Dest string `yaml:"dest"`
|
||||
Ping string `yaml:"ping"`
|
||||
AssetsEnabled bool `yaml:"assets"`
|
||||
}
|
||||
fh, err := os.Open(d.FileName)
|
||||
if err != nil {
|
||||
@ -94,7 +95,17 @@ func (d *File) List() (res []discovery.URLMapper, err error) {
|
||||
if srv == "default" {
|
||||
srv = "*"
|
||||
}
|
||||
mapper := discovery.URLMapper{Server: srv, SrcMatch: *rx, Dst: f.Dest, PingURL: f.Ping, ProviderID: discovery.PIFile}
|
||||
mapper := discovery.URLMapper{
|
||||
Server: srv,
|
||||
SrcMatch: *rx,
|
||||
Dst: f.Dest,
|
||||
PingURL: f.Ping,
|
||||
ProviderID: discovery.PIFile,
|
||||
MatchType: discovery.MTProxy,
|
||||
}
|
||||
if f.AssetsEnabled {
|
||||
mapper.MatchType = discovery.MTStatic
|
||||
}
|
||||
res = append(res, mapper)
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ func TestFile_Events_BusyListener(t *testing.T) {
|
||||
// exhaust creation and one write event
|
||||
for i := 0; i < 2; i++ {
|
||||
t.Log("event")
|
||||
<- ch
|
||||
<-ch
|
||||
}
|
||||
|
||||
// wait until last write definitely has happened
|
||||
@ -105,20 +105,25 @@ func TestFile_List(t *testing.T) {
|
||||
res, err := f.List()
|
||||
require.NoError(t, err)
|
||||
t.Logf("%+v", res)
|
||||
assert.Equal(t, 3, len(res))
|
||||
assert.Equal(t, 4, len(res))
|
||||
|
||||
assert.Equal(t, "/api/svc3/xyz", res[0].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.3:8080/blah3/xyz", res[0].Dst)
|
||||
assert.Equal(t, "http://127.0.0.3:8080/ping", res[0].PingURL)
|
||||
assert.Equal(t, "*", res[0].Server)
|
||||
|
||||
assert.Equal(t, "^/api/svc1/(.*)", res[1].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.1:8080/blah1/$1", res[1].Dst)
|
||||
assert.Equal(t, "/web/", res[1].SrcMatch.String())
|
||||
assert.Equal(t, "/var/web", res[1].Dst)
|
||||
assert.Equal(t, "", res[1].PingURL)
|
||||
assert.Equal(t, "*", res[1].Server)
|
||||
|
||||
assert.Equal(t, "^/api/svc2/(.*)", res[2].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.2:8080/blah2/$1/abc", res[2].Dst)
|
||||
assert.Equal(t, "^/api/svc1/(.*)", res[2].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.1:8080/blah1/$1", res[2].Dst)
|
||||
assert.Equal(t, "", res[2].PingURL)
|
||||
assert.Equal(t, "srv.example.com", res[2].Server)
|
||||
assert.Equal(t, "*", res[2].Server)
|
||||
|
||||
assert.Equal(t, "^/api/svc2/(.*)", res[3].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.2:8080/blah2/$1/abc", res[3].Dst)
|
||||
assert.Equal(t, "", res[3].PingURL)
|
||||
assert.Equal(t, "srv.example.com", res[3].Server)
|
||||
}
|
||||
|
@ -34,13 +34,26 @@ func (s *Static) List() (res []discovery.URLMapper, err error) {
|
||||
return discovery.URLMapper{}, fmt.Errorf("can't parse regex %s: %w", elems[1], err)
|
||||
}
|
||||
|
||||
return discovery.URLMapper{
|
||||
dst := strings.TrimSpace(elems[2])
|
||||
assets := false
|
||||
if strings.HasPrefix(dst, "assets:") {
|
||||
dst = strings.TrimPrefix(dst, "assets:")
|
||||
assets = true
|
||||
}
|
||||
|
||||
res := discovery.URLMapper{
|
||||
Server: strings.TrimSpace(elems[0]),
|
||||
SrcMatch: *rx,
|
||||
Dst: strings.TrimSpace(elems[2]),
|
||||
Dst: dst,
|
||||
PingURL: strings.TrimSpace(elems[3]),
|
||||
ProviderID: discovery.PIStatic,
|
||||
}, nil
|
||||
MatchType: discovery.MTProxy,
|
||||
}
|
||||
if assets {
|
||||
res.MatchType = discovery.MTStatic
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
for _, r := range s.Rules {
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/reproxy/app/discovery"
|
||||
)
|
||||
|
||||
func TestStatic_List(t *testing.T) {
|
||||
@ -13,13 +15,15 @@ func TestStatic_List(t *testing.T) {
|
||||
tbl := []struct {
|
||||
rule string
|
||||
server, src, dst, ping string
|
||||
static bool
|
||||
err bool
|
||||
}{
|
||||
{"example.com,123,456, ping ", "example.com", "123", "456", "ping", false},
|
||||
{"*,123,456,", "*", "123", "456", "", false},
|
||||
{"123,456", "", "", "", "", true},
|
||||
{"123", "", "", "", "", true},
|
||||
{"example.com , 123, 456 ,ping", "example.com", "123", "456", "ping", false},
|
||||
{"example.com,123,456, ping ", "example.com", "123", "456", "ping", false, false},
|
||||
{"*,123,456,", "*", "123", "456", "", false, false},
|
||||
{"123,456", "", "", "", "", false, true},
|
||||
{"123", "", "", "", "", false, true},
|
||||
{"example.com , 123, 456 ,ping", "example.com", "123", "456", "ping", false, false},
|
||||
{"example.com,123, assets:456, ping ", "example.com", "123", "456", "ping", true, false},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
@ -36,6 +40,11 @@ func TestStatic_List(t *testing.T) {
|
||||
assert.Equal(t, tt.src, res[0].SrcMatch.String())
|
||||
assert.Equal(t, tt.dst, res[0].Dst)
|
||||
assert.Equal(t, tt.ping, res[0].PingURL)
|
||||
if tt.static {
|
||||
assert.Equal(t, discovery.MTStatic, res[0].MatchType)
|
||||
} else {
|
||||
assert.Equal(t, discovery.MTProxy, res[0].MatchType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
1
app/discovery/provider/testdata/config.yml
vendored
1
app/discovery/provider/testdata/config.yml
vendored
@ -1,5 +1,6 @@
|
||||
default:
|
||||
- {route: "^/api/svc1/(.*)", dest: "http://127.0.0.1:8080/blah1/$1"}
|
||||
- {route: "/api/svc3/xyz", dest: "http://127.0.0.3:8080/blah3/xyz", "ping": "http://127.0.0.3:8080/ping"}
|
||||
- {route: "/web/", dest: "/var/web", "static": yes}
|
||||
srv.example.com:
|
||||
- {route: "^/api/svc2/(.*)", dest: "http://127.0.0.2:8080/blah2/$1/abc"}
|
||||
|
35
app/main.go
35
app/main.go
@ -6,9 +6,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@ -103,7 +103,15 @@ func main() {
|
||||
}
|
||||
|
||||
setupLog(opts.Dbg)
|
||||
catchSignal()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() { // catch signal and invoke graceful termination
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
<-stop
|
||||
log.Printf("[WARN] interrupt signal")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
providers, err := makeProviders()
|
||||
if err != nil {
|
||||
@ -163,7 +171,11 @@ func main() {
|
||||
ResponseHeader: opts.Timeouts.ResponseHeader,
|
||||
},
|
||||
}
|
||||
if err := px.Run(context.Background()); err != nil {
|
||||
if err := px.Run(ctx); err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
log.Printf("[WARN] proxy server closed, %v", err) //nolint gocritic
|
||||
return
|
||||
}
|
||||
log.Fatalf("[ERROR] proxy server failed, %v", err) //nolint gocritic
|
||||
}
|
||||
}
|
||||
@ -258,20 +270,3 @@ func setupLog(dbg bool) {
|
||||
}
|
||||
log.Setup(log.Msec, log.LevelBraces)
|
||||
}
|
||||
|
||||
func catchSignal() {
|
||||
// catch SIGQUIT and print stack traces
|
||||
sigChan := make(chan os.Signal)
|
||||
go func() {
|
||||
for range sigChan {
|
||||
log.Print("[INFO] SIGQUIT detected")
|
||||
stacktrace := make([]byte, 8192)
|
||||
length := runtime.Stack(stacktrace, true)
|
||||
if length > 8192 {
|
||||
length = 8192
|
||||
}
|
||||
fmt.Println(string(stacktrace[:length]))
|
||||
}
|
||||
}()
|
||||
signal.Notify(sigChan, syscall.SIGQUIT)
|
||||
}
|
||||
|
97
app/main_test.go
Normal file
97
app/main_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Main(t *testing.T) {
|
||||
|
||||
port := chooseRandomUnusedPort()
|
||||
os.Args = []string{"test", "--static.enabled",
|
||||
"--static.rule=*,/svc1, https://httpbin.org/get,https://feedmaster.umputun.com/ping",
|
||||
"--dbg", "--logger.stdout", "--listen=127.0.0.1:" + strconv.Itoa(port), "--signature"}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
<-done
|
||||
e := syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
require.NoError(t, e)
|
||||
}()
|
||||
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
main()
|
||||
close(finished)
|
||||
}()
|
||||
|
||||
// defer cleanup because require check below can fail
|
||||
defer func() {
|
||||
close(done)
|
||||
<-finished
|
||||
}()
|
||||
|
||||
waitForHTTPServerStart(port)
|
||||
time.Sleep(time.Second)
|
||||
|
||||
{
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/ping", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pong", string(body))
|
||||
}
|
||||
{
|
||||
client := http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/svc1", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(body), `"Host": "127.0.0.1"`)
|
||||
}
|
||||
{
|
||||
client := http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/bas", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func chooseRandomUnusedPort() (port int) {
|
||||
for i := 0; i < 10; i++ {
|
||||
port = 40000 + int(rand.Int31n(10000))
|
||||
if ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)); err == nil {
|
||||
_ = ln.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
func waitForHTTPServerStart(port int) {
|
||||
// wait for up to 10 seconds for server to start before returning it
|
||||
client := http.Client{Timeout: time.Second}
|
||||
for i := 0; i < 100; i++ {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
if resp, err := client.Get(fmt.Sprintf("http://localhost:%d", port)); err == nil {
|
||||
_ = resp.Body.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@ type Http struct { // nolint golint
|
||||
// Matcher source info (server and route) to the destination url
|
||||
// If no match found return ok=false
|
||||
type Matcher interface {
|
||||
Match(srv, src string) (string, bool)
|
||||
Match(srv, src string) (string, discovery.MatchType, bool)
|
||||
Servers() (servers []string)
|
||||
Mappers() (mappers []discovery.URLMapper)
|
||||
}
|
||||
@ -194,20 +194,36 @@ func (h *Http) proxyHandler() http.HandlerFunc {
|
||||
if server == "" {
|
||||
server = strings.Split(r.Host, ":")[0]
|
||||
}
|
||||
u, ok := h.Match(server, r.URL.Path)
|
||||
u, mt, ok := h.Match(server, r.URL.Path)
|
||||
if !ok {
|
||||
assetsHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
uu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusBadGateway)
|
||||
return
|
||||
switch mt {
|
||||
case discovery.MTProxy:
|
||||
uu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] proxy to %s", uu)
|
||||
ctx := context.WithValue(r.Context(), contextKey("url"), uu) // set destination url in request's context
|
||||
reverseProxy.ServeHTTP(w, r.WithContext(ctx))
|
||||
case discovery.MTStatic:
|
||||
// static match result has webroot:location, i.e. /www:/var/somedir/
|
||||
ae := strings.Split(u, ":")
|
||||
if len(ae) != 2 { // shouldn't happen
|
||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fs, err := R.FileServer(ae[0], ae[1])
|
||||
if err != nil {
|
||||
http.Error(w, "Server error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), contextKey("url"), uu) // set destination url in request's context
|
||||
reverseProxy.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
func TestHttp_Do(t *testing.T) {
|
||||
port := rand.Intn(10000) + 40000
|
||||
h := Http{Timeouts: Timeouts{ResponseHeader: 200 * time.Millisecond}, Address: fmt.Sprintf("127.0.0.1:%d", port),
|
||||
AccessLog: io.Discard, Signature: true, ProxyHeaders: []string{"hh1:vv1", "hh2:vv2"}}
|
||||
AccessLog: io.Discard, Signature: true, ProxyHeaders: []string{"hh1:vv1", "hh2:vv2"}, StdOutEnabled: true}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
@ -154,7 +154,72 @@ func TestHttp_DoWithAssets(t *testing.T) {
|
||||
assert.Equal(t, "test html", string(body))
|
||||
assert.Equal(t, "", resp.Header.Get("App-Name"))
|
||||
assert.Equal(t, "", resp.Header.Get("h1"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHttp_DoWithAssetRules(t *testing.T) {
|
||||
port := rand.Intn(10000) + 40000
|
||||
h := Http{Timeouts: Timeouts{ResponseHeader: 200 * time.Millisecond}, Address: fmt.Sprintf("127.0.0.1:%d", port),
|
||||
AccessLog: io.Discard}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
ds := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("req: %v", r)
|
||||
w.Header().Add("h1", "v1")
|
||||
require.Equal(t, "127.0.0.1", r.Header.Get("X-Real-IP"))
|
||||
fmt.Fprintf(w, "response %s", r.URL.String())
|
||||
}))
|
||||
|
||||
svc := discovery.NewService([]discovery.Provider{
|
||||
&provider.Static{Rules: []string{
|
||||
"localhost,^/api/(.*)," + ds.URL + "/123/$1,",
|
||||
"127.0.0.1,^/api/(.*)," + ds.URL + "/567/$1,",
|
||||
"*,/web,assets:testdata,",
|
||||
},
|
||||
}}, time.Millisecond*10)
|
||||
|
||||
go func() {
|
||||
_ = svc.Run(context.Background())
|
||||
}()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
h.Matcher = svc
|
||||
go func() {
|
||||
_ = h.Run(ctx)
|
||||
}()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
client := http.Client{}
|
||||
|
||||
{
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:"+strconv.Itoa(port)+"/api/something", nil)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
t.Logf("%+v", resp.Header)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "response /567/something", string(body))
|
||||
assert.Equal(t, "", resp.Header.Get("App-Name"))
|
||||
assert.Equal(t, "v1", resp.Header.Get("h1"))
|
||||
}
|
||||
|
||||
{
|
||||
resp, err := client.Get("http://localhost:" + strconv.Itoa(port) + "/web/1.html")
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
t.Logf("%+v", resp.Header)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test html", string(body))
|
||||
assert.Equal(t, "", resp.Header.Get("App-Name"))
|
||||
assert.Equal(t, "", resp.Header.Get("h1"))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,18 +1,17 @@
|
||||
run: install
|
||||
whoami -port 8081 -name=svc1 &
|
||||
whoami -port 8082 -name=svc2 &
|
||||
whoami -port 8083 -name=svc3 &
|
||||
../../dist/reproxy --file.enabled --file.name=reproxy.yml --assets.location=./web --assets.root=/static
|
||||
pkill -9 whoami
|
||||
echo-http --listen=0.0.0.0:8081 --message=svc1 &
|
||||
echo-http --listen=0.0.0.0:8082 --message=svc2 &
|
||||
echo-http --listen=0.0.0.0:8083 --message=svc3 &
|
||||
../../dist/reproxy --file.enabled --file.name=reproxy.yml --assets.location=./web --assets.root=/static --dbg --logger.stdout
|
||||
pkill -9 echo-http
|
||||
|
||||
run_assets_only: install
|
||||
../../dist/reproxy --assets.location=./web --assets.root=/
|
||||
|
||||
pkill -9 whoami
|
||||
pkill -9 echo-http
|
||||
|
||||
kill:
|
||||
pkill -9 whoami
|
||||
pkill -9 echo-http
|
||||
|
||||
install:
|
||||
cd ../../app && CGO_ENABLED=0 go build -o ../dist/reproxy
|
||||
cd /tmp && go install github.com/traefik/whoami@latest
|
||||
cd /tmp && go install github.com/umputun/echo-http@latest
|
||||
|
@ -3,3 +3,4 @@ default:
|
||||
- {route: "/api/svc2", dest: "http://127.0.0.1:8082/api", "ping": "http://127.0.0.1:8082/health"}
|
||||
localhost:
|
||||
- {route: "^/api/svc3/(.*)", dest: "http://localhost:8083/$1","ping": "http://127.0.0.1:8083/health"}
|
||||
- {route: "/www", dest: "web2", "static": y}
|
||||
|
1
examples/file/web2/1.html
Normal file
1
examples/file/web2/1.html
Normal file
@ -0,0 +1 @@
|
||||
1.html web2
|
1
examples/file/web2/index.html
Normal file
1
examples/file/web2/index.html
Normal file
@ -0,0 +1 @@
|
||||
index web2
|
Loading…
Reference in New Issue
Block a user