summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore15
-rw-r--r--config.go39
-rw-r--r--go.mod10
-rw-r--r--go.sum22
-rw-r--r--handler.go71
-rw-r--r--main.go29
-rw-r--r--path.go60
7 files changed, 246 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a48fe5c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# IDE specific files
+.idea/
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..cd315c0
--- /dev/null
+++ b/config.go
@@ -0,0 +1,39 @@
+package main
+
+import "fmt"
+
+// Config represents the global Portgate config.
+type Config struct {
+	// Where Portgate will be running at.
+	portgatePort int
+	portgateHost string
+
+	// Where the requests will be proxied to.
+	targetHost string
+
+	allowedPorts   []int
+	forbiddenPorts []int
+}
+
+// GetConfig creates the Portgate config from outside sources such as
+// the environment variables and the portgate.yml file.
+func GetConfig() (Config, error) {
+	// TODO: Read config from environment/file
+	return Config{
+		portgatePort: 8080,
+		targetHost:   "localhost",
+
+		allowedPorts:   []int{80},
+		forbiddenPorts: []int{},
+	}, nil
+}
+
+// PortgateAddress is the address on which Portgate will run.
+func (c *Config) PortgateAddress() string {
+	return fmt.Sprintf("%s:%d", c.portgateHost, c.portgatePort)
+}
+
+// TargetAddress is the address of the destination server.
+func (c *Config) TargetAddress(port int) string {
+	return fmt.Sprintf("%s:%d", c.targetHost, port)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..6ba1ccd
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module portgate
+
+go 1.16
+
+require (
+	github.com/valyala/fasthttp v1.28.0
+
+	github.com/andybalholm/brotli v1.0.3 // indirect
+	github.com/klauspost/compress v1.13.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c912579
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,22 @@
+github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
+github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
+github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.28.0 h1:ruVmTmZaBR5i67NqnjvvH5gEv0zwHfWtbjoyW98iho4=
+github.com/valyala/fasthttp v1.28.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/handler.go b/handler.go
new file mode 100644
index 0000000..edb0ff2
--- /dev/null
+++ b/handler.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+	"github.com/valyala/fasthttp"
+	"net/http"
+)
+
+// RequestHandler keeps data relevant to the request handlers.
+type RequestHandler struct {
+	// Pointer to the global Portgate config, the values of which can change at runtime.
+	config *Config
+	// HTTP Client for requesting resources from the destination host.
+	client fasthttp.Client
+}
+
+// handleRequest handles all types of requests and delegates to more specific handlers.
+func (h *RequestHandler) handleRequest(ctx *fasthttp.RequestCtx) {
+	path := ParsePath(string(ctx.Path()))
+
+	if path.DestinationIdentifier == -1 {
+		// We were not given a port.
+
+		if path.ResourcePath == "/_portgate" {
+			h.handlePortgateRequest(ctx)
+		} else {
+			// TODO: Try to grab actual destination from Referer header.
+			h.handleUnknownRequest(ctx)
+		}
+	} else {
+		// We were given a port, so we have to pass the request through to the destination host.
+
+		h.handlePassthroughRequest(ctx, path)
+	}
+}
+
+// handlePassthroughRequest handles requests which are supposed to be proxied to the destination host.
+// If the user is authorized they are allowed to pass, otherwise they should be redirected to
+// the authentication page. (/_portgate)
+func (h *RequestHandler) handlePassthroughRequest(ctx *fasthttp.RequestCtx, p Path) {
+	// TODO: Check authorization.
+	// TODO: Check whether port is allowed to be accessed.
+
+	// We reuse the request given to us by the user with minor changes to route it to the
+	// destination host.
+	ctx.Request.SetRequestURI(p.MakeUrl(h.config.targetHost))
+	ctx.Request.Header.Set("Host", h.config.TargetAddress(p.DestinationIdentifier))
+
+	// We pipe the response given to us by the destination host back to the user.
+	// Since it's possible that we get a redirect, we take this into account,
+	// but only allow upto 10 redirects.
+	err := h.client.DoRedirects(&ctx.Request, &ctx.Response, 10)
+	if err != nil {
+		ctx.SetStatusCode(http.StatusInternalServerError)
+		_, _ = ctx.WriteString("An error occurred.")
+	}
+}
+
+// handlePortgateRequest handles all Portgate specific request for either showing Portgate
+// specific pages or handling creation of authorization tokens.
+func (h *RequestHandler) handlePortgateRequest(ctx *fasthttp.RequestCtx) {
+	// TODO: Implement authentication, authorization
+	_, _ = ctx.WriteString("Portgate request.")
+}
+
+// handleUnknownRequest handles any request which could not be processed due to missing
+// information.
+func (h *RequestHandler) handleUnknownRequest(ctx *fasthttp.RequestCtx) {
+	// TODO: Show error page
+	ctx.SetStatusCode(http.StatusNotFound)
+	_, _ = ctx.WriteString("Unknown request.")
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..9870dee
--- /dev/null
+++ b/main.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+	"github.com/valyala/fasthttp"
+	"log"
+)
+
+func main() {
+	log.Print("Starting Portgate...")
+
+	// Get global Portgate config.
+	config, err := GetConfig()
+	if err != nil {
+		log.Fatal("Failed to get Portgate config.")
+	}
+
+	// Create handler for requests
+	handler := RequestHandler{
+		config: &config,
+		client: fasthttp.Client{},
+	}
+
+	// Start to listen to the outside world.
+	log.Print("Listening for requests on port 8080.")
+	err = fasthttp.ListenAndServe(config.PortgateAddress(), handler.handleRequest)
+	if err != nil {
+		log.Fatalf("Portgate server could not be started: %s", err)
+	}
+}
diff --git a/path.go b/path.go
new file mode 100644
index 0000000..bb7154a
--- /dev/null
+++ b/path.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+	"fmt"
+	"path"
+	"strconv"
+	"strings"
+)
+
+// Path represents a routing destination the user gave.
+type Path struct {
+	// Identifier to which port of the destination host the path points to and to which the
+	// user's request will be proxied to.
+	// If there was no identifier given it's -1.
+	DestinationIdentifier int
+	// The path without the port identifier.
+	// This is the path which will be requested from the destination.
+	ResourcePath string
+}
+
+// ParsePath creates a Path from the requested URL.
+func ParsePath(p string) Path {
+	p = path.Clean("/" + p)
+
+	// Get first path part, which is the destinationIdentifier
+
+	destinationIdentifierEnd := strings.Index(p[1:], "/") + 1
+
+	// If there is no '/' at in the path, apart from the root, the first part is the entire path
+	if destinationIdentifierEnd == 0 {
+		destinationIdentifierEnd = len(p)
+	}
+
+	destinationIdentifier := p[1:destinationIdentifierEnd]
+
+	// We have to to check that destinationIdentifier is a port
+	port, err := strconv.Atoi(destinationIdentifier)
+	if err == nil {
+		// We got an identifier and can split the path
+
+		resourcePath := path.Clean("/" + p[destinationIdentifierEnd:])
+		return Path{
+			DestinationIdentifier: port,
+			ResourcePath:          resourcePath,
+		}
+	} else {
+		// We got some other path without an identifier
+
+		return Path{
+			DestinationIdentifier: -1,
+			ResourcePath:          p,
+		}
+	}
+}
+
+// MakeUrl creates the URL on the destination host that the user wants to access.
+func (p *Path) MakeUrl(targetHost string) string {
+	// TODO: Figure out what to do with TLS
+	return fmt.Sprintf("http://%s:%d%s", targetHost, p.DestinationIdentifier, p.ResourcePath)
+}