├── default.env
├── .gitignore
├── sample.env
├── examples
├── docker-compose.volume.yml
├── docker-compose.simple.yml
├── docker-compose.ssl.yml
└── docker-compose.proxy.yml
├── go.mod
├── Dockerfile
├── go.sum
├── LICENSE
├── main.go
└── README.md
/default.env:
--------------------------------------------------------------------------------
1 | PORT=80
2 | SSL="FALSE"
3 | SECRET="a-very-long-and-complicated-secret"
4 |
5 | MAX_SIZE=1024
6 | RECOVERY_LEVEL="Medium"
7 | ENABLE_LOGS="TRUE"
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | *.exe~
3 | *.dll
4 | *.so
5 | *.dylib
6 | *.test
7 | *.out
8 | *.backup
9 | go.work
10 | scripts
11 | .aliases
12 | .env
13 | cloud
14 |
--------------------------------------------------------------------------------
/sample.env:
--------------------------------------------------------------------------------
1 | PORT=80
2 | SSL=""
3 | SECRET=""
4 |
5 | MAX_SIZE=1024
6 | RECOVERY_LEVEL="<[Low|Medium|High|Highest]>"
7 | ENABLE_LOGS=""
8 |
--------------------------------------------------------------------------------
/examples/docker-compose.volume.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | bond:
4 | image: mosswill/bond:latest
5 | ports:
6 | - 80:80
7 | volumes:
8 | - .env:/.env
9 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module will-moss/bond
2 |
3 | go 1.21.4
4 |
5 | require (
6 | github.com/go-chi/chi v1.5.5
7 | github.com/joho/godotenv v1.5.1
8 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
9 | )
10 |
--------------------------------------------------------------------------------
/examples/docker-compose.simple.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | bond:
4 | image: mosswill/bond:latest
5 | ports:
6 | - "80:80"
7 | environment:
8 | PORT: "80"
9 | SECRET: ""
10 |
--------------------------------------------------------------------------------
/examples/docker-compose.ssl.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | bond:
4 | image: mosswill/bond:latest
5 | ports:
6 | - "443:443"
7 | volumes:
8 | - ./certificate.pem:/certificate.pem
9 | - ./key.pem:/key.pem
10 | environment:
11 | SSL: "TRUE"
12 | PORT: "443"
13 | SECRET: ""
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.21-alpine AS builder
2 |
3 | WORKDIR /app
4 |
5 | COPY go.mod .
6 | COPY go.sum .
7 | COPY main.go .
8 | COPY default.env .
9 |
10 | RUN CGO_ENABLED=0 GOOS=linux go build -o bond main.go
11 |
12 | FROM gcr.io/distroless/base
13 |
14 | WORKDIR /
15 |
16 | COPY --from=builder /app/bond .
17 | COPY --from=builder /app/default.env .
18 |
19 | ENTRYPOINT ["./bond"]
20 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
2 | github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
3 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
4 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
5 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
6 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
7 |
--------------------------------------------------------------------------------
/examples/docker-compose.proxy.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | bond:
4 | image: mosswill/bond:latest
5 | networks:
6 | - global
7 | expose:
8 | - 80
9 | environment:
10 | PORT: "80"
11 | SECRET: ""
12 |
13 | VIRTUAL_HOST: "your-domain.tld"
14 | VIRTUAL_PORT: "80"
15 | # Depending on your setup, you may also need
16 | # CERT_NAME: "default"
17 | # Or even
18 | # LETSENCRYPT_HOST: "your-domain.tld"
19 |
20 | proxy:
21 | image: jwilder/nginx-proxy
22 | networks:
23 | - global
24 | ports:
25 | - "443:443"
26 | volumes:
27 | - /var/run/docker.sock:/tmp/docker.sock:ro
28 |
29 | networks:
30 | # Assumption made : network "global" is created beforehand
31 | # with : docker network create global
32 | global:
33 | external: true
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Will Moss
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strconv"
7 | "os"
8 | "strings"
9 | "log"
10 |
11 | "github.com/joho/godotenv"
12 | "github.com/go-chi/chi"
13 | "github.com/go-chi/chi/middleware"
14 | qrcode "github.com/skip2/go-qrcode"
15 | )
16 |
17 | // Alias for os.GetEnv, with support for fallback value, and boolean normalization
18 | func getEnv(key string, fallback ...string) string {
19 | value, exists := os.LookupEnv(key)
20 | if !exists {
21 | if len(fallback) > 0 {
22 | value = fallback[0]
23 | } else {
24 | value = ""
25 | }
26 | } else {
27 | // Quotes removal
28 | value = strings.Trim(value, "\"")
29 |
30 | // Boolean normalization
31 | mapping := map[string]string{
32 | "0": "FALSE",
33 | "off": "FALSE",
34 | "false": "FALSE",
35 | "1": "TRUE",
36 | "on": "TRUE",
37 | "true": "TRUE",
38 | }
39 | normalized, isBool := mapping[strings.ToLower(value)]
40 | if isBool {
41 | value = normalized
42 | }
43 | }
44 |
45 | return value
46 | }
47 |
48 | // Entrypoint
49 | func main() {
50 | godotenv.Load("default.env")
51 |
52 | // Load custom settings via .env file
53 | err := godotenv.Overload(".env")
54 | if err != nil {
55 | log.Print("No .env file provided, will continue with system env")
56 | }
57 |
58 | // Define the keyword-int association for QR code recovery levels
59 | recoveryLevels := map[string]qrcode.RecoveryLevel{
60 | "LOW": qrcode.Low,
61 | "MEDIUM": qrcode.Medium,
62 | "HIGH": qrcode.High,
63 | "HIGHEST": qrcode.Highest,
64 | }
65 |
66 | // Instantiate server
67 | app := chi.NewRouter()
68 |
69 | // Set up basic middleware
70 | if getEnv("ENABLE_LOGS") == "TRUE" {
71 | app.Use(middleware.Logger)
72 | }
73 | app.Use(middleware.Recoverer)
74 |
75 | // CORS-specific
76 | app.Options("/", func(w http.ResponseWriter, r *http.Request) {
77 | w.Header().Set("Access-Control-Allow-Origin", "*")
78 | w.Header().Set("Access-Control-Allow-Methods", "GET")
79 | })
80 |
81 | // GET /
82 | app.Get("/", func(w http.ResponseWriter, r *http.Request) {
83 | w.Header().Set("Access-Control-Allow-Origin", "*")
84 | w.Header().Set("Access-Control-Allow-Methods", "GET")
85 |
86 | secret := r.URL.Query().Get("secret")
87 | content := r.URL.Query().Get("content")
88 | size := r.URL.Query().Get("size")
89 |
90 | // Ensure secret is provided and matches the one in store
91 | if secret != getEnv("SECRET") {
92 | w.WriteHeader(http.StatusForbidden)
93 | return
94 | }
95 |
96 | // Ensure Size and Content are provided
97 | if content == "" || size == "" {
98 | w.WriteHeader(http.StatusBadRequest)
99 | return
100 | }
101 |
102 | realSize, err := strconv.Atoi(size)
103 |
104 | if err != nil {
105 | w.WriteHeader(http.StatusInternalServerError)
106 | w.Write([]byte(err.Error()))
107 | return
108 | }
109 |
110 | maxSize, _ := strconv.Atoi(getEnv("MAX_SIZE"))
111 | if realSize <= 0 || realSize > maxSize {
112 | w.WriteHeader(http.StatusBadRequest)
113 | return
114 | }
115 |
116 | // Generate the QR code
117 | png, err := qrcode.Encode(content, recoveryLevels[strings.ToUpper(getEnv("RECOVERY_LEVEL"))], realSize)
118 |
119 | if err != nil {
120 | w.WriteHeader(http.StatusBadRequest)
121 | return
122 | }
123 |
124 | // Output the QR code
125 | _, err = w.Write(png)
126 |
127 | if err != nil {
128 | w.WriteHeader(http.StatusInternalServerError)
129 | log.Print(err.Error())
130 | return
131 | }
132 |
133 | w.Header().Set("Content-Type", "application/octet-stream")
134 | })
135 |
136 | log.Printf("Server starting on port %s", getEnv("PORT"))
137 | if getEnv("SSL") == "TRUE" {
138 | err = http.ListenAndServeTLS(fmt.Sprintf(":%s", getEnv("PORT")), "certificate.pem", "key.pem", app)
139 | } else {
140 | err = http.ListenAndServe(fmt.Sprintf(":%s", getEnv("PORT")), app)
141 | }
142 |
143 | if err != nil {
144 | log.Print(err.Error())
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Bond
3 |
4 | Self-hostable headless QR code generator
5 |
6 | Generate QR codes with a one-endpoint API
7 |
8 |
9 | Table of Contents -
10 | Install -
11 | Configure
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Table of Contents
21 |
22 | - [Introduction](#introduction)
23 | - [Features](#features)
24 | - [Deployment and Examples](#deployment-and-examples)
25 | * [Deploy with Docker](#deploy-with-docker)
26 | * [Deploy with Docker Compose](#deploy-with-docker-compose)
27 | * [Deploy as a standalone application](#deploy-as-a-standalone-application)
28 | - [Configuration](#configuration)
29 | - [API Reference](#api-reference)
30 | * [Generate a QR code](#generate-a-qr-code)
31 | - [Usage](#usage)
32 | * [Curl](#curl)
33 | * [Wget](#wget)
34 | * [Javascript](#javascript)
35 | - [Free Hosted Service](#free-hosted-service)
36 | - [Troubleshoot](#troubleshoot)
37 | - [Credits](#credits)
38 |
39 | ## Introduction
40 |
41 | Bond is a tiny, simple, and self-hostable service that enables you to generate QR codes by calling an API.
42 | It was born out of a need to generate QR codes for my business when I couldn't find a fully free and secure API without limitations, along with Google shutting down their service.
43 | I also wanted something rudimentary without gimmicks or customization (colors, redirection, logo, shapes, etc.), hence I decided to make it myself.
44 |
45 | ## Features
46 |
47 | Bond has all these features implemented :
48 | - Generate a QR code of any size with any content
49 | - Simple security using a defined secret to deter bots
50 | - Support for HTTP and HTTPS
51 | - Support for standalone / proxy deployment
52 |
53 | On top of these, one may appreciate the following characteristics :
54 | - Written in Go
55 | - Holds in a single file with few dependencies
56 | - Holds in a ~14 MB compressed Docker image
57 |
58 | For more information, read about [Configuration](#configuration) and [API Reference](#api-reference).
59 |
60 | ## Deployment and Examples
61 |
62 | ### Deploy with Docker
63 |
64 | You can run Bond with Docker on the command line very quickly.
65 |
66 | You can use the following commands :
67 |
68 | ```sh
69 | # Create a .env file
70 | touch .env
71 |
72 | # Edit .env file ...
73 |
74 | # Option 1 : Run Bond attached to the terminal (useful for debugging)
75 | docker run --env-file .env -p mosswill/bond
76 |
77 | # Option 2 : Run Bond as a daemon
78 | docker run -d --env-file .env -p mosswill/bond
79 | ```
80 |
81 | ### Deploy with Docker Compose
82 |
83 | To help you get started quickly, multiple example `docker-compose` files are located in the ["examples/"](examples) directory.
84 |
85 | Here's a description of every example :
86 |
87 | - `docker-compose.simple.yml`: Run Bond as a front-facing service on port 80, with environment variables supplied in the `docker-compose` file directly.
88 |
89 | - `docker-compose.volume.yml`: Run Bond as a front-facing service on port 80, with environment variables supplied as a `.env` file mounted as a volume.
90 |
91 | - `docker-compose.ssl.yml`: Run Bond as a front-facing service on port 443, listening for HTTPS requests, with certificate and private key provided as mounted volumes.
92 |
93 | - `docker-compose.proxy.yml`: A full setup with Bond running on port 80, behind a proxy listening on port 443.
94 |
95 | When your `docker-compose` file is on point, you can use the following commands :
96 | ```sh
97 | # Run Bond in the current terminal (useful for debugging)
98 | docker-compose up
99 |
100 | # Run Bond in a detached terminal (most common)
101 | docker-compose up -d
102 |
103 | # Show the logs written by Bond (useful for debugging)
104 | docker logs
105 | ```
106 |
107 | ### Deploy as a standalone application
108 |
109 | Deploying Bond as a standalone application assumes the following prerequisites :
110 | - You have Go installed on your server
111 | - You have properly filled your `.env` file
112 | - Your DNS and networking configuration is on point
113 |
114 | When all the prerequisites are met, you can run the following commands in your terminal :
115 |
116 | ```sh
117 | # Retrieve the code
118 | git clone https://github.com/will-moss/bond
119 | cd bond
120 |
121 | # Create a new .env file
122 | cp sample.env .env
123 |
124 | # Edit .env file ...
125 |
126 | # Build the code into an executable
127 | go build -o bond main.go
128 |
129 | # Option 1 : Run Bond in the current terminal
130 | ./bond
131 |
132 | # Option 2 : Run Bond as a background process
133 | ./bond &
134 |
135 | # Option 3 : Run Bond using screen
136 | screen -S bond
137 | ./bond
138 |
139 | ```
140 |
141 | ## Configuration
142 |
143 | To run Bond, you will need to set the following environment variables in a `.env` file located next to your executable :
144 |
145 | > **Note :** Regular environment variables provided on the commandline work too
146 |
147 | | Parameter | Type | Description | Default |
148 | | :---------------------- | :-------- | :------------------------- | ------- |
149 | | `SSL` | `boolean` | Whether HTTPS should be used in place of HTTP. When configured, Bond will look for `certificate.pem` and `key.pem` next to the executable for configuring SSL. Note that if Bond is behind a proxy that already handles SSL, this should be set to `false`. | False |
150 | | `PORT` | `integer` | The port Bond listens on. | 80 |
151 | | `SECRET` | `string` | The secret used to secure your Bond instance against bots / malicious usage. (This parameter can be left empty to disable security) | a-very-long-and-complicated-secret |
152 | | `MAX_SIZE` | `integer` | The max size for your QR codes, in pixels, such that a QR code can never be greater than MAX_SIZE x MAX_SIZE pixels. | 1024 |
153 | | `RECOVERY_LEVEL` | `string` | The recovery level used to generate the QR codes. One of : Low, Medium, High, and Highest (case-insensitive). | Medium |
154 | | `ENABLE_LOGS` | `boolean` | Whether all the HTTP requests should be displayed in the console / logs. | TRUE |
155 |
156 | > **Note :** Boolean values are case-insensitive, and can be represented via "ON" / "OFF" / "TRUE" / "FALSE" / 0 / 1.
157 |
158 | > **Tip :** You can generate a random secret with the following command :
159 |
160 | ```sh
161 | head -c 1024 /dev/urandom | base64 | tr -cd "[:lower:][:upper:][:digit:]" | head -c 32
162 | ```
163 |
164 | ## API Reference
165 |
166 | Bond exposes the following API, consisting of a single endpoint :
167 |
168 | #### Generate a QR code
169 |
170 | ```
171 | GET /
172 | ```
173 |
174 | | Parameter | Type | Description |
175 | | :-------- | :------- | :-------------------------------- |
176 | | `secret` | `string` | **Required.** Your server secret (can be empty if your `SECRET` setting is empty). |
177 | | `size` | `string` | **Required.** The size (in pixels) of the QR code to generate. (The QR code will be size x size pixels.) |
178 | | `content` | `string` | **Required.** The data to encode in the QR code. |
179 |
180 | The API will directly return the image representing the QR code generated using your settings.
181 |
182 | ## Usage
183 |
184 | To generate QR codes using Bond, you can copy and adapt the following examples :
185 |
186 | ### curl
187 |
188 | ```sh
189 | curl -o qr-code.png "https://bond.your-domain.tld/?content=YOUR-CONTENT&size=512&secret=YOUR-SECRET"
190 | ```
191 |
192 | ### wget
193 |
194 | ```sh
195 | wget -O qr-code.png "https://bond.your-domain.tld/?content=YOUR-CONTENT&size=512&secret=YOUR-SECRET"
196 | ```
197 |
198 | ### Javascript
199 |
200 | ```javascript
201 | async function to_qrcode(text) {
202 | const url = `https://bond.your-domain.tld/?` + new URLSearchParams({
203 | size: 512,
204 | content: text,
205 | secret: 'YOUR-SECRET'
206 | });
207 | let response = await fetch(url);
208 |
209 | if (response.status !== 200) {
210 | console.log('HTTP-Error: ' + response.status);
211 | return null;
212 | }
213 |
214 | const blob = await response.blob();
215 | const objectURL = URL.createObjectURL(blob);
216 |
217 | const image = document.createElement('img');
218 | image.src = objectURL;
219 |
220 | const container = document.getElementById('YOUR-CONTAINER');
221 | container.append(image);
222 | }
223 |
224 | await to_qrcode("YOUR-CONTENT");
225 | ```
226 |
227 | ## Free Hosted Service
228 |
229 | Using our hosted service, you can use Bond to generate QR codes for free and without account registration.
230 |
231 | The service is provided freely with the following characteristics :
232 | - **URL :** `https://endpoint.bond/`
233 | - **Secret :** `52e679fae92441942a2ed4390ad9e8639eab9347a74a19ebaa00ef4a5494f7f3`
234 | - **Limitations :**
235 | * **Max QR code size :** 512x512 pixels
236 | * **Recovery level :** Low
237 | * **HTTP verbs :** OPTIONS, GET
238 | * **Rate limit :** 1 request per second
239 | - **Miscellaneous :**
240 | * Requests are not logged
241 | * HTTPS is required
242 |
243 | For more advanced needs, please open an issue, or send an email to the address displayed on my Github Profile.
244 |
245 | ## Troubleshoot
246 |
247 | Should you encounter any issue running Bond, please refer to the following common problems that may occur.
248 |
249 | > If none of these matches your case, feel free to open an issue.
250 |
251 | #### Bond is unreachable over HTTP / HTTPS
252 |
253 | Please make sure that the following requirements are met :
254 |
255 | - If Bond runs as a standalone application without proxy :
256 | - Make sure your server / firewall accepts incoming connections on Bond's port.
257 | - Make sure your DNS configuration is correct. (Usually, such record should suffice : `A bond XXX.XXX.XXX.XXX` for `https://bond.your-server-tld`)
258 | - Make sure your `.env` file is well configured according to the [Configuration](#configuration) section.
259 |
260 | - If Bond runs behind Docker / a proxy :
261 | - Perform the previous (standalone) verifications first.
262 | - Make sure that `PORT` (Bond's port) is well set in `.env`.
263 | - Check your proxy forwarding rules.
264 |
265 | In any case, the crucial part is [Configuration](#configuration).
266 |
267 | #### Bond returns an error 4xx instead of a QR code
268 |
269 | Please make sure that :
270 | - You're using the `GET` HTTP method.
271 | - You've included the `secret` parameter, and the value of it equals the value of the `SECRET` defined in your `.env`.
272 | - The `size` you requested fits within the range 1 <= `size` <= `MAX_SIZE`.
273 |
274 |
275 | #### Something else
276 |
277 | Please feel free to open an issue, explaining what happens, and describing your environment.
278 |
279 | ## Credits
280 |
281 | Hey hey ! It's always a good idea to say thank you and mention the people and projects that help us move forward.
282 |
283 | Big thanks to the individuals / teams behind these projects :
284 | - [go-qrcode](https://github.com/skip2/go-qrcode) : For the QR code generation.
285 | - [go-chi](https://github.com/go-chi/chi) : For the web server.
286 | - The countless others!
287 |
288 | And don't forget to mention Bond if you like it or if it helps you in any way!
289 |
--------------------------------------------------------------------------------