Skip to content

Commit

Permalink
feat: create initial proxy logic (#1)
Browse files Browse the repository at this point in the history
* 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
smuu authored Nov 6, 2023
1 parent 2c92264 commit e8ff6e9
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 1 deletion.
25 changes: 25 additions & 0 deletions .github/workflows/docker-build-publish.yaml
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
27 changes: 27 additions & 0 deletions Dockerfile
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"]
6 changes: 5 additions & 1 deletion README.md
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
8 changes: 8 additions & 0 deletions go.mod
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
)
4 changes: 4 additions & 0 deletions go.sum
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=
236 changes: 236 additions & 0 deletions main.go
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)
}

0 comments on commit e8ff6e9

Please sign in to comment.