23 Commits
v0.1 ... v0.8.1

Author SHA1 Message Date
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
bel
d71b00e067 Fix 2019-11-06 19:00:56 -07:00
Bel LaPointe
d98703610d remove unused and rename 2019-11-03 07:56:43 -07:00
Bel LaPointe
01b7b06971 Optional oauth via + flag 2019-11-03 07:55:38 -07:00
Bel LaPointe
7d3d6d88f6 Re-enable config files 2019-11-03 07:45:23 -07:00
Bel LaPointe
8c415f2a39 Update to scoped oauth 2019-11-02 08:03:35 -06:00
Bel LaPointe
df0232e24c Create dockerfile from dockerize.do and test WS 2019-11-02 07:26:58 -06:00
bel
bc11dd7f82 change conf to argsset and flag for oauth 2019-10-22 05:03:53 +00:00
bel
e20ba5361d about to replace config and storage with shared package 2019-10-22 02:36:59 +00:00
Bel LaPointe
8a90a3adda Implement tcp proxy with single forward 2019-10-02 09:07:59 -06:00
Bel LaPointe
698edf7e45 insecure skip verify 2019-09-24 12:47:06 -06:00
Bel LaPointe
48e0048216 no more logger 2019-05-02 09:35:53 -06:00
Bel LaPointe
f58f6f7cf3 Support body rewrite 2019-04-10 10:47:30 -06:00
Bel LaPointe
f72ecc5e53 passes tests with no rewrites 2019-04-10 10:08:47 -06:00
Bel LaPointe
3bd1527b98 Fix tests 2019-04-10 09:52:54 -06:00
35 changed files with 910 additions and 251 deletions

115
.config/config.go Executable file
View File

@@ -0,0 +1,115 @@
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
}

161
.config/new.go Executable file
View File

@@ -0,0 +1,161 @@
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()
}

0
config/new_test.go → .config/new_test.go Normal file → Executable file
View File

4
.gitignore vendored Normal file → Executable file
View File

@@ -1,6 +1,10 @@
lz4
rclone
rcloner
exec
exec-*
**/exec
**/exec-*
Go
cloudly
dockfile

16
Dockerfile Executable file
View File

@@ -0,0 +1,16 @@
FROM golang:1.13-alpine as certs
RUN apk update && apk add --no-cache ca-certificates
FROM busybox:glibc
RUN mkdir -p /var/log
WORKDIR /main
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
COPY . .
ENV GOPATH=""
ENV MNT="/mnt/"
ENTRYPOINT ["/main/exec-rproxy3"]
CMD []

View File

@@ -1,11 +0,0 @@
p: 54243
r:
- echo:http://localhost:49982
- echo2:http://192.168.0.86:38090
#crt: ./testdata/rproxy3server.crt
#key: ./testdata/rproxy3server.key
#user: bel
#pass: bel
rate: 1
burst: 2
timeout: 10

BIN
config/.config.go.un~ Normal file

Binary file not shown.

BIN
config/.new.go.un~ Normal file

Binary file not shown.

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

@@ -1,76 +1,110 @@
package config
import (
"local/rproxy3/storage/packable"
"strconv"
"encoding/json"
"fmt"
"log"
"strings"
"time"
)
func GetPort() string {
v := packable.NewString()
conf.Get(nsConf, flagPort, v)
return ":" + strings.TrimPrefix(v.String(), ":")
type Proxy struct {
To 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
func parseProxy(s string) (string, Proxy) {
p := Proxy{}
key := ""
l := strings.Split(s, ",")
if len(l) > 0 {
key = l[0]
}
return m
if len(l) > 1 {
p.To = l[1]
}
return key, p
}
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 GetAuthelia() (string, bool) {
authelia := conf.Get("authelia").GetString()
return authelia, authelia != ""
}
func GetBOAuthZ() (string, bool) {
boauthz := conf.Get("oauth").GetString()
return boauthz, boauthz != ""
}
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)
user := conf.Get("user").GetString()
pass := conf.Get("pass").GetString()
return user, pass, user != "" && pass != ""
}
func notEmpty(s ...string) bool {
for i := range s {
if s[i] == "" || s[i] == "/dev/null" {
return false
}
}
return true
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) {
r := packable.NewString()
conf.Get(nsConf, flagRate, r)
b := packable.NewString()
conf.Get(nsConf, flagBurst, b)
rate, _ := strconv.Atoi(r.String())
burst, _ := strconv.Atoi(b.String())
rate := conf.Get("r").GetInt()
burst := conf.Get("b").GetInt()
log.Println("rate/burst:", rate, burst)
return rate, burst
}
func GetTimeout() int {
t := packable.NewString()
conf.Get(nsConf, flagTimeout, t)
func GetRoutes() map[string]Proxy {
list := conf.Get("proxy").GetString()
definitions := strings.Split(list, ",,")
routes := make(map[string]Proxy)
for _, definition := range definitions {
k, v := parseProxy(definition)
routes[k] = v
}
return routes
}
timeout, _ := strconv.Atoi(t.String())
func GetSSL() (string, string, bool) {
crt := conf.Get("crt").GetString()
key := conf.Get("key").GetString()
return crt, key, crt != "" && key != ""
}
func GetTCP() (string, bool) {
tcp := conf.Get("tcp").GetString()
return tcp, tcp != ""
}
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")
}

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

@@ -1,148 +1,61 @@
package config
import (
"flag"
"io/ioutil"
"local/rproxy3/storage"
"local/rproxy3/storage/packable"
"fmt"
"local/args"
"local/logb"
"log"
"os"
"strings"
yaml "gopkg.in/yaml.v2"
"time"
)
const nsConf = "configuration"
const flagPort = "p"
const flagRoutes = "r"
const flagConf = "c"
const flagCert = "crt"
const flagKey = "key"
const flagUser = "user"
const flagPass = "pass"
const flagRate = "rate"
const flagBurst = "burst"
const flagTimeout = "timeout"
var conf *args.ArgSet
var conf = storage.NewMap()
type toBind struct {
flag string
value *string
func init() {
if err := Refresh(); err != nil {
panic(err)
}
}
type fileConf struct {
Port string `yaml:"p"`
Routes []string `yaml:"r"`
CertPath string `yaml:"crt"`
KeyPath string `yaml:"key"`
Username string `yaml:"user"`
Password string `yaml:"pass"`
Rate string `yaml:"rate"`
Burst string `yaml:"burst"`
Timeout string `yaml:"timeout"`
}
func Init() error {
func Refresh() error {
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
if err := fromFile(); err != nil {
return err
}
if err := fromFlags(); err != nil {
log.SetFlags(log.Ltime | log.Lshortfile)
as, err := parseArgs()
if err != nil && !strings.Contains(fmt.Sprint(os.Args), "-test") {
return err
}
conf = as
logb.Set(logb.LevelFromString(as.GetString("level")))
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
func parseArgs() (*args.ArgSet, error) {
configFiles := []string{}
if v, ok := os.LookupEnv("CONFIG"); ok {
configFiles = strings.Split(v, ",")
}
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, 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
}
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(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"))
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()
as := args.NewArgSet(configFiles...)
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, "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 auth)from,scheme://to.tld:port,,", "")
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()
return as, err
}

11
example_config.yaml Executable file
View File

@@ -0,0 +1,11 @@
user: ""
pass: ""
port: 51555
r: 100
b: 100
crt: ""
key: ""
tcp: ""
timeout: 1m
proxy: a,http://localhost:41912,,+b,http://localhost:41912
oauth: http://localhost:23456

2
main.go Normal file → Executable file
View File

@@ -6,7 +6,7 @@ import (
)
func main() {
if err := config.Init(); err != nil {
if err := config.Refresh(); err != nil {
panic(err)
}

14
main_test.go Normal file → Executable file
View File

@@ -34,8 +34,8 @@ func TestHTTPSMain(t *testing.T) {
"username",
"-pass",
"password",
"-r",
"hello:" + addr,
"-proxy",
"hello," + addr,
"-crt",
"./testdata/rproxy3server.crt",
"-key",
@@ -51,7 +51,7 @@ func TestHTTPSMain(t *testing.T) {
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
r, _ := http.NewRequest("GET", "https://hello.localhost"+port, nil)
r, _ := http.NewRequest("GET", "https://hello.localhost:"+port, nil)
if resp, err := client.Do(r); err != nil {
t.Fatalf("client failed: %v", err)
@@ -89,8 +89,8 @@ func TestHTTPMain(t *testing.T) {
"username",
"-pass",
"password",
"-r",
"hello:" + addr,
"-proxy",
"hello," + addr,
}
main()
}()
@@ -98,7 +98,7 @@ func TestHTTPMain(t *testing.T) {
time.Sleep(time.Millisecond * 100)
client := &http.Client{}
r, _ := http.NewRequest("GET", "http://hello.localhost"+port, nil)
r, _ := http.NewRequest("GET", "http://hello.localhost:"+port, nil)
if resp, err := client.Do(r); err != nil {
t.Fatalf("client failed: %v", err)
@@ -127,5 +127,5 @@ func echoServer() (string, func()) {
func getPort() string {
s := httptest.NewServer(nil)
s.Close()
return s.URL[strings.LastIndex(s.URL, ":"):]
return s.URL[strings.LastIndex(s.URL, ":")+1:]
}

7
server/new.go Normal file → Executable file
View File

@@ -9,10 +9,15 @@ import (
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),
}
_, server.auth.BOAuthZ = config.GetBOAuthZ()
_, server.auth.Authelia = config.GetAuthelia()
return server
}

0
server/new_test.go Normal file → Executable file
View File

79
server/proxy.go Normal file → Executable file
View File

@@ -1,6 +1,9 @@
package server
import (
"bytes"
"crypto/tls"
"io"
"local/rproxy3/storage/packable"
"log"
"net/http"
@@ -10,37 +13,56 @@ import (
)
type redirPurge struct {
proxyHost string
targetHost string
proxyHost string
targetHost string
baseTransport http.RoundTripper
}
type rewrite struct {
rewrites map[string]string
baseTransport http.RoundTripper
}
func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) {
newURL, err := s.lookup(r.Host)
transport := &redirPurge{
proxyHost: r.Host,
targetHost: newURL.Host,
newURL, err := s.lookup(mapKey(r.Host))
var transport http.RoundTripper
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
transport = &redirPurge{
proxyHost: r.Host,
targetHost: newURL.Host,
baseTransport: http.DefaultTransport,
}
if err != nil {
http.NotFound(w, r)
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)
}
func (s *Server) lookup(host string) (*url.URL, error) {
host = strings.Split(host, ".")[0]
host = strings.Split(host, ":")[0]
v := packable.NewURL()
err := s.db.Get(nsRouting, host, v)
return v.URL(), err
}
func (s *Server) lookupAuth(host string) (bool, error) {
v := packable.NewString()
err := s.db.Get(nsBOAuthZ, host, v)
return v.String() == "true", err
}
func mapKey(host string) string {
host = strings.Split(host, ".")[0]
host = strings.Split(host, ":")[0]
return host
}
func (rp *redirPurge) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := http.DefaultTransport.RoundTrip(r)
resp, err := rp.baseTransport.RoundTrip(r)
if err != nil {
return resp, err
}
@@ -49,3 +71,40 @@ func (rp *redirPurge) RoundTrip(r *http.Request) (*http.Response, error) {
}
return resp, err
}
func (rw *rewrite) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := rw.baseTransport.RoundTrip(r)
if err != nil {
return resp, err
}
if len(rw.rewrites) == 0 {
return resp, err
}
resp.Header.Del("Content-Length")
pr, pw := io.Pipe()
body := resp.Body
resp.Body = pr
go func() {
buff := make([]byte, 1024)
n, err := body.Read(buff)
for err == nil || n > 0 {
chunk := buff[:n]
for k, v := range rw.rewrites {
chunk = bytes.Replace(chunk, []byte(k), []byte(v), -1)
}
n = len(chunk)
m := 0
for m < n {
l, err := pw.Write(chunk[m:])
if err != nil {
pw.CloseWithError(err)
return
}
m += l
}
n, err = body.Read(buff)
}
pw.CloseWithError(err)
}()
return resp, err
}

39
server/proxy_test.go Normal file → Executable file
View File

@@ -1,3 +1,42 @@
package server
import (
"io/ioutil"
"net/http"
"strings"
"testing"
)
type fakeTransport struct{}
func (ft fakeTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return &http.Response{
Body: r.Body,
}, nil
}
// empty url -> OK //TODO
func TestRewrite(t *testing.T) {
transport := &rewrite{
rewrites: map[string]string{
"a": "b",
},
baseTransport: fakeTransport{},
}
r, err := http.NewRequest("GET", "asdf", strings.NewReader("mary had a little lamb"))
if err != nil {
t.Fatal(err)
}
resp, err := transport.RoundTrip(r)
if err != nil {
t.Fatal(err)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(b) != "mbry hbd b little lbmb" {
t.Errorf("failed to replace: got %q, want \"b\"", b)
}
}

0
server/routes.go Normal file → Executable file
View File

0
server/routes_test.go Normal file → Executable file
View File

240
server/server.go Normal file → Executable file
View File

@@ -5,12 +5,18 @@ import (
"crypto/tls"
"encoding/base64"
"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"
"strings"
"time"
@@ -18,12 +24,14 @@ import (
)
const nsRouting = "routing"
const nsBOAuthZ = "oauth"
type listenerScheme int
const (
schemeHTTP listenerScheme = iota
schemeHTTPS listenerScheme = iota
schemeTCP listenerScheme = iota
)
func (ls listenerScheme) String() string {
@@ -32,6 +40,8 @@ func (ls listenerScheme) String() string {
return "http"
case schemeHTTPS:
return "https"
case schemeTCP:
return "tcp"
}
return ""
}
@@ -39,32 +49,36 @@ func (ls listenerScheme) String() string {
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, dst string) error {
log.Printf("Adding route %q -> %q...\n", src, dst)
u, err := url.Parse(dst)
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 {
scheme := schemeHTTP
if _, _, ok := config.GetSSL(); ok {
scheme = schemeHTTPS
}
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,
@@ -83,15 +97,110 @@ func (s *Server) Run() error {
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) doAuth(foo http.HandlerFunc) http.HandlerFunc {
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"
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 {
httpreq.Header.Set(k, v)
}
}
}
if cookie, err := r.Cookie("authelia_session"); err == nil {
req.AddCookie(cookie)
}
c := &http.Client{
Timeout: time.Minute,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
autheliaKey := mapKey(req.Host)
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: "/",
Domain: r2.Host,
SameSite: http.SameSiteLaxMode,
Secure: true,
HttpOnly: true,
Expires: time.Now().Add(24 * time.Hour * 30),
}
http.SetCookie(w, cookie)
}
}
foo(w, r)
return
}
url.Path = ""
q := url.Query()
q.Set("rd", r2.URL.String())
url.RawQuery = q.Encode()
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 := getProxyAuth(r)
usr, pwd, ok := r.BasicAuth()
if !ok || rusr != usr || rpwd != pwd {
w.WriteHeader(http.StatusUnauthorized)
@@ -99,19 +208,72 @@ func (s *Server) doAuth(foo http.HandlerFunc) http.HandlerFunc {
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) {
ctx, can := context.WithTimeout(r.Context(), time.Second*time.Duration(config.GetTimeout()))
ctx, can := context.WithTimeout(r.Context(), time.Duration(config.GetTimeout()))
defer can()
if err := s.limiter.Wait(ctx); err != nil {
w.WriteHeader(http.StatusTooManyRequests)
return
}
s.doAuth(foo)(w, r)
if did := s.doCORS(w, r); did {
return
}
if s.auth.BOAuthZ {
s.doBOAuthZ(foo)(w, r)
} else if s.auth.Authelia {
s.doAuthelia(foo)(w, r)
} else {
foo(w, r)
}
}
}
@@ -119,6 +281,22 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.Pre(s.Proxy)(w, r)
}
func (s *Server) doCORS(w http.ResponseWriter, r *http.Request) bool {
key := mapKey(r.Host)
if !config.GetCORS(key) {
return false
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token, content-type, Content-Type")
if r.Method != "OPTIONS" {
return false
}
w.Header().Set("Content-Length", "0")
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, TRACE, PATCH, HEAD, DELETE")
return true
}
func getProxyAuth(r *http.Request) (string, string) {
proxyAuthHeader := r.Header.Get("Proxy-Authorization")
proxyAuthB64 := strings.TrimPrefix(proxyAuthHeader, "Basic ")
@@ -130,3 +308,39 @@ 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
if _, _, ok := config.GetSSL(); ok {
scheme = schemeHTTPS
}
if _, ok := config.GetTCP(); ok {
scheme = schemeTCP
}
return scheme
}

20
server/server_test.go Normal file → Executable file
View File

@@ -1,18 +1,25 @@
package server
import (
"context"
"fmt"
"local/rproxy3/config"
"local/rproxy3/storage"
"net/http"
"net/http/httptest"
"strings"
"testing"
"golang.org/x/time/rate"
)
func TestServerStart(t *testing.T) {
server := mockServer()
if err := server.Route("world", "http://hello.localhost"+server.addr); err != nil {
p := config.Proxy{
To: "http://hello.localhost" + server.addr,
}
if err := server.Route("world", p); err != nil {
t.Fatalf("cannot add route: %v", err)
}
@@ -33,8 +40,9 @@ func mockServer() *Server {
port := strings.Split(portServer.URL, ":")[2]
portServer.Close()
s := &Server{
db: storage.NewMap(),
addr: ":" + port,
db: storage.NewMap(),
addr: ":" + port,
limiter: rate.NewLimiter(rate.Limit(50), 50),
}
if err := s.Routes(); err != nil {
panic(fmt.Sprintf("cannot initiate server routes; %v", err))
@@ -44,11 +52,15 @@ func mockServer() *Server {
func TestServerRoute(t *testing.T) {
server := mockServer()
if err := server.Route("world", "http://hello.localhost"+server.addr); err != nil {
p := config.Proxy{
To: "http://hello.localhost" + server.addr,
}
if err := server.Route("world", p); err != nil {
t.Fatalf("cannot add route: %v", err)
}
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "http://world.localhost"+server.addr, nil)
r = r.WithContext(context.Background())
server.ServeHTTP(w, r)
if w.Code != 502 {
t.Fatalf("cannot proxy from 'world' to 'hello', status %v", w.Code)

0
storage/db.go Normal file → Executable file
View File

0
storage/db_test.go Normal file → Executable file
View File

0
storage/map.go Normal file → Executable file
View File

0
storage/packable/packable.go Normal file → Executable file
View File

0
storage/packable/packable_test.go Normal file → Executable file
View File

0
testdata/Bserver.crt vendored Normal file → Executable file
View File

0
testdata/Bserver.key vendored Normal file → Executable file
View File

0
testdata/Bserver.pkcs12 vendored Normal file → Executable file
View File

36
testdata/index.html vendored Executable file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Go WebSocket Tutorial</title>
</head>
<body>
<h2>Hello World</h2>
<script>
let socket = new WebSocket("ws://a.bel.test:51555/ws");
document.getElementsByTagName("body")[0].innerHTML += "<br>connecting";
socket.onopen = () => {
document.getElementsByTagName("body")[0].innerHTML += "<br>connected";
socket.send("Hi From the Client!")
};
socket.onclose = event => {
document.getElementsByTagName("body")[0].innerHTML += "<br>disconnected";
socket.send("Client Closed!")
};
socket.onerror = error => {
document.getElementsByTagName("body")[0].innerHTML += "<br>error:" + error;
console.log("Socket Error: ", error);
};
socket.onmessage = function(msgevent) {
document.getElementsByTagName("body")[0].innerHTML += "<br>got:" + msgevent.data;
};
</script>
</body>
</html>

0
testdata/rproxy3server.crt vendored Normal file → Executable file
View File

0
testdata/rproxy3server.key vendored Normal file → Executable file
View File

0
testdata/rproxy3server.pkcs12 vendored Normal file → Executable file
View File

76
testdata/ws.go vendored Executable file
View File

@@ -0,0 +1,76 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
func homePage(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadFile("./index.html")
fmt.Fprintf(w, "%s", b)
}
func setupRoutes() {
http.HandleFunc("/", homePage)
http.HandleFunc("/ws", wsEndpoint)
}
func main() {
fmt.Println("Hello World")
setupRoutes()
log.Fatal(http.ListenAndServe(":8080", nil))
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func reader(conn *websocket.Conn) {
for {
// read in a message
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
// print out that message for clarity
fmt.Println(string(p))
if err := conn.WriteMessage(messageType, p); err != nil {
log.Println(err)
return
}
}
}
func wsEndpoint(w http.ResponseWriter, r *http.Request) {
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
// upgrade this connection to a WebSocket
// connection
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
}
log.Println("Client Connected")
// listen indefinitely for new messages coming
// through on our WebSocket connection
go reader(ws)
for {
log.Println("writing...")
err = ws.WriteMessage(1, []byte("Hi Client!"))
log.Println("written")
if err != nil {
log.Println(err)
return
}
time.Sleep(time.Second)
}
}

25
vendor/vendor.json vendored
View File

@@ -1,25 +0,0 @@
{
"comment": "",
"ignore": "test",
"package": [
{
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context",
"revision": "65e2d4e15006aab9813ff8769e768bbf4bb667a0",
"revisionTime": "2019-02-01T23:59:58Z"
},
{
"checksumSHA1": "HoCvrd3hEhsFeBOdEw7cbcfyk50=",
"path": "golang.org/x/time/rate",
"revision": "fbb02b2291d28baffd63558aa44b4b56f178d650",
"revisionTime": "2018-04-12T16:56:04Z"
},
{
"checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=",
"path": "gopkg.in/yaml.v2",
"revision": "51d6538a90f86fe93ac480b35f37b2be17fef232",
"revisionTime": "2018-11-15T11:05:04Z"
}
],
"rootPath": "local/rproxy3"
}