├── 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 | --------------------------------------------------------------------------------