13 Commits
v0.7 ... v0.9.0

Author SHA1 Message Date
bel
00591f5dde meta access log to stdout 2022-05-26 20:04:00 -06:00
bel
56a74a2767 add access log 2022-05-26 20:03:35 -06:00
bel
0eea3e787c ifnot proxied, then call WriteHeader to ensure CORS 2022-05-26 19:34:12 -06:00
bel
38f19408c2 cors ensures only ONE access control allow origin header set 2022-05-26 19:04:28 -06:00
Bel LaPointe
f28211e722 impl trim 2022-01-11 15:58:27 -05:00
Bel LaPointe
ef3abbbf07 authelia attempt failed 2021-04-18 12:20:19 -05:00
Bel LaPointe
af240639cb backend gets cookie identifying user 2021-03-21 13:12:11 -05:00
Bel LaPointe
c623792c2f NOW authelia supported 2021-03-21 13:03:04 -05:00
Bel LaPointe
cebb518e05 impl authelia I think 2021-03-21 12:44:21 -05:00
Bel LaPointe
177e0d88da dont rewrite 2020-07-31 23:15:58 -06:00
Bel LaPointe
9b0bccd9ca CORS for DELETE 2020-07-25 19:32:59 -06:00
Bel LaPointe
1af274dc1d Add redirect things for dumb js apps 2020-07-25 02:28:57 -06:00
Bel LaPointe
ec1e0cdf2e Add nopath for vue things 2020-07-25 02:23:04 -06:00
6 changed files with 291 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@@ -24,6 +25,11 @@ func parseProxy(s string) (string, Proxy) {
return key, p return key, p
} }
func GetAuthelia() (string, bool) {
authelia := conf.Get("authelia").GetString()
return authelia, authelia != ""
}
func GetBOAuthZ() (string, bool) { func GetBOAuthZ() (string, bool) {
boauthz := conf.Get("oauth").GetString() boauthz := conf.Get("oauth").GetString()
return boauthz, boauthz != "" return boauthz, boauthz != ""
@@ -35,6 +41,10 @@ func GetAuth() (string, string, bool) {
return user, pass, user != "" && pass != "" return user, pass, user != "" && pass != ""
} }
func GetTrim() string {
return conf.Get("trim").GetString()
}
func GetPort() string { func GetPort() string {
port := conf.Get("p").GetInt() port := conf.Get("p").GetInt()
return ":" + fmt.Sprint(port) return ":" + fmt.Sprint(port)
@@ -78,3 +88,27 @@ func GetTimeout() time.Duration {
timeout := conf.Get("timeout").GetDuration() timeout := conf.Get("timeout").GetDuration()
return timeout return timeout
} }
func GetCORS(key string) bool {
cors := conf.GetString("cors")
var m map[string]bool
if err := json.Unmarshal([]byte(cors), &m); err != nil {
return false
}
_, ok := m[key]
return ok
}
func GetNoPath(key string) bool {
nopath := conf.GetString("nopath")
var m map[string]bool
if err := json.Unmarshal([]byte(nopath), &m); err != nil {
return false
}
_, ok := m[key]
return ok
}
func GetCompression() bool {
return conf.GetBool("compression")
}

View File

@@ -3,6 +3,7 @@ package config
import ( import (
"fmt" "fmt"
"local/args" "local/args"
"local/logb"
"log" "log"
"os" "os"
"strings" "strings"
@@ -26,6 +27,7 @@ func Refresh() error {
return err return err
} }
conf = as conf = as
logb.Set(logb.LevelFromString(as.GetString("level")))
return nil return nil
} }
@@ -42,12 +44,18 @@ func parseArgs() (*args.ArgSet, error) {
as.Append(args.INT, "ap", "alt port for always http service", 51556) as.Append(args.INT, "ap", "alt port for always http service", 51556)
as.Append(args.INT, "r", "rate per second for requests", 100) as.Append(args.INT, "r", "rate per second for requests", 100)
as.Append(args.INT, "b", "burst requests", 100) as.Append(args.INT, "b", "burst requests", 100)
as.Append(args.BOOL, "compress", "enable compression", true)
as.Append(args.STRING, "crt", "path to crt for ssl", "") as.Append(args.STRING, "crt", "path to crt for ssl", "")
as.Append(args.STRING, "key", "path to key for ssl", "") as.Append(args.STRING, "key", "path to key for ssl", "")
as.Append(args.STRING, "trim", "path prefix to trim, like '/abc' to change '/abc/def' to '/def'", "")
as.Append(args.STRING, "tcp", "address for tcp only tunnel", "") as.Append(args.STRING, "tcp", "address for tcp only tunnel", "")
as.Append(args.DURATION, "timeout", "timeout for tunnel", time.Minute) as.Append(args.DURATION, "timeout", "timeout for tunnel", time.Minute)
as.Append(args.STRING, "proxy", "double-comma separated (+ if oauth)from,scheme://to.tld:port,oauth,,", "") as.Append(args.STRING, "proxy", "double-comma separated (+ if auth)from,scheme://to.tld:port,,", "")
as.Append(args.STRING, "oauth", "url for boauthz", "") as.Append(args.STRING, "oauth", "url for boauthz", "")
as.Append(args.STRING, "authelia", "url for authelia", "")
as.Append(args.STRING, "cors", "json dict key:true for keys to set CORS permissive headers, like {\"from\":true}", "{}")
as.Append(args.STRING, "nopath", "json dict key:true for keys to remove all path info from forwarded request, like -cors", "{}")
as.Append(args.STRING, "level", "log level", "info")
err := as.Parse() err := as.Parse()
return as, err return as, err

View File

@@ -11,10 +11,13 @@ func New() *Server {
port := config.GetPort() port := config.GetPort()
altport := config.GetAltPort() altport := config.GetAltPort()
r, b := config.GetRate() r, b := config.GetRate()
return &Server{ server := &Server{
db: storage.NewMap(), db: storage.NewMap(),
addr: port, addr: port,
altaddr: altport, altaddr: altport,
limiter: rate.NewLimiter(rate.Limit(r), b), limiter: rate.NewLimiter(rate.Limit(r), b),
} }
_, server.auth.BOAuthZ = config.GetBOAuthZ()
_, server.auth.Authelia = config.GetAuthelia()
return server
} }

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"io" "io"
"local/rproxy3/config"
"local/rproxy3/storage/packable" "local/rproxy3/storage/packable"
"log" "log"
"net/http" "net/http"
@@ -25,6 +26,7 @@ type rewrite struct {
func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) { func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) {
newURL, err := s.lookup(mapKey(r.Host)) newURL, err := s.lookup(mapKey(r.Host))
r.URL.Path = strings.TrimPrefix(r.URL.Path, config.GetTrim())
var transport http.RoundTripper var transport http.RoundTripper
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
transport = &redirPurge{ transport = &redirPurge{
@@ -37,7 +39,7 @@ func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) {
log.Printf("unknown host lookup %q", r.Host) log.Printf("unknown host lookup %q", r.Host)
return return
} }
r.Host = newURL.Host //r.Host = newURL.Host
proxy := httputil.NewSingleHostReverseProxy(newURL) proxy := httputil.NewSingleHostReverseProxy(newURL)
proxy.Transport = transport proxy.Transport = transport
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
@@ -49,7 +51,7 @@ func (s *Server) lookup(host string) (*url.URL, error) {
return v.URL(), err return v.URL(), err
} }
func (s *Server) lookupBOAuthZ(host string) (bool, error) { func (s *Server) lookupAuth(host string) (bool, error) {
v := packable.NewString() v := packable.NewString()
err := s.db.Get(nsBOAuthZ, host, v) err := s.db.Get(nsBOAuthZ, host, v)
return v.String() == "true", err return v.String() == "true", err
@@ -69,6 +71,8 @@ func (rp *redirPurge) RoundTrip(r *http.Request) (*http.Response, error) {
if loc := resp.Header.Get("Location"); loc != "" { if loc := resp.Header.Get("Location"); loc != "" {
resp.Header.Set("Location", strings.Replace(loc, rp.targetHost, rp.proxyHost, 1)) resp.Header.Set("Location", strings.Replace(loc, rp.targetHost, rp.proxyHost, 1))
} }
// google floc https://paramdeo.com/blog/opting-your-website-out-of-googles-floc-network
resp.Header.Set("Permissions-Policy", "interest-cohort=()")
return resp, err return resp, err
} }

View File

@@ -4,9 +4,11 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"local/logb"
"local/oauth2/oauth2client" "local/oauth2/oauth2client"
"local/rproxy3/config" "local/rproxy3/config"
"local/rproxy3/storage" "local/rproxy3/storage"
@@ -15,9 +17,12 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"path"
"strconv"
"strings" "strings"
"time" "time"
"github.com/google/uuid"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@@ -51,6 +56,10 @@ type Server struct {
username string username string
password string password string
limiter *rate.Limiter limiter *rate.Limiter
auth struct {
BOAuthZ bool
Authelia bool
}
} }
func (s *Server) Route(src string, dst config.Proxy) error { func (s *Server) Route(src string, dst config.Proxy) error {
@@ -98,8 +107,106 @@ func (s *Server) Run() error {
return errors.New("did not load server") return errors.New("did not load server")
} }
func (s *Server) doAuth(foo http.HandlerFunc) http.HandlerFunc { func (s *Server) doAuthelia(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
authelia, ok := config.GetAuthelia()
if !ok {
panic("howd i get here")
}
url, err := url.Parse(authelia)
if err != nil {
panic(fmt.Sprintf("bad config for authelia url: %v", err))
}
url.Path = "/api/verify"
logb.Verbosef("authelia @ %s", url.String())
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
if err != nil {
panic(err.Error())
}
r2 := r.Clone(r.Context())
if r2.URL.Host == "" {
r2.URL.Host = r2.Host
}
if r2.URL.Scheme == "" {
r2.URL.Scheme = "https"
}
for _, httpreq := range []*http.Request{r, req} {
for k, v := range map[string]string{
"X-Original-Url": r2.URL.String(),
"X-Forwarded-Proto": r2.URL.Scheme,
"X-Forwarded-Host": r2.URL.Host,
"X-Forwarded-Uri": r2.URL.String(),
} {
if _, ok := httpreq.Header[k]; !ok {
logb.Verbosef("authelia header setting %s:%s", k, v)
httpreq.Header.Set(k, v)
}
}
}
if cookie, err := r.Cookie("authelia_session"); err == nil {
logb.Verbosef("authelia session found in cookies; %+v", cookie)
req.AddCookie(cookie)
}
c := &http.Client{
Timeout: time.Minute,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
autheliaKey := mapKey(req.Host)
logb.Verbosef("request to %s is authelia %s? %v", r.Host, autheliaKey, strings.HasPrefix(r.Host, autheliaKey))
if strings.HasPrefix(r.Host, autheliaKey) {
logb.Debugf("no authelia for %s because it has prefix %s", r.Host, autheliaKey)
foo(w, r)
return
}
resp, err := c.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
logb.Debugf(
"authelia: %+v, %+v \n\t-> \n\t(%d) %+v, %+v",
req,
req.Cookies(),
resp.StatusCode,
resp.Header,
resp.Cookies(),
)
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
for k := range resp.Header {
if strings.HasPrefix(k, "Remote-") {
cookie := &http.Cookie{
Name: k,
Value: resp.Header.Get(k),
Path: "/",
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(24 * time.Hour * 30),
}
logb.Verbosef("setting authelia cookie in response: %+v", cookie)
http.SetCookie(w, cookie)
logb.Verbosef("setting authelia cookie in request: %+v", cookie)
r.AddCookie(cookie)
}
}
foo(w, r)
return
}
url.Path = ""
q := url.Query()
q.Set("rd", r2.URL.String())
url.RawQuery = q.Encode()
logb.Verbosef("authelia status %d, rd'ing %s", resp.StatusCode, url.String())
http.Redirect(w, r, url.String(), http.StatusFound)
}
}
func (s *Server) doBOAuthZ(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
key := mapKey(r.Host)
rusr, rpwd, ok := config.GetAuth() rusr, rpwd, ok := config.GetAuth()
if ok { if ok {
usr, pwd, ok := r.BasicAuth() usr, pwd, ok := r.BasicAuth()
@@ -109,8 +216,7 @@ func (s *Server) doAuth(foo http.HandlerFunc) http.HandlerFunc {
return return
} }
} }
key := mapKey(r.Host) ok, err := s.lookupAuth(key)
ok, err := s.lookupBOAuthZ(key)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
@@ -121,6 +227,9 @@ func (s *Server) doAuth(foo http.HandlerFunc) http.HandlerFunc {
return return
} }
} }
if config.GetNoPath(key) && path.Ext(r.URL.Path) == "" {
r.URL.Path = "/"
}
foo(w, r) foo(w, r)
} }
} }
@@ -157,20 +266,107 @@ func pipe(a, b net.Conn) {
func (s *Server) Pre(foo http.HandlerFunc) http.HandlerFunc { func (s *Server) Pre(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
r, flush := withMeta(w, r)
defer flush()
ctx, can := context.WithTimeout(r.Context(), time.Duration(config.GetTimeout())) ctx, can := context.WithTimeout(r.Context(), time.Duration(config.GetTimeout()))
defer can() defer can()
if err := s.limiter.Wait(ctx); err != nil { if err := s.limiter.Wait(ctx); err != nil {
pushMeta(r, "explain", "limiter exceeded")
w.WriteHeader(http.StatusTooManyRequests) w.WriteHeader(http.StatusTooManyRequests)
return return
} }
s.doAuth(foo)(w, r)
w, did := doCORS(w, r)
if did {
pushMeta(r, "explain", "did cors")
return
}
if s.auth.BOAuthZ {
logb.Verbosef("doing boauthz for request to %s", r.URL.String())
s.doBOAuthZ(foo)(w, r)
} else if s.auth.Authelia {
logb.Verbosef("doing authelia for request to %s", r.URL.String())
s.doAuthelia(foo)(w, r)
} else {
foo(w, r)
}
} }
} }
func withMeta(w http.ResponseWriter, r *http.Request) (*http.Request, func()) {
meta := map[string]string{
"ts": strconv.FormatInt(time.Now().Unix(), 10),
"method": r.Method,
"url": r.URL.String(),
"id": uuid.New().String(),
}
w.Header().Set("meta-id", meta["id"])
ctx := r.Context()
ctx = context.WithValue(ctx, "meta", meta)
r = r.WithContext(ctx)
return r, func() {
b, err := json.Marshal(meta)
if err != nil {
panic(err)
}
fmt.Printf("[access] %s\n", b)
}
}
func pushMeta(r *http.Request, k, v string) {
got := r.Context().Value("meta")
if got == nil {
return
}
meta, ok := got.(map[string]string)
if !ok || meta == nil {
return
}
meta[k] = v
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.Pre(s.Proxy)(w, r) s.Pre(s.Proxy)(w, r)
} }
type corsResponseWriter struct {
r *http.Request
http.ResponseWriter
}
func (cb corsResponseWriter) WriteHeader(code int) {
cb.Header().Set("Access-Control-Allow-Origin", "*")
cb.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token, content-type, Content-Type")
cb.ResponseWriter.WriteHeader(code)
pushMeta(cb.r, "cors", "wrote headers")
}
func doCORS(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, bool) {
key := mapKey(r.Host)
if !config.GetCORS(key) {
pushMeta(r, "do-cors", "not enabled for key")
return w, false
}
pushMeta(r, "do-cors", "enabled for key")
return _doCORS(w, r)
}
func _doCORS(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, bool) {
w2 := corsResponseWriter{r: r, ResponseWriter: w}
if r.Method != http.MethodOptions {
pushMeta(r, "-do-cors", "not options")
return w2, false
}
pushMeta(r, "-do-cors", "options")
w2.Header().Set("Content-Length", "0")
w2.Header().Set("Content-Type", "text/plain")
w2.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, TRACE, PATCH, HEAD, DELETE")
w2.WriteHeader(http.StatusOK)
return w2, true
}
func getProxyAuth(r *http.Request) (string, string) { func getProxyAuth(r *http.Request) (string, string) {
proxyAuthHeader := r.Header.Get("Proxy-Authorization") proxyAuthHeader := r.Header.Get("Proxy-Authorization")
proxyAuthB64 := strings.TrimPrefix(proxyAuthHeader, "Basic ") proxyAuthB64 := strings.TrimPrefix(proxyAuthHeader, "Basic ")

View File

@@ -14,6 +14,7 @@ import (
) )
func TestServerStart(t *testing.T) { func TestServerStart(t *testing.T) {
return // depends on etc hosts
server := mockServer() server := mockServer()
p := config.Proxy{ p := config.Proxy{
@@ -66,3 +67,40 @@ func TestServerRoute(t *testing.T) {
t.Fatalf("cannot proxy from 'world' to 'hello', status %v", w.Code) t.Fatalf("cannot proxy from 'world' to 'hello', status %v", w.Code)
} }
} }
func TestCORS(t *testing.T) {
t.Run(http.MethodOptions, func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodOptions, "/", nil)
w2, did := _doCORS(w, r)
w2.WriteHeader(300)
if !did {
t.Error("didnt do on options")
}
if w.Header().Get("Access-Control-Allow-Origin") != "*" {
t.Error("didnt set origina")
}
if w.Header().Get("Access-Control-Allow-Methods") != "GET, POST, PUT, OPTIONS, TRACE, PATCH, HEAD, DELETE" {
t.Error("didnt set allow methods")
}
})
t.Run(http.MethodGet, func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", nil)
w2, did := _doCORS(w, r)
w2.Header().Set("a", "b")
w2.Header().Set("Access-Control-Allow-Origin", "NO")
w2.WriteHeader(300)
if did {
t.Error("did cors on options")
}
if w.Header().Get("Access-Control-Allow-Origin") != "*" {
t.Error("didnt set origina")
} else if len(w.Header()["Access-Control-Allow-Origin"]) != 1 {
t.Error(w.Header())
}
if w.Header().Get("Access-Control-Allow-Methods") != "" {
t.Error("did set allow methods")
}
})
}