diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..449ea57
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.code-workspace
+proxperfect
+todo.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8b1b14a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog of proxperfect
+
+## v1.0.1 (June 26, 2022)
+* Initial release by Sven Breuner.
+
+### Contributors
+* Thanks to Paul Hargreaves for helpful comments and suggestions.
+
diff --git a/Dockerfile b/Dockerfile
new file mode 120000
index 0000000..394173d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1 @@
+build_helpers/docker/Dockerfile.alpine
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6232b6e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+EXE ?= ./proxperfect
+
+all: $(EXE)
+
+$(EXE): $(EXE).go
+ go build -o $(EXE) $(EXE).go
+
+clean:
+ rm -f $(EXE)
+
+.PHONY: clean
diff --git a/README.md b/README.md
index b1bbbe5..59c8615 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,18 @@
# proxperfect
-A fan-out reverse proxy for HTTP (including S3)
+
+
+
+**A fan-out reverse proxy for HTTP (including S3)**
+
+proxperfect proxides a single endpoint for HTTP clients to connect to and forwards incoming requests to a given set of servers in a round-robin fashion. This is a way to balance load across multiple servers for applications that have no native support for multiple endpoints and for cases in which DNS-based load balancing is not feasible.
+
+## Usage
+
+The built-in help (`proxperfect --help`) provides simple examples to get started.
+
+You can get proxperfect pre-built for Linux from the [Releases section](https://github.com/breuner/proxperfect/releases) and from [Docker Hub](https://hub.docker.com/r/breuner/proxperfect).
+
+### Questions & Comments
+
+In case of questions, comments, if something is missing to make proxperfect more useful or if you would just like to share your thoughts, feel free to contact me: sven.breuner[at]gmail.com
+
diff --git a/build_helpers/docker/Dockerfile.alpine b/build_helpers/docker/Dockerfile.alpine
new file mode 100644
index 0000000..77a29f6
--- /dev/null
+++ b/build_helpers/docker/Dockerfile.alpine
@@ -0,0 +1,26 @@
+# Proxperfect main executable from local source code on Alpine Linux for minimum image size.
+#
+# Run docker build from proxperfect repository root dir like this:
+# docker build -t proxperfect-local -f build_helpers/docker/Dockerfile.alpine.local .
+
+FROM alpine:3 as builder
+
+RUN apk add bash go make
+
+RUN cd /root && \
+ git clone https://github.com/breuner/proxperfect.git && \
+ cd proxperfect && \
+ go mod download && \
+ make clean && \
+ make
+
+FROM alpine:3
+
+COPY --from=builder /root/proxperfect/proxperfect /usr/local/bin/
+
+RUN apk add bash && \
+ ln -s /usr/local/bin/proxperfect /usr/bin/proxperfect && \
+ /usr/local/bin/proxperfect --version
+
+ENTRYPOINT ["/usr/local/bin/proxperfect"]
+
diff --git a/build_helpers/docker/Dockerfile.alpine.local b/build_helpers/docker/Dockerfile.alpine.local
new file mode 100644
index 0000000..548e7da
--- /dev/null
+++ b/build_helpers/docker/Dockerfile.alpine.local
@@ -0,0 +1,26 @@
+# Proxperfect main executable from local source code on Alpine Linux for minimum image size.
+#
+# Run docker build from proxperfect repository root dir like this:
+# docker build -t proxperfect-local -f build_helpers/docker/Dockerfile.alpine.local .
+
+FROM alpine:3 as builder
+
+RUN apk add bash go make
+
+COPY ./ /root/proxperfect
+
+RUN cd /root/proxperfect && \
+ go mod download && \
+ make clean && \
+ make
+
+FROM alpine:3
+
+COPY --from=builder /root/proxperfect/proxperfect /usr/local/bin/
+
+RUN apk add bash && \
+ ln -s /usr/local/bin/proxperfect /usr/bin/proxperfect && \
+ /usr/local/bin/proxperfect --version
+
+ENTRYPOINT ["/usr/local/bin/proxperfect"]
+
diff --git a/build_helpers/docker/README.md b/build_helpers/docker/README.md
new file mode 100644
index 0000000..182b3a9
--- /dev/null
+++ b/build_helpers/docker/README.md
@@ -0,0 +1,8 @@
+# proxperfect
+
+
+
+**A fan-out reverse proxy for HTTP (including S3)**
+
+For the full description, see proxperfect's readme and changelog on github: https://github.com/breuner/proxperfect
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..2cb3881
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/breuner/proxperfect
+
+go 1.18
+
+require golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..ff51700
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/graphics/proxperfect-logo.svg b/graphics/proxperfect-logo.svg
new file mode 100755
index 0000000..65ede26
--- /dev/null
+++ b/graphics/proxperfect-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/proxperfect.go b/proxperfect.go
new file mode 100644
index 0000000..de2479a
--- /dev/null
+++ b/proxperfect.go
@@ -0,0 +1,284 @@
+// A reverse proxy that distributes load by forwarding different requests to different http servers
+
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "golang.org/x/sync/semaphore"
+ "log"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "syscall"
+)
+
+const ProgName = "ProxPerfect"
+const ProgVersion string = "1.0.1"
+
+type Config struct {
+ beVerbose bool
+ showVersion bool
+ listenPort int
+ proxyStrings []string
+ poolBufSize int
+ numConnsPerServer int // 0 disables this limit
+ redirectCode int // 0 disables redirect
+ fdLimit uint64 // 0 disables attempt to change
+}
+
+var config Config
+
+type ProxyState struct {
+ proxies []*httputil.ReverseProxy
+ connLimiters []*semaphore.Weighted // per-proxy limit
+ requestNum uint32
+}
+
+var proxyState ProxyState
+
+// proxyBufferPool is a httputil.BufferPool backed by a thread-safe sync.Pool
+// note: sync.Pool is garbage-collected on mem pressure, so doesn't need upper bound of elems
+type proxyBufferPool struct {
+ pool *sync.Pool
+ bufAllocNum uint32
+}
+
+func NewProxyBufferPool() httputil.BufferPool {
+ return &proxyBufferPool{
+ pool: new(sync.Pool),
+ bufAllocNum: 0,
+ }
+}
+
+func (bufPool *proxyBufferPool) Get() []byte {
+ buf := bufPool.pool.Get()
+ if buf == nil {
+ if config.beVerbose {
+ var currentAllocNum = atomic.AddUint32(&bufPool.bufAllocNum, 1)
+
+ fmt.Printf("Allocating proxy pool buf. Num: %d; Total alloc size: %d\n", currentAllocNum, uint32(config.poolBufSize)*currentAllocNum)
+ }
+
+ return make([]byte, config.poolBufSize)
+ }
+
+ return buf.([]byte)
+}
+
+func (bufPool *proxyBufferPool) Put(buf []byte) {
+ bufPool.pool.Put(buf)
+}
+
+func Usage() {
+ exename := filepath.Base(os.Args[0])
+
+ fmt.Printf("proxperfect - A fan-out reverse proxy")
+
+ fmt.Printf("Usage: ./%s [OPTIONS] HTTP_SERVERS...\n", exename)
+ fmt.Println()
+
+ fmt.Println("Options:")
+ flag.PrintDefaults()
+ fmt.Println()
+
+ fmt.Printf("Example:\n")
+ fmt.Printf(" Forward requests round-robin to servers 192.168.0.1 through 192.168.0.8:\n")
+ fmt.Printf(" $ ./%s http://192.168.0.{1..8}\n", exename)
+}
+
+// parse command line args and init config struct
+func ParseArguments() {
+ showVersionConfigPtr := flag.Bool("version", false, "Print version and exit.")
+ beVerboseConfigPtr := flag.Bool("verbose", false, "Print verbose output.")
+ listenPortConfigPtr := flag.Int("port", 8080, "Port to listen on for incoming connections.")
+ poolBufSizeConfigPtr := flag.Int("bufsize", 128*1024, "Size of each pooled buffer in bytes. [0 disables buffer pooling.]")
+ numConnsPerServer := flag.Int("maxconns", 10, "Max number of connections per server. [0 disables limit.]")
+ redirectCode := flag.Int("redirect", 0, "Redirect requests using given HTTP code instead of proxying. [0 disables redirect; 301 is temporary redirect.]")
+ fdLimit := flag.Uint64("fdlimit", 0, "Increase open file descriptor limit of process (as in 'ulimit -n').")
+
+ flag.Parse()
+
+ config.beVerbose = *beVerboseConfigPtr
+ config.showVersion = *showVersionConfigPtr
+ config.listenPort = *listenPortConfigPtr
+ config.poolBufSize = *poolBufSizeConfigPtr
+ config.proxyStrings = flag.Args()
+ config.numConnsPerServer = *numConnsPerServer
+ config.redirectCode = *redirectCode
+ config.fdLimit = *fdLimit
+
+ if config.showVersion {
+ fmt.Printf("%s v%s\n", ProgName, ProgVersion)
+ os.Exit(0)
+ }
+
+ if config.beVerbose {
+ fmt.Println("HTTP Servers:", flag.Args())
+ }
+
+ if len(flag.Args()) == 0 {
+ fmt.Println("ERROR: HTTP servers missing. Specify one or more server arguments.")
+ fmt.Println(" (Format: \"http://:\")")
+ fmt.Println()
+ Usage()
+
+ os.Exit(1)
+ }
+
+ if config.fdLimit != 0 {
+ SetOpenFilesLimit()
+ }
+
+ if config.beVerbose {
+ rlimit := GetOpenFilesLimit()
+ fmt.Printf("Current open files limit: %d (Max: %d)\n", rlimit.Cur, rlimit.Max)
+ }
+
+}
+
+// get "ulimit -n"
+func GetOpenFilesLimit() syscall.Rlimit {
+ rlimit := syscall.Rlimit{Cur: 0, Max: 0}
+
+ err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit)
+
+ if err != nil {
+ fmt.Println("Unable to get limit for open file handles:", err)
+ }
+
+ return rlimit
+}
+
+// set "ulimit -n"
+// this func will only increase the limit, not decrease
+func SetOpenFilesLimit() {
+ rlimit := GetOpenFilesLimit()
+
+ if config.fdLimit >= rlimit.Max {
+ fmt.Printf("Adjusting config for open files limit: Was exceeding max. (Current: %d; Max: %d; Config: %d)\n", rlimit.Cur, rlimit.Max, config.fdLimit)
+ config.fdLimit = rlimit.Max
+ }
+
+ if rlimit.Cur >= config.fdLimit {
+ fmt.Printf("Skipping increase of open files limit: Config value is not higher than current value (Current: %d; Config: %d)\n", rlimit.Cur, config.fdLimit)
+ return
+ }
+
+ rlimit.Cur = config.fdLimit
+
+ err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimit)
+ if err != nil {
+ fmt.Println("Setting open files limit failed:", err)
+ }
+}
+
+// NewProxy takes target host and creates a reverse proxy
+func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
+ url, err := url.Parse(targetHost)
+ if err != nil {
+ return nil, err
+ }
+
+ return httputil.NewSingleHostReverseProxy(url), nil
+}
+
+func InitProxyState() {
+ proxyState.requestNum = 0
+
+ for i, proxyStr := range flag.Args() {
+ if config.beVerbose {
+ fmt.Printf("Adding proxy. Index: %d; Server: %s\n", i, proxyStr)
+ }
+
+ proxy, err := NewProxy(flag.Arg(i))
+ if err != nil {
+ panic(err)
+ }
+
+ proxy.FlushInterval = -1 // negative value means "flush immediately"
+
+ if config.poolBufSize > 0 {
+ proxy.BufferPool = NewProxyBufferPool()
+ }
+
+ proxyState.proxies = append(proxyState.proxies, proxy)
+
+ if config.numConnsPerServer != 0 {
+ var sem = semaphore.NewWeighted(int64(config.numConnsPerServer))
+ proxyState.connLimiters = append(proxyState.connLimiters, sem)
+ }
+ }
+
+}
+
+// ProxyRequestHandler proxies the http request to server from given list
+func ProxyRequestHandler() func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var currentRequestNum = atomic.AddUint32(&proxyState.requestNum, 1)
+ var proxyIdx = currentRequestNum % uint32(len(proxyState.proxies))
+ var proxy = proxyState.proxies[proxyIdx]
+ var limiter *semaphore.Weighted
+
+ // limit concurrent connections for this proxy
+ if config.numConnsPerServer != 0 {
+ limiter = proxyState.connLimiters[proxyIdx]
+ ctx := context.Background()
+ limiter.Acquire(ctx, 1)
+ }
+
+ if config.beVerbose {
+ fmt.Printf("[%s START #%d]: %s %s\n", config.proxyStrings[proxyIdx], currentRequestNum, r.Method, r.URL.String())
+ }
+
+ proxy.ServeHTTP(w, r)
+
+ if config.beVerbose {
+ fmt.Printf("[%s END #%d]: %s %s\n", config.proxyStrings[proxyIdx], currentRequestNum, r.Method, r.URL.String())
+ }
+
+ if config.numConnsPerServer != 0 {
+ limiter.Release(1)
+ }
+ }
+}
+
+// RedirectHandler redirects incoming http request to server from given list
+func RedirectHandler(w http.ResponseWriter, r *http.Request) {
+ var currentRequestNum = atomic.AddUint32(&proxyState.requestNum, 1)
+ var proxyIdx = currentRequestNum % uint32(len(proxyState.proxies))
+ var serverStr = config.proxyStrings[proxyIdx] + r.URL.String()
+
+ if config.beVerbose {
+ fmt.Printf("[%s REDIRECT #%d]: %s %s\n", config.proxyStrings[proxyIdx], currentRequestNum, r.Method, r.URL.String())
+ }
+
+ http.Redirect(w, r, serverStr, config.redirectCode)
+}
+
+func main() {
+ flag.Usage = Usage
+
+ ParseArguments()
+
+ InitProxyState()
+
+ // register http request handler
+ if config.redirectCode == 0 {
+ // handle requests through proxy
+ http.HandleFunc("/", ProxyRequestHandler())
+ } else {
+ // handle requests through redirector
+ http.HandleFunc("/", RedirectHandler)
+ }
+
+ fmt.Printf("Listening on port %d...\n", config.listenPort)
+
+ log.Fatal(http.ListenAndServe(":"+strconv.Itoa(config.listenPort), nil))
+}