-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create initial proxy logic (#1)
* feat: create initial proxy logic Signed-off-by: Smuu <[email protected]> * feat: add logging Signed-off-by: Smuu <[email protected]> * feat: add support for handling grpc Signed-off-by: Smuu <[email protected]> * bugfix: handle grpc headers Signed-off-by: Smuu <[email protected]> * feat: remove support for grpc Signed-off-by: Smuu <[email protected]> * feat: add support for subdomains Signed-off-by: Smuu <[email protected]> * feat: dont use subdomain Signed-off-by: Smuu <[email protected]> * bugfix: forewarding Signed-off-by: Smuu <[email protected]> * feat: fix replacing not working due to compression Signed-off-by: Smuu <[email protected]> * feat: handle websocket connections sperately Signed-off-by: Smuu <[email protected]> * feat: allow all origing Signed-off-by: Smuu <[email protected]> * debugging Signed-off-by: Smuu <[email protected]> * debugging Signed-off-by: Smuu <[email protected]> * feat: decompression with other protocols Signed-off-by: Smuu <[email protected]> * feat: contiunue with original even if decompression failed Signed-off-by: Smuu <[email protected]> * testing Signed-off-by: Smuu <[email protected]> --------- Signed-off-by: Smuu <[email protected]>
- Loading branch information
Showing
6 changed files
with
305 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
name: Docker Build & Publish | ||
|
||
# Trigger on all push events, new semantic version tags, and all PRs | ||
on: | ||
push: | ||
branches: | ||
- "main" | ||
- "v[0-9].[0-9].x" | ||
- "v[0-9].[0-9][0-9].x" | ||
- "v[0-9].x" | ||
tags: | ||
- "v[0-9]+.[0-9]+.[0-9]+" | ||
- "v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+" | ||
- "v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+" | ||
- "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" | ||
pull_request: | ||
|
||
jobs: | ||
docker-security-build: | ||
permissions: | ||
contents: write | ||
packages: write | ||
uses: celestiaorg/.github/.github/workflows/[email protected] # yamllint disable-line rule:line-length | ||
with: | ||
dockerfile: Dockerfile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Use the official Golang image to create a build artifact. | ||
FROM golang:1.21.1 AS builder | ||
|
||
# Set the working directory inside the container. | ||
WORKDIR /app | ||
|
||
# Copy go mod and sum files | ||
#COPY go.mod go.sum ./ | ||
COPY go.mod ./ | ||
|
||
# Download all dependencies. | ||
RUN go mod download | ||
|
||
# Copy the source code into the container. | ||
COPY . . | ||
|
||
# Build the Go app | ||
RUN CGO_ENABLED=0 GOOS=linux go build -o main . | ||
|
||
# Use a lightweight image for the final image. | ||
FROM alpine:3 | ||
|
||
# Copy the binary. | ||
COPY --from=builder /app/main /app/main | ||
|
||
# Run the binary. | ||
CMD ["/app/main"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
# autoscale-proxy | ||
# autoscale-proxy | ||
|
||
## ToDo | ||
|
||
- use internal services instead of domain names |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module github.com/celestiaorg/autoscale-proxy | ||
|
||
go 1.21.1 | ||
|
||
require ( | ||
github.com/andybalholm/brotli v1.0.6 | ||
github.com/gorilla/websocket v1.5.0 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= | ||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= | ||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"compress/flate" | ||
"compress/gzip" | ||
"io" | ||
"log" | ||
"net/http" | ||
"os" | ||
"strings" | ||
|
||
"github.com/andybalholm/brotli" | ||
"github.com/gorilla/websocket" | ||
) | ||
|
||
var ( | ||
debugLog *log.Logger | ||
infoLog *log.Logger | ||
errorLog *log.Logger | ||
) | ||
|
||
func init() { | ||
debugLog = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile) | ||
infoLog = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) | ||
errorLog = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) | ||
} | ||
|
||
func replaceDomainInResponse(originalSubdomain, replaceSubdomain, originalDomain string, buffer *bytes.Buffer) { | ||
body := buffer.String() | ||
fullReplace := replaceSubdomain + "." + "lunaroasis.net" // We know that statescale and snapscale are under this domain | ||
fullOriginal := originalSubdomain + "." + originalDomain // Original domain can vary | ||
replacedBody := strings.ReplaceAll(body, fullReplace, fullOriginal) | ||
buffer.Reset() | ||
buffer.WriteString(replacedBody) | ||
} | ||
|
||
func proxyRequest(fullSubdomain, path string, buffer *bytes.Buffer, r *http.Request) (int, map[string]string, error) { | ||
client := &http.Client{} | ||
target := "https://" + fullSubdomain + ".lunaroasis.net" + path | ||
newReq, err := http.NewRequest(r.Method, target, r.Body) | ||
if err != nil { | ||
errorLog.Printf("Failed to create request: %v", err) | ||
return 0, nil, err | ||
} | ||
newReq.Header = r.Header | ||
|
||
resp, err := client.Do(newReq) | ||
if err != nil { | ||
errorLog.Printf("Failed to send request: %v", err) | ||
return 0, nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
headers := make(map[string]string) | ||
for key, values := range resp.Header { | ||
for _, value := range values { | ||
headers[key] = value | ||
} | ||
} | ||
|
||
encoding := resp.Header.Get("Content-Encoding") | ||
var reader io.Reader | ||
switch encoding { | ||
case "br": | ||
// Decompress Brotli data | ||
reader = brotli.NewReader(resp.Body) | ||
case "gzip": | ||
// Decompress Gzip data | ||
reader, err = gzip.NewReader(resp.Body) | ||
if err != nil { | ||
errorLog.Printf("Failed to create gzip reader: %v", err) | ||
return 0, nil, err | ||
} | ||
case "deflate": | ||
// Decompress Deflate data | ||
reader = flate.NewReader(resp.Body) | ||
default: | ||
reader = resp.Body | ||
} | ||
io.Copy(buffer, reader) | ||
|
||
return resp.StatusCode, headers, nil | ||
} | ||
|
||
func handleHttpRequest(w http.ResponseWriter, r *http.Request) { | ||
infoLog.Printf("Received request from %s", r.Host) | ||
|
||
hostParts := strings.Split(r.Host, ".") | ||
if len(hostParts) < 3 { | ||
errorLog.Printf("Invalid domain: %s", r.Host) | ||
http.Error(w, "Invalid domain", http.StatusBadRequest) | ||
return | ||
} | ||
|
||
subdomain := hostParts[0] // Extract original domain | ||
originalDomain := strings.Join(hostParts[1:], ".") | ||
|
||
// Check for WebSocket upgrade headers | ||
if strings.ToLower(r.Header.Get("Upgrade")) == "websocket" { | ||
// Handle WebSocket requests by proxying to snapscale | ||
proxyWebSocketRequest(subdomain, w, r) | ||
return | ||
} | ||
|
||
buffer := new(bytes.Buffer) | ||
backupBuffer := new(bytes.Buffer) | ||
|
||
debugLog.Printf("Proxying request to %s", subdomain+"-statescale") | ||
statusCode, headers, err := proxyRequest(subdomain+"-statescale", r.RequestURI, buffer, r) | ||
debugLog.Printf("Received status code %d", statusCode) | ||
if err != nil || statusCode >= 400 { | ||
debugLog.Printf("Proxying request to %s", subdomain+"-snapscale") | ||
backupStatusCode, backupHeaders, _ := proxyRequest(subdomain+"-snapscale", r.RequestURI, backupBuffer, r) | ||
debugLog.Printf("Received status code %d", backupStatusCode) | ||
|
||
replaceDomainInResponse(subdomain, subdomain+"-snapscale", originalDomain, backupBuffer) | ||
|
||
for key, value := range backupHeaders { | ||
w.Header().Set(key, value) | ||
} | ||
w.WriteHeader(backupStatusCode) | ||
encoding := headers["Content-Encoding"] | ||
buffer = compressData(buffer, encoding) | ||
io.Copy(w, backupBuffer) | ||
return | ||
} | ||
|
||
replaceDomainInResponse(subdomain, subdomain+"-statescale", originalDomain, buffer) | ||
for key, value := range headers { | ||
w.Header().Set(key, value) | ||
} | ||
w.WriteHeader(statusCode) | ||
// If the original response was Brotli-compressed, recompress the data | ||
encoding := headers["Content-Encoding"] | ||
buffer = compressData(buffer, encoding) | ||
io.Copy(w, buffer) | ||
} | ||
|
||
var upgrader = websocket.Upgrader{ | ||
CheckOrigin: func(r *http.Request) bool { | ||
return true // Allow all connections | ||
}, | ||
} | ||
|
||
func proxyWebSocketRequest(subdomain string, w http.ResponseWriter, r *http.Request) { | ||
// Build target URL | ||
fullSubdomain := subdomain + "-snapscale" | ||
target := "wss://" + fullSubdomain + ".lunaroasis.net" + r.RequestURI | ||
|
||
// Create a new WebSocket connection to the target | ||
dialer := websocket.Dialer{} | ||
targetConn, resp, err := dialer.Dial(target, nil) | ||
if err != nil { | ||
errorLog.Printf("Failed to connect to target: %v", err) | ||
if resp != nil { | ||
errorLog.Printf("Handshake response status: %s", resp.Status) | ||
// Log all response headers for debugging | ||
for k, v := range resp.Header { | ||
errorLog.Printf("%s: %s", k, v) | ||
} | ||
} | ||
http.Error(w, "Internal server error", http.StatusInternalServerError) | ||
return | ||
} | ||
defer targetConn.Close() | ||
|
||
// Upgrade the client connection to a WebSocket connection | ||
clientConn, err := upgrader.Upgrade(w, r, nil) | ||
if err != nil { | ||
errorLog.Printf("Failed to upgrade client connection: %v", err) | ||
return // No need to send an error response, Upgrade already did if there was an error | ||
} | ||
defer clientConn.Close() | ||
|
||
// Start goroutines to copy data between the client and target | ||
go func() { | ||
for { | ||
messageType, message, err := targetConn.ReadMessage() | ||
if err != nil { | ||
errorLog.Printf("Failed to read from target: %v", err) | ||
return | ||
} | ||
err = clientConn.WriteMessage(messageType, message) | ||
if err != nil { | ||
errorLog.Printf("Failed to write to client: %v", err) | ||
return | ||
} | ||
} | ||
}() | ||
go func() { | ||
for { | ||
messageType, message, err := clientConn.ReadMessage() | ||
if err != nil { | ||
errorLog.Printf("Failed to read from client: %v", err) | ||
return | ||
} | ||
err = targetConn.WriteMessage(messageType, message) | ||
if err != nil { | ||
errorLog.Printf("Failed to write to target: %v", err) | ||
return | ||
} | ||
} | ||
}() | ||
|
||
// The goroutines will run until one of the connections is closed | ||
select {} | ||
} | ||
|
||
func compressData(buffer *bytes.Buffer, encoding string) *bytes.Buffer { | ||
var compressedData bytes.Buffer | ||
var writer io.WriteCloser | ||
switch encoding { | ||
case "br": | ||
writer = brotli.NewWriterLevel(&compressedData, brotli.DefaultCompression) | ||
case "gzip": | ||
writer = gzip.NewWriter(&compressedData) | ||
case "deflate": | ||
writer, _ = flate.NewWriter(&compressedData, flate.DefaultCompression) | ||
default: | ||
return buffer | ||
} | ||
io.Copy(writer, buffer) | ||
writer.Close() | ||
return &compressedData | ||
} | ||
|
||
func handleRequest(w http.ResponseWriter, r *http.Request) { | ||
handleHttpRequest(w, r) | ||
} | ||
|
||
func main() { | ||
infoLog.Println("Starting server on :8080") | ||
http.HandleFunc("/", handleRequest) | ||
http.ListenAndServe(":8080", nil) | ||
} |