417 lines
10 KiB
Go
Executable File
417 lines
10 KiB
Go
Executable File
package server
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"local/logb"
|
|
"local/oauth2/oauth2client"
|
|
"local/rproxy3/config"
|
|
"local/rproxy3/storage"
|
|
"local/rproxy3/storage/packable"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
const nsRouting = "routing"
|
|
const nsBOAuthZ = "oauth"
|
|
|
|
type listenerScheme int
|
|
|
|
const (
|
|
schemeHTTP listenerScheme = iota
|
|
schemeHTTPS listenerScheme = iota
|
|
schemeTCP listenerScheme = iota
|
|
)
|
|
|
|
func (ls listenerScheme) String() string {
|
|
switch ls {
|
|
case schemeHTTP:
|
|
return "http"
|
|
case schemeHTTPS:
|
|
return "https"
|
|
case schemeTCP:
|
|
return "tcp"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type Server struct {
|
|
db storage.DB
|
|
addr string
|
|
altaddr string
|
|
username string
|
|
password string
|
|
limiter *rate.Limiter
|
|
auth struct {
|
|
BOAuthZ bool
|
|
Authelia bool
|
|
}
|
|
}
|
|
|
|
func (s *Server) Route(src string, dst config.Proxy) error {
|
|
hasOAuth := strings.HasPrefix(src, "+")
|
|
src = strings.TrimPrefix(src, "+")
|
|
log.Printf("Adding route %q -> %v...\n", src, dst)
|
|
u, err := url.Parse(dst.To)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.db.Set(nsBOAuthZ, src, packable.NewString(fmt.Sprint(hasOAuth)))
|
|
return s.db.Set(nsRouting, src, packable.NewURL(u))
|
|
}
|
|
|
|
func (s *Server) Run() error {
|
|
go s.alt()
|
|
scheme := getScheme()
|
|
log.Printf("Listening for %v on %v...\n", scheme, s.addr)
|
|
switch scheme {
|
|
case schemeHTTP:
|
|
return http.ListenAndServe(s.addr, s)
|
|
case schemeHTTPS:
|
|
c, k, _ := config.GetSSL()
|
|
httpsServer := &http.Server{
|
|
Addr: s.addr,
|
|
Handler: s,
|
|
TLSConfig: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
|
PreferServerCipherSuites: true,
|
|
CipherSuites: []uint16{
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
},
|
|
},
|
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
|
|
}
|
|
return httpsServer.ListenAndServeTLS(c, k)
|
|
case schemeTCP:
|
|
addr, _ := config.GetTCP()
|
|
return s.ServeTCP(addr)
|
|
}
|
|
return errors.New("did not load server")
|
|
}
|
|
|
|
func (s *Server) doAuthelia(foo http.HandlerFunc) http.HandlerFunc {
|
|
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()
|
|
if ok {
|
|
usr, pwd, ok := r.BasicAuth()
|
|
if !ok || rusr != usr || rpwd != pwd {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
log.Printf("denying proxy basic auth")
|
|
return
|
|
}
|
|
}
|
|
ok, err := s.lookupAuth(key)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if url, exists := config.GetBOAuthZ(); ok && exists {
|
|
err := oauth2client.Authenticate(url, key, w, r)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
if config.GetNoPath(key) && path.Ext(r.URL.Path) == "" {
|
|
r.URL.Path = "/"
|
|
}
|
|
foo(w, r)
|
|
}
|
|
}
|
|
|
|
func (s *Server) ServeTCP(addr string) error {
|
|
listen, err := net.Listen("tcp", s.addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for {
|
|
c, err := listen.Accept()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go func(c net.Conn) {
|
|
d, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
go pipe(c, d)
|
|
go pipe(d, c)
|
|
}(c)
|
|
}
|
|
}
|
|
|
|
func pipe(a, b net.Conn) {
|
|
log.Println("open pipe")
|
|
defer log.Println("close pipe")
|
|
defer a.Close()
|
|
defer b.Close()
|
|
io.Copy(a, b)
|
|
}
|
|
|
|
func (s *Server) Pre(foo http.HandlerFunc) http.HandlerFunc {
|
|
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()))
|
|
defer can()
|
|
if err := s.limiter.Wait(ctx); err != nil {
|
|
pushMeta(r, "explain", "limiter exceeded")
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
log.Printf("[access] %s", 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) {
|
|
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) {
|
|
proxyAuthHeader := r.Header.Get("Proxy-Authorization")
|
|
proxyAuthB64 := strings.TrimPrefix(proxyAuthHeader, "Basic ")
|
|
proxyAuthBytes, _ := base64.StdEncoding.DecodeString(proxyAuthB64)
|
|
proxyAuth := string(proxyAuthBytes)
|
|
if !strings.Contains(proxyAuth, ":") {
|
|
return "", ""
|
|
}
|
|
proxyAuthSplit := strings.Split(proxyAuth, ":")
|
|
return proxyAuthSplit[0], proxyAuthSplit[1]
|
|
}
|
|
|
|
func (s *Server) alt() {
|
|
switch getScheme() {
|
|
case schemeHTTP:
|
|
case schemeHTTPS:
|
|
default:
|
|
return
|
|
}
|
|
foo := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
r.URL.Scheme = getScheme().String()
|
|
if hostname := r.URL.Hostname(); hostname != "" {
|
|
r.URL.Host = r.URL.Hostname() + s.addr
|
|
} else if hostname := r.URL.Host; hostname != "" {
|
|
r.URL.Host = r.URL.Host + s.addr
|
|
} else {
|
|
u := url.URL{Host: r.Host}
|
|
r.URL.Host = u.Hostname() + s.addr
|
|
}
|
|
http.Redirect(w, r, r.URL.String(), http.StatusSeeOther)
|
|
})
|
|
log.Println("redirecting from", s.altaddr)
|
|
if err := http.ListenAndServe(s.altaddr, foo); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func getScheme() listenerScheme {
|
|
scheme := schemeHTTP
|
|
if _, _, ok := config.GetSSL(); ok {
|
|
scheme = schemeHTTPS
|
|
}
|
|
if _, ok := config.GetTCP(); ok {
|
|
scheme = schemeTCP
|
|
}
|
|
return scheme
|
|
}
|