Skip to content

Commit 0dd9b63

Browse files
authored
feat: add improvements to the server and docker image (#1)
* docs(readme): fix the example on the readme file * feat(server): add a server package to make the app more testable * docs(server): add doc comments to functions * test(server): add a test function to validate the behavior of the proxy server * refactor(server): use env variables instead of flags * docs(readme): update the running command * build(docker): add a dockerfile to run a container * build(makefile): add a makefile * ci(github): add a github action workflow to ship the container to the github registry * feat(server): call and run the server package on the server command * feat(middlewares): add middleware to log the site of interest * docs(readme): expand the content of the readme file to explain how to use the project
1 parent 33aa06c commit 0dd9b63

13 files changed

Lines changed: 422 additions & 63 deletions

File tree

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.git
2+
LICENSE
3+
Makefile
4+
README.md
5+
bin/

.github/workflows/deploy_image.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Deploy Jumble Proxy Server Container Image
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v[0-9]+.[0-9]+.[0-9]+' # Only build on tag with semantic versioning format
7+
8+
jobs:
9+
push_avestruz_image:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
packages: write
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
- name: Set up QEMU
18+
uses: docker/setup-qemu-action@v3
19+
# Script for setting the version without the leading 'v'
20+
- name: Set Versions
21+
uses: actions/github-script@v7
22+
id: set_version
23+
with:
24+
script: |
25+
// Get the tag
26+
const tag = context.ref.substring(10)
27+
// Replace the tag with one without v
28+
const no_v = tag.replace('v', '')
29+
// Looks for a dash
30+
const dash_index = no_v.lastIndexOf('-')
31+
// If any, removes it, otherwise return the value unchanged
32+
const no_dash = (dash_index > -1) ? no_v.substring(0, dash_index) : no_v
33+
// Set the tag, no-v and no-dash as output variables
34+
core.setOutput('tag', tag)
35+
core.setOutput('no-v', no_v)
36+
core.setOutput('no-dash', no_dash)
37+
- name: Set up Docker Buildx
38+
uses: docker/setup-buildx-action@v3
39+
- name: Login to GitHub Container Registry
40+
uses: docker/login-action@v3
41+
with:
42+
registry: ghcr.io
43+
username: ${{ github.actor }}
44+
password: ${{ secrets.GITHUB_TOKEN }}
45+
- name: Build and push
46+
uses: docker/build-push-action@v5
47+
with:
48+
context: .
49+
platforms: linux/amd64,linux/arm64
50+
push: true
51+
tags: ghcr.io/${{ github.actor }}/jumble-proxy-server:latest, ghcr.io/${{ github.actor }}/jumble-proxy-server:${{steps.set_version.outputs.no-dash}}
52+
- name: Release
53+
uses: softprops/action-gh-release@v1

.gitignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Code coverage profiles and other test artifacts
15+
*.out
16+
coverage.*
17+
*.coverprofile
18+
profile.cov
19+
20+
# Dependency directories (remove the comment below to include it)
21+
# vendor/
22+
23+
# Go workspace file
24+
go.work
25+
go.work.sum
26+
27+
# env file
28+
.env
29+
30+
# Editor/IDE
31+
# .idea/
32+
# .vscode/
33+
bin/

Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM golang:1.24-bookworm AS build
2+
3+
ARG TARGETOS
4+
ARG TARGETARCH
5+
6+
RUN apt-get update -y \
7+
&& apt-get clean
8+
9+
WORKDIR /app
10+
11+
COPY go.* ./
12+
RUN go mod download
13+
14+
COPY . .
15+
16+
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o jumble-proxy-server .
17+
18+
FROM gcr.io/distroless/base-debian11 AS build-release-stage
19+
20+
ENV PORT=8080
21+
ENV AllOW_ORIGIN=https://jumble.social
22+
23+
COPY --from=build /app/jumble-proxy-server /bin/jumble-proxy-server
24+
25+
ENTRYPOINT ["/bin/jumble-proxy-server", "server"]

Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.PHONY: docker-build
2+
## docker-build: Builds the Docker image
3+
docker-build:
4+
@docker build -t jumble-proxy-server .
5+
6+
.PHONY: build
7+
## build: Builds the Go program
8+
build:
9+
@CGO_ENABLED=0 go build -o bin/jumble-proxy-server .
10+
11+
.PHONY: docker-run
12+
## docker-run: Run the container
13+
docker-run: docker-build
14+
@docker run --rm -e ALLOW_ORIGIN=https://jumble.social -e PORT=8080 -p 8080:8080 jumble-proxy-server:latest
15+
16+
.PHONY: test
17+
## test: Runs the tests
18+
test:
19+
go test -v -race ./...
20+
21+
.PHONY: help
22+
## help: Prints this help message
23+
help:
24+
@echo "Usage:"
25+
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'

README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,44 @@
11
# jumble-proxy-server
22

3-
__This application is a proxy server used by the Jumble Nostr client as a workaround to fix CORS erros,so that the client can show the URL preview from links' Open Graph data.__
3+
__This application is a proxy server used by the Jumble Nostr client as a workaround to fix CORS errors, so that the client can show the URL preview from given links Open Graph data.__
44

55
## Usage
6+
7+
### From source
8+
9+
Compile the binary:
10+
11+
```
12+
CGO_ENABLED=0 go build -o bin/jumble-proxy-server .
13+
```
14+
15+
Run the binary with environment variables:
16+
617
```sh
7-
jumble-proxy-server -a https://jumble.social -p 8080
18+
ALLOW_ORIGIN=https://jumble.social PORT=8080 bin/jumble-proxy-server server
19+
```
20+
21+
### Using Docker
22+
23+
Pull the image:
24+
25+
```
26+
docker pull ghcr.io/danvergara/jumble-proxy-server:latest
27+
```
28+
29+
Run the container;
30+
831
```
32+
docker run docker run --rm -e ALLOW_ORIGIN=https://jumble.social -e PORT=8080 -p 8080:8080 ghcr.io/danvergara/jumble-proxy-server:latest
33+
34+
```
35+
36+
### How to hit the proxy server
37+
38+
The inner URL needs to be encoded so it doesn't break the outer URL structure.
939

1040
```sh
11-
curl -X GET http://localhost:8080/sites/https:/www.youtube.com/watch\?v\=i-OZcpNSzg0
41+
curl -X GET http://localhost:8080/sites/https%3A%2F%2Fyoutu.be%2FNVm_jGdwTjQ%3Fsi%3DblYLT44WrrPjL9gU
1242
```
43+
44+
The server will respond with the HTML from the website of interest.

cmd/server.go

Lines changed: 16 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
/*
2-
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
2+
Copyright © 2025 Daniel Vergara daniel.omar.vergara@gmail.com
33
*/
44
package cmd
55

66
import (
7+
"context"
78
"fmt"
8-
"io"
99
"log"
10-
"net/http"
1110
"os"
1211

1312
"github.com/spf13/cobra"
13+
14+
"github.com/danvergara/jumble-proxy-server/pkg/config"
15+
"github.com/danvergara/jumble-proxy-server/pkg/server"
1416
)
1517

1618
var (
@@ -29,72 +31,26 @@ so that the client can show the URL preview from links' Open Graph data.`,
2931
port = "8000"
3032
}
3133

32-
http.HandleFunc("GET /sites/{site}", proxyHandler)
33-
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil {
34-
fmt.Fprintf(os.Stderr, "%s\n", err)
35-
os.Exit(1)
34+
cfg := config.Config{
35+
Port: port,
36+
AllowedOrigin: allowedOrigin,
3637
}
3738

3839
log.Printf("server listening on port %s\n", port)
3940

40-
return nil
41-
},
42-
}
43-
44-
func proxyHandler(w http.ResponseWriter, r *http.Request) {
45-
if allowedOrigin == "" {
46-
allowedOrigin = "*"
47-
}
48-
49-
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
50-
w.Header().Set("Access-Control-Allow-Methods", "GET")
51-
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
52-
53-
site := r.PathValue("site")
54-
55-
// Send request to the target site.
56-
req, err := http.NewRequest(r.Method, site, r.Body)
57-
if err != nil {
58-
http.Error(w, "Failed to create request", http.StatusInternalServerError)
59-
return
60-
}
61-
62-
// Copy headers from original request.
63-
for header, values := range r.Header {
64-
for _, value := range values {
65-
req.Header.Add(header, value)
66-
}
67-
}
68-
69-
// Perform the proxy request.
70-
client := &http.Client{}
71-
resp, err := client.Do(req)
72-
if err != nil {
73-
fmt.Printf("Proxy request failed %v", err)
74-
http.Error(w, "Proxy request failed", http.StatusBadGateway)
75-
return
76-
}
77-
78-
defer resp.Body.Close()
79-
80-
// Copy the response headers.
81-
for header, values := range resp.Header {
82-
for _, value := range values {
83-
w.Header().Add(header, value)
41+
ctx := context.Background()
42+
if err := server.Run(ctx, &cfg); err != nil {
43+
fmt.Fprintf(os.Stderr, "%s\n", err)
44+
os.Exit(1)
8445
}
85-
}
8646

87-
// Set the status code and write the response body.
88-
w.WriteHeader(resp.StatusCode)
89-
body, _ := io.ReadAll(resp.Body)
90-
w.Write(body)
47+
return nil
48+
},
9149
}
9250

9351
func init() {
9452
rootCmd.AddCommand(serverCmd)
9553

96-
serverCmd.Flags().
97-
StringVarP(&allowedOrigin, "allowed-origin", "a", "", "Restrict access to a specific allowed domain")
98-
serverCmd.Flags().
99-
StringVarP(&port, "port", "p", "", "Server Port")
54+
port = os.Getenv("PORT")
55+
allowedOrigin = os.Getenv("ALLOW_ORIGIN")
10056
}

pkg/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package config
2+
3+
type Config struct {
4+
Host string
5+
Port string
6+
AllowedOrigin string
7+
}

pkg/server/handlers.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
8+
"github.com/danvergara/jumble-proxy-server/pkg/config"
9+
)
10+
11+
// proxyHandler adds headers to overcome the CORS errors for the Jumble Nostr client.
12+
func proxyHandler(config *config.Config) func(w http.ResponseWriter, r *http.Request) {
13+
return func(w http.ResponseWriter, r *http.Request) {
14+
allowedOrigin := config.AllowedOrigin
15+
16+
if allowedOrigin == "" {
17+
allowedOrigin = "*"
18+
}
19+
20+
// add the paraters to fix CORS issues.
21+
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
22+
w.Header().Set("Access-Control-Allow-Methods", "GET")
23+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
24+
25+
// get the site of interest from the path parameters.
26+
site := r.PathValue("site")
27+
28+
// Send request to the target site.
29+
req, err := http.NewRequest(r.Method, site, r.Body)
30+
if err != nil {
31+
http.Error(w, "Failed to create request", http.StatusInternalServerError)
32+
return
33+
}
34+
35+
// Copy headers from original request.
36+
for header, values := range r.Header {
37+
for _, value := range values {
38+
req.Header.Add(header, value)
39+
}
40+
}
41+
42+
// Perform the proxy request.
43+
client := &http.Client{}
44+
resp, err := client.Do(req)
45+
if err != nil {
46+
fmt.Printf("proxy request failed %v", err)
47+
http.Error(w, "proxy request failed", http.StatusBadGateway)
48+
return
49+
}
50+
51+
defer resp.Body.Close()
52+
53+
// Copy the response headers.
54+
for header, values := range resp.Header {
55+
for _, value := range values {
56+
w.Header().Add(header, value)
57+
}
58+
}
59+
60+
// Set the status code and write the response body.
61+
w.WriteHeader(resp.StatusCode)
62+
body, _ := io.ReadAll(resp.Body)
63+
w.Write(body)
64+
}
65+
}

pkg/server/middlewares.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package server
2+
3+
import (
4+
"log"
5+
"net/http"
6+
)
7+
8+
func loggingMiddlware(next http.Handler) http.Handler {
9+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
10+
site := r.PathValue("site")
11+
log.Printf("fetching site: %s\n", site)
12+
next.ServeHTTP(w, r)
13+
})
14+
}

0 commit comments

Comments
 (0)