Skip to content

Commit

Permalink
Merge pull request #3 from thoughtgears/feature/multiple-proxies
Browse files Browse the repository at this point in the history
multiple proxies
  • Loading branch information
jensskott authored Mar 6, 2024
2 parents 4ec76e5 + 006b2e8 commit e93f3c2
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 35 deletions.
73 changes: 67 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Proximo

A simple, lightweight HTTP proxy to allow anyone to access cloud run endpoints behind authentication. This will proxy
requests to the cloud run endpoint, and add the `Authorization` header to the request based on your credentials.
A simple, lightweight HTTP proxy to allow anyone to access cloud run endpoints behind authentication. This will proxy requests to the cloud run endpoint, and add the `Authorization` header to the request based on your credentials.

## Usage

Expand All @@ -15,19 +14,81 @@ gcloud auth login

### Start the proxy

**Start with a single target**

```shell
proximo -url https://my-cloud-run-endpoint.run.app
proximo -targets service=https://my-cloud-run-endpoint.run.app
```

This will start the proxy on port 8080, proxying requests to the given Cloud Run URL and adding the Authorization header with your Google credentials. You should be able to access the endpoint at http://localhost:8080.

**Start with multiple targets**

```shell
proximo -targets service1=https://service1.com,service2=https://service2.com
```

This will start the proxy on port 8080, proxying requests to the given Cloud Run URLs and adding the Authorization header with your Google credentials. You should be able to access the endpoints at http://localhost:8080.

**Start with config file**

```shell
proximo -file config.yaml
```

This will start the proxy on port 8080, proxying requests to the given Cloud Run URLs and adding the Authorization header with your Google credentials. You should be able to access the endpoints at http://localhost:8080.
This will use a config file to specify target and url mappings.

```json
{
"targets": {
"google": "https://google.com",
"microsoft": "https://microsoft.com"
}
}
```

**Change the port of the proxy**

```shell
proximo -url https://my-cloud-run-endpoint.run.app -port 4040
proximo -targets service=https://my-cloud-run-endpoint.run.app -port 4040
```

**Disable the JWT token**

```shell
proximo -config config.json -auth=false
```

This will start the proxy on port 8080, proxying requests to the given Cloud Run URL and adding the Authorization
header with your Google credentials. You should be able to access the endpoint at http://localhost:8080.
## Multiple Backends

Proximo supports routing requests to multiple backends based on the `Host` header. You can specify multiple host-to-target mappings when starting the proxy using the `-targets` or `-file` flag.

```shell
proximo -targets="search=https://search.com,api=https://api.com"
```

This will create reverse proxies for \`search.com\` and \`api.com\`, respectively. You can then send requests to these backends using the appropriate \`Host\` header:

```shell
# For https://search.com
curl -H "Host: search" http://localhost:8080/path/to/resource

# For https://api.com
curl -H "Host: api" http://localhost:8080/api/v1/endpoint
```

## Authentication and Authorization

Proximo automatically adds an \`Authorization\` header with a JWT token to the forwarded requests. The token is obtained from the default Google Cloud Platform credentials on your machine.

To ensure that the proxy can authenticate with your Google account, you need to be logged in:

```shell
gcloud auth login
```

The proxy will use the credentials from your currently authenticated Google account to generate the JWT token.

## Building

Expand Down
6 changes: 6 additions & 0 deletions example/targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"targets": {
"google": "https://google.com",
"microsoft": "https://microsoft.com"
}
}
130 changes: 101 additions & 29 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,65 +1,137 @@
// Package main provides a reverse proxy server that supports multiple backends
// and custom host-to-target mappings. It also adds an Authorization header to
// the forwarded request using a JWT token from the default credentials.
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"

"golang.org/x/oauth2/google"
)

type Config struct {
Targets map[string]string `json:"targets"`
}

// main is the entry point of the application. It parses command-line flags,
// creates reverse proxies for each host-to-target mapping, and starts the server.
func main() {
targetURL := flag.String("url", "", "URL of the target service")
localPort := flag.String("port", "8080", "Local port to listen on")
targets := flag.String("targets", "", "Comma-separated list of target URLs and hosts, e.g.-targets search=https://search.com,api=https://api.com")
configFile := flag.String("file", "", "Path to the configuration file, e.g. -file config.json")
localPort := flag.String("port", "8080", "Local port to listen on, e.g. -port 8080 (default: 8080)")
auth := flag.Bool("auth", true, "Enable authentication, e.g. -auth false (default: true)")
flag.Parse()

if *targetURL == "" {
log.Fatal("Missing required flag: -url")
}
var hostToTarget map[string]string

target, err := url.Parse(*targetURL)
if err != nil {
log.Fatal(err)
if *configFile != "" {
cfg, err := loadConfig(*configFile)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
hostToTarget = cfg.Targets
} else if *targets != "" {
hostToTarget = parseHostToTarget(*targets)
} else {
log.Fatal("Missing required flag: either -file or -url must be provided")
}

proxy := httputil.NewSingleHostReverseProxy(target)

// Modify the request's Host header to the target host.
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
req.Host = target.Host

// Get JWT token
token, err := getAccessToken(context.Background())
proxies := make(map[string]*httputil.ReverseProxy)
for host, targetURL := range hostToTarget {
target, err := url.Parse(targetURL)
if err != nil {
log.Printf("Error getting access token: %v", err)
return
log.Fatal(err)
}

// Add the Authorization header to the forwarded request.
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
proxy := httputil.NewSingleHostReverseProxy(target)
proxies[host] = proxy

// Modify the request's Host header to the target host.
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
req.Host = target.Host

if *auth {
// Get JWT token
token, err := getAccessToken(context.Background())
if err != nil {
log.Printf("Error getting access token: %v", err)
return
}

// Add the Authorization header to the forwarded request.
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
}

http.HandleFunc("/", loggingMiddleware(handler(proxy)))
http.HandleFunc("/", loggingMiddleware(handleRequest(proxies)))

log.Printf("Starting server at http://localhost:%s\n", *localPort)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", *localPort), nil))
}

// loadConfig loads the configuration from the given file path.
// It returns a Config struct or an error if the file cannot be read or parsed.
func loadConfig(path string) (*Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()

var cfg Config
err = json.NewDecoder(file).Decode(&cfg)
if err != nil {
return nil, err
}

return &cfg, nil
}

func handler(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
// handleRequest is an HTTP handler function that routes the request to the
// appropriate reverse proxy based on the request's Host header.
func handleRequest(proxies map[string]*httputil.ReverseProxy) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
p.ServeHTTP(w, r)
proxy, ok := proxies[r.Host]
if !ok {
http.Error(w, "No proxy configured for this host", http.StatusBadGateway)
return
}

proxy.ServeHTTP(w, r)
}
}

// parseHostToTarget parses the given comma-separated string of host-to-target mappings
// into a map. The expected format is "host1=target1,host2=target2,...".
// It returns a map where the keys are the hosts and the values are the corresponding targets.
func parseHostToTarget(targets string) map[string]string {
hostToTarget := make(map[string]string)
pairs := strings.Split(targets, ",")
for _, pair := range pairs {
parts := strings.SplitN(pair, "=", 2)
if len(parts) != 2 {
log.Fatalf("Invalid target format: %s", pair)
}
host, target := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
hostToTarget[host] = target
}
return hostToTarget
}

// getAccessToken retrieves a JWT token from the default credentials.
// See https://cloud.google.com/docs/authentication/production#finding_credentials_automatically
// getAccessToken retrieves a JWT token from the default Google Cloud Platform credentials.
// See https://cloud.google.com/docs/authentication/production#finding_credentials_automaticall
func getAccessToken(ctx context.Context) (string, error) {
credentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/cloud-platform")
if err != nil {
Expand All @@ -75,10 +147,10 @@ func getAccessToken(ctx context.Context) (string, error) {
return token.Extra("id_token").(string), nil
}

// loggingMiddleware logs the request method and URL.
// loggingMiddleware is a middleware function that logs the request method and URL.
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request: Method=%s, URL=%s", r.Method, r.URL)
log.Printf("Request: Method=%s, URL=%s, Host=%s", r.Method, r.URL, r.Host)
next.ServeHTTP(w, r) // call original handler
}
}

0 comments on commit e93f3c2

Please sign in to comment.