Compare commits

...

39 Commits
v0.5 ... master

Author SHA1 Message Date
bel 057a0ab341 to bel.blue 2025-11-20 17:56:06 -07:00
Bel LaPointe b6422eb0c0 support tcp AND ssl 2025-11-20 17:15:22 -07:00
Bel LaPointe 4a0e3c15e9 mto bel.blue thanks render 2025-11-20 17:14:50 -07:00
bel 3b53ef938d build.sh 2025-11-20 08:47:54 -07:00
bel 40d95d5b63 fix tests 2025-11-20 08:29:11 -07:00
bel 2ff12869cd impl cidr as password:CIDR:http://target 2025-11-20 08:28:25 -07:00
bel 9791f80b28 shhhh 2025-06-19 17:03:01 -06:00
bel 8d018aa236 oh 2025-06-19 16:56:52 -06:00
bel 00eda1e419 hrm 2025-06-19 16:52:36 -06:00
bel 5a0f567da3 _ lists 2025-06-19 16:49:06 -06:00
bel e4451923e9 whatevs 2024-12-21 13:50:53 -07:00
bel 4211b238c5 X-Forwarded-Proto 2024-12-21 13:48:57 -07:00
bel 5ab41100fb Merge branch 'master' of https://gitea.inhome.blapointe.com/local/rproxy3 2024-12-21 13:44:47 -07:00
bel 66f2a4df94 passthrough basic auth if not configred at rproxy3 level 2024-04-01 06:46:27 -06:00
Bel LaPointe dee04b6962 ewwww broke passthrough basic auth 2024-04-01 06:45:31 -06:00
Bel LaPointe 87d95b4eff Merge branch 'master' of https://gitea.inhome.blapointe.com/local/rproxy3 2024-04-01 06:44:15 -06:00
bel f083763f1d there we go 2024-03-10 11:17:48 -06:00
bel abf628d2bb proxy2 does auth inline 2024-03-10 11:08:51 -06:00
bel fb6d7af6d3 from gogs to gitea 2024-03-10 09:54:05 -06:00
bel 9941706b73 trivial 2024-03-10 09:51:29 -06:00
Bel LaPointe 669f3283f4 from gogs 2024-01-09 08:05:16 -07:00
Bel LaPointe fb30cc8436 to gogs.inhome 2023-04-10 11:18:10 -06:00
bel c8330aab26 NOW go mod 2022-10-25 22:42:05 -06:00
bel 32891c518c go mod 2022-10-25 22:40:00 -06:00
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
Bel LaPointe 61811e8e61 Listen on second port and redirect to main 2020-02-14 14:57:26 -07:00
bel c4c37068f3 New oauth2client 2019-12-31 11:21:15 -07:00
22 changed files with 476 additions and 398 deletions

View File

@ -1,115 +0,0 @@
package config
import (
"local/rproxy3/storage/packable"
"log"
"strconv"
"strings"
)
func GetPort() string {
v := packable.NewString()
conf.Get(nsConf, flagPort, v)
return ":" + strings.TrimPrefix(v.String(), ":")
}
func GetRoutes() map[string]string {
v := packable.NewString()
conf.Get(nsConf, flagRoutes, v)
m := make(map[string]string)
for _, v := range strings.Split(v.String(), ",") {
if len(v) == 0 {
return m
}
from := v[:strings.Index(v, ":")]
to := v[strings.Index(v, ":")+1:]
m[from] = to
}
return m
}
func GetTCP() (string, bool) {
v := packable.NewString()
conf.Get(nsConf, flagTCP, v)
tcpAddr := v.String()
return tcpAddr, notEmpty(tcpAddr)
}
func GetSSL() (string, string, bool) {
v := packable.NewString()
conf.Get(nsConf, flagCert, v)
certPath := v.String()
conf.Get(nsConf, flagKey, v)
keyPath := v.String()
return certPath, keyPath, notEmpty(certPath, keyPath)
}
func GetAuth() (string, string, bool) {
v := packable.NewString()
conf.Get(nsConf, flagUser, v)
user := v.String()
conf.Get(nsConf, flagPass, v)
pass := v.String()
return user, pass, notEmpty(user, pass)
}
func notEmpty(s ...string) bool {
for i := range s {
if s[i] == "" || s[i] == "/dev/null" {
return false
}
}
return true
}
func GetRate() (int, int) {
r := packable.NewString()
conf.Get(nsConf, flagRate, r)
b := packable.NewString()
conf.Get(nsConf, flagBurst, b)
rate, err := strconv.Atoi(r.String())
if err != nil {
log.Printf("illegal rate: %v", err)
rate = 5
}
burst, _ := strconv.Atoi(b.String())
if err != nil {
log.Printf("illegal burst: %v", err)
burst = 5
}
return rate, burst
}
func GetTimeout() int {
t := packable.NewString()
conf.Get(nsConf, flagTimeout, t)
timeout, err := strconv.Atoi(t.String())
if err != nil || timeout == 5 {
return 5
}
return timeout
}
func GetRewrites(hostMatch string) map[string]string {
v := packable.NewString()
conf.Get(nsConf, flagRewrites, v)
m := make(map[string]string)
for _, v := range strings.Split(v.String(), ",") {
vs := strings.Split(v, ":")
if len(v) < 3 {
continue
}
host := vs[0]
if host != hostMatch {
continue
}
from := vs[1]
to := strings.Join(vs[2:], ":")
m[from] = to
}
return m
}

View File

@ -1,161 +0,0 @@
package config
import (
"flag"
"io/ioutil"
"local/rproxy3/storage"
"local/rproxy3/storage/packable"
"log"
"os"
"strings"
yaml "gopkg.in/yaml.v2"
)
const nsConf = "configuration"
const flagPort = "p"
const flagRoutes = "r"
const flagConf = "c"
const flagCert = "crt"
const flagTCP = "tcp"
const flagKey = "key"
const flagUser = "user"
const flagPass = "pass"
const flagRate = "rate"
const flagBurst = "burst"
const flagTimeout = "timeout"
const flagRewrites = "rw"
var conf = storage.NewMap()
type toBind struct {
flag string
value *string
}
type fileConf struct {
Port string `yaml:"p"`
Routes []string `yaml:"r"`
CertPath string `yaml:"crt"`
TCPPath string `yaml:"tcp"`
KeyPath string `yaml:"key"`
Username string `yaml:"user"`
Password string `yaml:"pass"`
Rate string `yaml:"rate"`
Burst string `yaml:"burst"`
Timeout string `yaml:"timeout"`
Rewrites []string `yaml:"rw"`
}
func Init() error {
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
log.SetFlags(log.Ltime | log.Lshortfile)
if err := fromFile(); err != nil {
return err
}
if err := fromFlags(); err != nil {
return err
}
return nil
}
func fromFile() error {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
defer func() {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
}()
flag.String(flagConf, "/dev/null", "yaml config file path")
flag.Parse()
confFlag := flag.Lookup(flagConf)
if confFlag == nil || confFlag.Value.String() == "" {
return nil
}
confBytes, err := ioutil.ReadFile(confFlag.Value.String())
if err != nil {
return err
}
var c fileConf
if err := yaml.Unmarshal(confBytes, &c); err != nil {
return err
}
if err := conf.Set(nsConf, flagPort, packable.NewString(c.Port)); err != nil {
return err
}
if err := conf.Set(nsConf, flagRoutes, packable.NewString(strings.Join(c.Routes, ","))); err != nil {
return err
}
if err := conf.Set(nsConf, flagCert, packable.NewString(c.CertPath)); err != nil {
return err
}
if err := conf.Set(nsConf, flagTCP, packable.NewString(c.TCPPath)); err != nil {
return err
}
if err := conf.Set(nsConf, flagKey, packable.NewString(c.KeyPath)); err != nil {
return err
}
if err := conf.Set(nsConf, flagUser, packable.NewString(c.Username)); err != nil {
return err
}
if err := conf.Set(nsConf, flagPass, packable.NewString(c.Password)); err != nil {
return err
}
if err := conf.Set(nsConf, flagRate, packable.NewString(c.Rate)); err != nil {
return err
}
if err := conf.Set(nsConf, flagBurst, packable.NewString(c.Burst)); err != nil {
return err
}
if err := conf.Set(nsConf, flagTimeout, packable.NewString(c.Timeout)); err != nil {
return err
}
if err := conf.Set(nsConf, flagRewrites, packable.NewString(strings.Join(c.Rewrites, ","))); err != nil {
return err
}
return nil
}
func fromFlags() error {
binds := make([]toBind, 0)
binds = append(binds, addFlag(flagPort, "51555", "port to bind to"))
binds = append(binds, addFlag(flagConf, "", "configuration file path"))
binds = append(binds, addFlag(flagRoutes, "", "comma-separated routes to map, each as from:scheme://to.tld:port"))
binds = append(binds, addFlag(flagCert, "", "path to .crt"))
binds = append(binds, addFlag(flagTCP, "", "tcp addr"))
binds = append(binds, addFlag(flagKey, "", "path to .key"))
binds = append(binds, addFlag(flagUser, "", "basic auth username"))
binds = append(binds, addFlag(flagPass, "", "basic auth password"))
binds = append(binds, addFlag(flagRate, "100", "rate limit per second"))
binds = append(binds, addFlag(flagBurst, "100", "rate limit burst"))
binds = append(binds, addFlag(flagTimeout, "30", "seconds to wait for limiter"))
binds = append(binds, addFlag(flagRewrites, "", "comma-separated from:replace:replacement:oauth to rewrite in response bodies"))
flag.Parse()
for _, bind := range binds {
confFlag := flag.Lookup(bind.flag)
if confFlag == nil || confFlag.Value.String() == "" {
continue
}
if err := conf.Set(nsConf, bind.flag, packable.NewString(*bind.value)); err != nil {
return err
}
}
return nil
}
func addFlag(key, def, help string) toBind {
def = getFlagOrDefault(key, def)
v := flag.String(key, def, help)
return toBind{
flag: key,
value: v,
}
}
func getFlagOrDefault(key, def string) string {
v := packable.NewString()
if err := conf.Get(nsConf, key, v); err != nil {
return def
}
return v.String()
}

View File

@ -1,46 +0,0 @@
package config
import (
"flag"
"os"
"testing"
)
func TestInit(t *testing.T) {
was := os.Args[:]
os.Args = []string{"program"}
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
defer func() {
os.Args = was[:]
}()
if err := Init(); err != nil {
t.Errorf("failed to init: %v", err)
}
}
func TestFromFile(t *testing.T) {
was := os.Args[:]
os.Args = []string{"program"}
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
defer func() {
os.Args = was[:]
}()
if err := fromFile(); err != nil {
t.Errorf("failed from file: %v", err)
}
}
func TestFromFlags(t *testing.T) {
was := os.Args[:]
os.Args = []string{"program"}
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
defer func() {
os.Args = was[:]
}()
if err := fromFlags(); err != nil {
t.Errorf("failed from flags: %v", err)
}
}

0
Dockerfile Normal file → Executable file
View File

5
build.sh Normal file
View File

@ -0,0 +1,5 @@
#! /usr/bin/env bash
export CGO_ENABLED=1
export CC=x86_64-linux-musl-gcc
exec go build -ldflags="-linkmode external -extldflags '-static'" -o exec-rproxy3

72
config/config.go Normal file → Executable file
View File

@ -1,17 +1,23 @@
package config
import (
"encoding/json"
"fmt"
"log"
"regexp"
"strings"
"time"
"gopkg.in/yaml.v2"
)
type Proxy struct {
To string
Auth string
From string
To string
}
func parseProxy(s string) (string, Proxy) {
func parseOneProxyCSV(s string) (string, Proxy) {
p := Proxy{}
key := ""
l := strings.Split(s, ",")
@ -24,22 +30,26 @@ func parseProxy(s string) (string, Proxy) {
return key, p
}
func GetBOAuthZ() (string, bool) {
boauthz := conf.Get("oauth").GetString()
return boauthz, boauthz != ""
}
func GetAuth() (string, string, bool) {
user := conf.Get("user").GetString()
pass := conf.Get("pass").GetString()
return user, pass, user != "" && pass != ""
}
func GetTrim() string {
return conf.Get("trim").GetString()
}
func GetPort() string {
port := conf.Get("p").GetInt()
return ":" + fmt.Sprint(port)
}
func GetAltPort() string {
port := conf.Get("ap").GetInt()
return ":" + fmt.Sprint(port)
}
func GetRate() (int, int) {
rate := conf.Get("r").GetInt()
burst := conf.Get("b").GetInt()
@ -48,11 +58,31 @@ func GetRate() (int, int) {
}
func GetRoutes() map[string]Proxy {
list := conf.Get("proxy").GetString()
s := conf.Get("proxy2").GetString()
var dict map[string]string
if err := yaml.Unmarshal([]byte(s), &dict); err == nil && len(s) > 0 {
pattern := regexp.MustCompile(`(([^:]*):)?(([^:]*):)?([a-z0-9]*:.*)`)
result := map[string]Proxy{}
for k, v := range dict {
submatches := pattern.FindAllStringSubmatch(v, -1)
log.Printf("%+v", submatches)
result[k] = Proxy{
Auth: submatches[0][2],
From: submatches[0][4],
To: submatches[0][5],
}
}
return result
}
return getRoutesCSV()
}
func getRoutesCSV() map[string]Proxy {
list := conf.Get("proxy2").GetString()
definitions := strings.Split(list, ",,")
routes := make(map[string]Proxy)
for _, definition := range definitions {
k, v := parseProxy(definition)
k, v := parseOneProxyCSV(definition)
routes[k] = v
}
return routes
@ -73,3 +103,27 @@ func GetTimeout() time.Duration {
timeout := conf.Get("timeout").GetDuration()
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")
}

14
config/new.go Normal file → Executable file
View File

@ -2,11 +2,13 @@ package config
import (
"fmt"
"local/args"
"log"
"os"
"strings"
"time"
"gitea.bel.blue/local/args"
"gitea.bel.blue/local/logb"
)
var conf *args.ArgSet
@ -26,6 +28,7 @@ func Refresh() error {
return err
}
conf = as
logb.Set(logb.LevelFromString(as.GetString("level")))
return nil
}
@ -39,14 +42,19 @@ func parseArgs() (*args.ArgSet, error) {
as.Append(args.STRING, "user", "username for basic auth", "")
as.Append(args.STRING, "pass", "password for basic auth", "")
as.Append(args.INT, "p", "port for service", 51555)
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, "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, "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.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, "oauth", "url for boauthz", "")
as.Append(args.STRING, "proxy2", "double-comma separated 'from,scheme://to.tld:port,,' OR a yaml dictionary of 'from: (password:)scheme://to.tld:port'", "")
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()
return as, err

View File

@ -7,5 +7,7 @@ crt: ""
key: ""
tcp: ""
timeout: 1m
proxy: a,http://localhost:41912,,+b,http://localhost:41912
proxy2: |
a: http://localhost:41912
b: password:http://localhost:41912
oauth: http://localhost:23456

17
go.mod Normal file
View File

@ -0,0 +1,17 @@
module gitea.bel.blue/local/rproxy3
go 1.18
require (
gitea.bel.blue/local/args v0.0.0-20251121001304-83c57f856714
gitea.bel.blue/local/logb v0.0.0-20251121001353-d45d53fbaae9
github.com/google/uuid v1.3.0
golang.org/x/time v0.1.0
)
require gopkg.in/yaml.v2 v2.4.0
require (
github.com/kr/pretty v0.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

18
go.sum Normal file
View File

@ -0,0 +1,18 @@
gitea.bel.blue/local/args v0.0.0-20251121001304-83c57f856714 h1:JHV86INH1QmPJoyIhdrDLJq7OKta+fJAwbK0pnxI4Hc=
gitea.bel.blue/local/args v0.0.0-20251121001304-83c57f856714/go.mod h1:GCzui3GPhOgKgGYNqtW55YkI3vIWCQEHPydGjFhaXV0=
gitea.bel.blue/local/logb v0.0.0-20251121001353-d45d53fbaae9 h1:lBkQPYgWZnPxt6CvsSwVh9EZtuvi2lIbGOHPqe/gn1Y=
gitea.bel.blue/local/logb v0.0.0-20251121001353-d45d53fbaae9/go.mod h1:+8sJb8UksdadKy43czL7/3TcfBwCkuYT6hFY+RaxP48=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -1,8 +1,8 @@
package main
import (
"local/rproxy3/config"
"local/rproxy3/server"
"gitea.bel.blue/local/rproxy3/config"
"gitea.bel.blue/local/rproxy3/server"
)
func main() {

View File

@ -34,7 +34,7 @@ func TestHTTPSMain(t *testing.T) {
"username",
"-pass",
"password",
"-proxy",
"-proxy2",
"hello," + addr,
"-crt",
"./testdata/rproxy3server.crt",
@ -89,7 +89,7 @@ func TestHTTPMain(t *testing.T) {
"username",
"-pass",
"password",
"-proxy",
"-proxy2",
"hello," + addr,
}
main()

View File

@ -1,18 +1,21 @@
package server
import (
"local/rproxy3/config"
"local/rproxy3/storage"
"gitea.bel.blue/local/rproxy3/config"
"gitea.bel.blue/local/rproxy3/storage"
"golang.org/x/time/rate"
)
func New() *Server {
port := config.GetPort()
altport := config.GetAltPort()
r, b := config.GetRate()
return &Server{
server := &Server{
db: storage.NewMap(),
addr: port,
altaddr: altport,
limiter: rate.NewLimiter(rate.Limit(r), b),
}
return server
}

View File

@ -4,12 +4,14 @@ import (
"bytes"
"crypto/tls"
"io"
"local/rproxy3/storage/packable"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"gitea.bel.blue/local/rproxy3/config"
"gitea.bel.blue/local/rproxy3/storage/packable"
)
type redirPurge struct {
@ -25,6 +27,7 @@ type rewrite struct {
func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) {
newURL, err := s.lookup(mapKey(r.Host))
r.URL.Path = strings.TrimPrefix(r.URL.Path, config.GetTrim())
var transport http.RoundTripper
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
transport = &redirPurge{
@ -37,7 +40,7 @@ func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) {
log.Printf("unknown host lookup %q", r.Host)
return
}
r.Host = newURL.Host
//r.Host = newURL.Host
proxy := httputil.NewSingleHostReverseProxy(newURL)
proxy.Transport = transport
proxy.ServeHTTP(w, r)
@ -49,10 +52,16 @@ func (s *Server) lookup(host string) (*url.URL, error) {
return v.URL(), err
}
func (s *Server) lookupBOAuthZ(host string) (bool, error) {
func (s *Server) lookupAuth(host string) (string, error) {
v := packable.NewString()
err := s.db.Get(nsBOAuthZ, host, v)
return v.String() == "true", err
err := s.db.Get(nsRouting, host+"//auth", v)
return v.String(), err
}
func (s *Server) lookupFrom(host string) (string, error) {
v := packable.NewString()
err := s.db.Get(nsRouting, host+"//from", v)
return v.String(), err
}
func mapKey(host string) string {
@ -69,6 +78,8 @@ func (rp *redirPurge) RoundTrip(r *http.Request) (*http.Response, error) {
if loc := resp.Header.Get("Location"); loc != "" {
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
}

View File

@ -1,7 +1,7 @@
package server
import (
"local/rproxy3/config"
"gitea.bel.blue/local/rproxy3/config"
)
func (s *Server) Routes() error {

View File

@ -4,32 +4,36 @@ import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"local/oauth2/oauth2client"
"local/rproxy3/config"
"local/rproxy3/storage"
"local/rproxy3/storage/packable"
"log"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"gitea.bel.blue/local/rproxy3/config"
"gitea.bel.blue/local/rproxy3/storage"
"gitea.bel.blue/local/rproxy3/storage/packable"
"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
schemeHTTP listenerScheme = iota
schemeHTTPS
schemeTCP
schemeTCPTLS
)
func (ls listenerScheme) String() string {
@ -40,6 +44,8 @@ func (ls listenerScheme) String() string {
return "https"
case schemeTCP:
return "tcp"
case schemeTCPTLS:
return "tcptls"
}
return ""
}
@ -47,38 +53,36 @@ func (ls listenerScheme) String() string {
type Server struct {
db storage.DB
addr string
altaddr string
username string
password string
limiter *rate.Limiter
}
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)))
if err := s.db.Set(nsRouting, src+"//from", packable.NewString(dst.From)); err != nil {
return err
}
if err := s.db.Set(nsRouting, src+"//auth", packable.NewString(dst.Auth)); err != nil {
return err
}
return s.db.Set(nsRouting, src, packable.NewURL(u))
}
func (s *Server) Run() error {
scheme := schemeHTTP
if _, _, ok := config.GetSSL(); ok {
scheme = schemeHTTPS
}
if _, ok := config.GetTCP(); ok {
scheme = schemeTCP
}
go s.alt()
scheme := getScheme()
log.Printf("Listening for %v on %v...\n", scheme, s.addr)
switch scheme {
case schemeHTTP:
log.Printf("Serve http")
return http.ListenAndServe(s.addr, s)
case schemeHTTPS:
log.Printf("Serve https")
c, k, _ := config.GetSSL()
httpsServer := &http.Server{
Addr: s.addr,
@ -98,38 +102,42 @@ func (s *Server) Run() error {
}
return httpsServer.ListenAndServeTLS(c, k)
case schemeTCP:
log.Printf("Serve tcp")
addr, _ := config.GetTCP()
return s.ServeTCP(addr)
case schemeTCPTLS:
addr, _ := config.GetTCP()
cert, key, _ := config.GetSSL()
return s.ServeTCPTLS(addr, cert, key)
}
return errors.New("did not load server")
}
func (s *Server) doAuth(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
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
}
}
key := mapKey(r.Host)
ok, err := s.lookupBOAuthZ(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
}
}
foo(w, r)
func (s *Server) ServeTCPTLS(addr, c, k string) error {
certificate, err := tls.LoadX509KeyPair(c, k)
if err != nil {
return err
}
certificates := []tls.Certificate{certificate}
listen, err := net.Listen("tcp", s.addr)
if err != nil {
return err
}
defer listen.Close()
config := &tls.Config{
Certificates: certificates,
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,
},
}
config.BuildNameToCertificate()
tlsListener := tls.NewListener(listen, config)
return s.serveTCP(addr, tlsListener)
}
func (s *Server) ServeTCP(addr string) error {
@ -137,6 +145,11 @@ func (s *Server) ServeTCP(addr string) error {
if err != nil {
return err
}
defer listen.Close()
return s.serveTCP(addr, listen)
}
func (s *Server) serveTCP(addr string, listen net.Listener) error {
for {
c, err := listen.Accept()
if err != nil {
@ -164,20 +177,168 @@ func pipe(a, b net.Conn) {
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
}
s.doAuth(foo)(w, r)
if r.URL.Scheme == "https" {
w.Header().Set("X-Forwarded-Proto", "https")
}
w, did := doCORS(w, r)
if did {
pushMeta(r, "explain", "did cors")
return
}
if mapKey(r.Host) == "_" {
s.List(w)
return
}
if auth, err := s.lookupAuth(mapKey(r.Host)); err != nil {
log.Printf("failed to lookup auth for %s (%s): %v", r.Host, mapKey(r.Host), err)
w.Header().Set("WWW-Authenticate", "Basic")
http.Error(w, err.Error(), http.StatusUnauthorized)
} else if _, p, _ := r.BasicAuth(); auth != "" && auth != p {
log.Printf("failed to auth: expected %q but got %q", auth, p)
w.Header().Set("WWW-Authenticate", "Basic")
http.Error(w, "unexpected basic auth", http.StatusUnauthorized)
} else if from, err := s.lookupFrom(mapKey(r.Host)); err != nil {
log.Printf("failed to lookup from for %s (%s): %v", r.Host, mapKey(r.Host), err)
http.Error(w, err.Error(), http.StatusBadGateway)
} else if err := assertFrom(from, r.RemoteAddr); err != nil {
log.Printf("failed to from: expected %q but got %q: %v", from, r.RemoteAddr, err)
http.Error(w, "unexpected from", http.StatusUnauthorized)
} else {
foo(w, r)
}
}
}
func assertFrom(from, remoteAddr string) error {
if from == "" {
return nil
}
pattern := regexp.MustCompile(`[0-9](:[0-9]+)$`).FindStringSubmatchIndex(remoteAddr)
if len(pattern) == 4 {
remoteAddr = remoteAddr[:pattern[2]]
}
remoteIP := net.ParseIP(remoteAddr)
if remoteIP == nil {
return fmt.Errorf("cannot parse remote %q", remoteAddr)
}
_, net, err := net.ParseCIDR(from)
if err != nil {
panic(err)
}
if net.Contains(remoteIP) {
return nil
}
return fmt.Errorf("expected like %q but got like %q", from, remoteAddr)
}
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) {
s.Pre(s.Proxy)(w, r)
}
func (s *Server) List(w http.ResponseWriter) {
keys := s.db.Keys(nsRouting)
hostURL := map[string]string{}
hostFrom := map[string]string{}
for _, key := range keys {
u, _ := s.lookup(key)
if u != nil && strings.TrimSuffix(key, "//auth") == key {
hostURL[key] = u.String()
}
if u != nil && strings.TrimSuffix(key, "//from") == key {
hostFrom[key] = u.String()
}
}
json.NewEncoder(w).Encode(map[string]any{
"hostsToURLs": hostURL,
"hostsToFrom": hostFrom,
})
}
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) {
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 ")
@ -189,3 +350,43 @@ func getProxyAuth(r *http.Request) (string, string) {
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
_, _, ssl := config.GetSSL()
if ssl {
scheme = schemeHTTPS
}
if _, ok := config.GetTCP(); ok {
scheme = schemeTCP
if ssl {
scheme = schemeTCPTLS
}
}
return scheme
}

View File

@ -3,17 +3,19 @@ package server
import (
"context"
"fmt"
"local/rproxy3/config"
"local/rproxy3/storage"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.bel.blue/local/rproxy3/config"
"gitea.bel.blue/local/rproxy3/storage"
"golang.org/x/time/rate"
)
func TestServerStart(t *testing.T) {
return // depends on etc hosts
server := mockServer()
p := config.Proxy{
@ -66,3 +68,69 @@ func TestServerRoute(t *testing.T) {
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")
}
})
}
func TestAssertFrom(t *testing.T) {
cases := map[string]struct {
from string
remote string
err bool
}{
"empty": {},
"ipv6 localhost": {
from: "::1/128",
remote: "::1:12345",
},
"ipv4 localhost": {
from: "127.0.0.1/32",
remote: "127.0.0.1:12345",
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
err := assertFrom(c.from, c.remote)
got := err != nil
if got != c.err {
t.Errorf("expected err=%v but got %v", c.err, err)
}
})
}
}

View File

@ -2,7 +2,8 @@ package storage
import (
"errors"
"local/rproxy3/storage/packable"
"gitea.bel.blue/local/rproxy3/storage/packable"
)
var ErrNotFound = errors.New("not found")
@ -10,5 +11,6 @@ var ErrNotFound = errors.New("not found")
type DB interface {
Get(string, string, packable.Packable) error
Set(string, string, packable.Packable) error
Keys(string) []string
Close() error
}

View File

@ -1,9 +1,10 @@
package storage
import (
"local/rproxy3/storage/packable"
"os"
"testing"
"gitea.bel.blue/local/rproxy3/storage/packable"
)
func TestDB(t *testing.T) {

View File

@ -2,7 +2,8 @@ package storage
import (
"fmt"
"local/rproxy3/storage/packable"
"gitea.bel.blue/local/rproxy3/storage/packable"
)
type Map map[string]map[string][]byte
@ -40,6 +41,15 @@ func (m Map) Close() error {
return nil
}
func (m Map) Keys(ns string) []string {
m2, _ := m[ns]
result := make([]string, 0, len(m2))
for k := range m2 {
result = append(result, k)
}
return result
}
func (m Map) Get(ns, key string, value packable.Packable) error {
if _, ok := m[ns]; !ok {
m[ns] = make(map[string][]byte)

0
testdata/index.html vendored Normal file → Executable file
View File

0
testdata/ws.go vendored Normal file → Executable file
View File