Compare commits

..

No commits in common. "master" and "v0.1" have entirely different histories.
master ... v0.1

34 changed files with 326 additions and 860 deletions

4
.gitignore vendored Executable file → Normal file
View File

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

View File

@ -1,16 +0,0 @@
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,5 +0,0 @@
#! /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

11
conf.yaml Normal file
View File

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

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

@ -1,129 +1,76 @@
package config
import (
"encoding/json"
"fmt"
"log"
"regexp"
"local/rproxy3/storage/packable"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v2"
)
type Proxy struct {
Auth string
From string
To string
}
func parseOneProxyCSV(s string) (string, Proxy) {
p := Proxy{}
key := ""
l := strings.Split(s, ",")
if len(l) > 0 {
key = l[0]
}
if len(l) > 1 {
p.To = l[1]
}
return key, p
}
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)
v := packable.NewString()
conf.Get(nsConf, flagPort, v)
return ":" + strings.TrimPrefix(v.String(), ":")
}
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()
log.Println("rate/burst:", rate, burst)
return rate, burst
}
func GetRoutes() map[string]Proxy {
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],
}
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
}
return result
from := v[:strings.Index(v, ":")]
to := v[strings.Index(v, ":")+1:]
m[from] = to
}
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 := parseOneProxyCSV(definition)
routes[k] = v
}
return routes
return m
}
func GetSSL() (string, string, bool) {
crt := conf.Get("crt").GetString()
key := conf.Get("key").GetString()
return crt, key, crt != "" && key != ""
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 GetTCP() (string, bool) {
tcp := conf.Get("tcp").GetString()
return tcp, tcp != ""
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 GetTimeout() time.Duration {
timeout := conf.Get("timeout").GetDuration()
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, _ := strconv.Atoi(r.String())
burst, _ := strconv.Atoi(b.String())
return rate, burst
}
func GetTimeout() int {
t := packable.NewString()
conf.Get(nsConf, flagTimeout, t)
timeout, _ := strconv.Atoi(t.String())
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")
}

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

@ -1,61 +1,148 @@
package config
import (
"fmt"
"flag"
"io/ioutil"
"local/rproxy3/storage"
"local/rproxy3/storage/packable"
"log"
"os"
"strings"
"time"
"gitea.bel.blue/local/args"
"gitea.bel.blue/local/logb"
yaml "gopkg.in/yaml.v2"
)
var conf *args.ArgSet
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"
func init() {
if err := Refresh(); err != nil {
panic(err)
}
var conf = storage.NewMap()
type toBind struct {
flag string
value *string
}
func Refresh() error {
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
log.SetFlags(log.Ltime | log.Lshortfile)
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"`
}
as, err := parseArgs()
if err != nil && !strings.Contains(fmt.Sprint(os.Args), "-test") {
func Init() error {
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
if err := fromFile(); err != nil {
return err
}
if err := fromFlags(); err != nil {
return err
}
conf = as
logb.Set(logb.LevelFromString(as.GetString("level")))
return nil
}
func parseArgs() (*args.ArgSet, error) {
configFiles := []string{}
if v, ok := os.LookupEnv("CONFIG"); ok {
configFiles = strings.Split(v, ",")
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
}
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, "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, "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
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()
}

46
config/new_test.go Normal file
View File

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

View File

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

17
go.mod
View File

@ -1,17 +0,0 @@
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
View File

@ -1,18 +0,0 @@
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=

6
main.go Executable file → Normal file
View File

@ -1,12 +1,12 @@
package main
import (
"gitea.bel.blue/local/rproxy3/config"
"gitea.bel.blue/local/rproxy3/server"
"local/rproxy3/config"
"local/rproxy3/server"
)
func main() {
if err := config.Refresh(); err != nil {
if err := config.Init(); err != nil {
panic(err)
}

14
main_test.go Executable file → Normal file
View File

@ -34,8 +34,8 @@ func TestHTTPSMain(t *testing.T) {
"username",
"-pass",
"password",
"-proxy2",
"hello," + addr,
"-r",
"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",
"-proxy2",
"hello," + addr,
"-r",
"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, ":")+1:]
return s.URL[strings.LastIndex(s.URL, ":"):]
}

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

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

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

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

@ -1,121 +1,51 @@
package server
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 {
proxyHost string
targetHost string
baseTransport http.RoundTripper
}
type rewrite struct {
rewrites map[string]string
baseTransport http.RoundTripper
proxyHost string
targetHost string
}
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{
proxyHost: r.Host,
targetHost: newURL.Host,
baseTransport: http.DefaultTransport,
newURL, err := s.lookup(r.Host)
transport := &redirPurge{
proxyHost: r.Host,
targetHost: newURL.Host,
}
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) (string, error) {
v := packable.NewString()
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 {
host = strings.Split(host, ".")[0]
host = strings.Split(host, ":")[0]
return host
}
func (rp *redirPurge) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := rp.baseTransport.RoundTrip(r)
resp, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
return resp, err
}
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
}
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 Executable file → Normal file
View File

@ -1,42 +1,3 @@
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)
}
}

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

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

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

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

@ -4,24 +4,16 @@ import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"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"
)
@ -30,10 +22,8 @@ const nsRouting = "routing"
type listenerScheme int
const (
schemeHTTP listenerScheme = iota
schemeHTTPS
schemeTCP
schemeTCPTLS
schemeHTTP listenerScheme = iota
schemeHTTPS listenerScheme = iota
)
func (ls listenerScheme) String() string {
@ -42,10 +32,6 @@ func (ls listenerScheme) String() string {
return "http"
case schemeHTTPS:
return "https"
case schemeTCP:
return "tcp"
case schemeTCPTLS:
return "tcptls"
}
return ""
}
@ -53,36 +39,32 @@ 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 {
src = strings.TrimPrefix(src, "+")
log.Printf("Adding route %q -> %v...\n", src, dst)
u, err := url.Parse(dst.To)
func (s *Server) Route(src, dst string) error {
log.Printf("Adding route %q -> %q...\n", src, dst)
u, err := url.Parse(dst)
if err != nil {
return err
}
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 {
go s.alt()
scheme := getScheme()
scheme := schemeHTTP
if _, _, ok := config.GetSSL(); ok {
scheme = schemeHTTPS
}
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,
@ -101,244 +83,42 @@ 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)
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) 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 {
listen, err := net.Listen("tcp", s.addr)
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 {
return err
}
go func(c net.Conn) {
d, err := net.Dial("tcp", addr)
if err != nil {
log.Println(err)
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 := getProxyAuth(r)
usr, pwd, ok := r.BasicAuth()
if !ok || rusr != usr || rpwd != pwd {
w.WriteHeader(http.StatusUnauthorized)
log.Printf("denying proxy basic auth")
return
}
go pipe(c, d)
go pipe(d, c)
}(c)
}
foo(w, r)
}
}
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()))
ctx, can := context.WithTimeout(r.Context(), time.Second*time.Duration(config.GetTimeout()))
defer can()
if err := s.limiter.Wait(ctx); err != nil {
pushMeta(r, "explain", "limiter exceeded")
w.WriteHeader(http.StatusTooManyRequests)
return
}
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)
}
s.doAuth(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 ")
@ -350,43 +130,3 @@ 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
}

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

@ -1,27 +1,18 @@
package server
import (
"context"
"fmt"
"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{
To: "http://hello.localhost" + server.addr,
}
if err := server.Route("world", p); err != nil {
if err := server.Route("world", "http://hello.localhost"+server.addr); err != nil {
t.Fatalf("cannot add route: %v", err)
}
@ -42,9 +33,8 @@ func mockServer() *Server {
port := strings.Split(portServer.URL, ":")[2]
portServer.Close()
s := &Server{
db: storage.NewMap(),
addr: ":" + port,
limiter: rate.NewLimiter(rate.Limit(50), 50),
db: storage.NewMap(),
addr: ":" + port,
}
if err := s.Routes(); err != nil {
panic(fmt.Sprintf("cannot initiate server routes; %v", err))
@ -54,83 +44,13 @@ func mockServer() *Server {
func TestServerRoute(t *testing.T) {
server := mockServer()
p := config.Proxy{
To: "http://hello.localhost" + server.addr,
}
if err := server.Route("world", p); err != nil {
if err := server.Route("world", "http://hello.localhost"+server.addr); 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)
}
}
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)
}
})
}
}

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

@ -2,8 +2,7 @@ package storage
import (
"errors"
"gitea.bel.blue/local/rproxy3/storage/packable"
"local/rproxy3/storage/packable"
)
var ErrNotFound = errors.New("not found")
@ -11,6 +10,5 @@ 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
}

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

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

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

@ -2,8 +2,7 @@ package storage
import (
"fmt"
"gitea.bel.blue/local/rproxy3/storage/packable"
"local/rproxy3/storage/packable"
)
type Map map[string]map[string][]byte
@ -41,15 +40,6 @@ 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
storage/packable/packable.go Executable file → Normal file
View File

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

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

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

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

36
testdata/index.html vendored
View File

@ -1,36 +0,0 @@
<!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 Executable file → Normal file
View File

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

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

76
testdata/ws.go vendored
View File

@ -1,76 +0,0 @@
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 Normal file
View File

@ -0,0 +1,25 @@
{
"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"
}