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:
Umputun 2021-04-16 02:49:00 -05:00 committed by GitHub
parent 33346c9f7a
commit 8cf4b9063d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 402 additions and 94 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}
})
}

View File

@ -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"}

View File

@ -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
View 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
}
}
}

View File

@ -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))
}
}

View File

@ -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"))
}
}

View File

@ -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

View File

@ -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}

View File

@ -0,0 +1 @@
1.html web2

View File

@ -0,0 +1 @@
index web2