Skip to content

Commit c086233

Browse files
authored
Merge pull request #336 from DefangLabs/linda-nounly-go
Nounly Sample
2 parents 3d82eed + 48e7ea1 commit c086233

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+5589
-0
lines changed

.github/workflows/deploy-changed-samples.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,14 @@ jobs:
9292
TEST_NC_S3_ACCESS_SECRET: ${{ secrets.TEST_NC_S3_ACCESS_SECRET }}
9393
TEST_OPENAI_KEY: ${{ secrets.TEST_OPENAI_KEY }}
9494
TEST_POSTGRES_PASSWORD: ${{ secrets.TEST_POSTGRES_PASSWORD }}
95+
TEST_PROJECT_HONEYPOT_KEY: ${{ secrets.TEST_PROJECT_HONEYPOT_KEY}}
9596
TEST_QUEUE: ${{ secrets.TEST_QUEUE }}
9697
TEST_SECRET_KEY: ${{ secrets.TEST_SECRET_KEY }}
9798
TEST_SECRET_KEY_BASE: ${{ secrets.TEST_SECRET_KEY_BASE }}
9899
TEST_SESSION_SECRET: ${{ secrets.TEST_SESSION_SECRET }}
99100
TEST_SLACK_CHANNEL_ID: ${{ secrets.TEST_SLACK_CHANNEL_ID }}
100101
TEST_SLACK_TOKEN: ${{ secrets.TEST_SLACK_TOKEN }}
102+
TEST_SHARED_SECRETS: ${{ secrets.TEST_SHARED_SECRETS}}
101103
TEST_ALLOWED_HOSTS: ${{ secrets.TEST_ALLOWED_HOSTS }}
102104
run: |
103105
SAMPLES=$(sed 's|^samples/||' changed_samples.txt | paste -s -d ',' -)

samples/nounly/.dockerignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Default .dockerignore file for Defang
2+
**/__pycache__
3+
**/.direnv
4+
**/.DS_Store
5+
**/.envrc
6+
**/.git
7+
**/.github
8+
**/.idea
9+
**/.next
10+
**/.vscode
11+
**/compose.*.yaml
12+
**/compose.*.yml
13+
**/compose.yaml
14+
**/compose.yml
15+
**/docker-compose.*.yaml
16+
**/docker-compose.*.yml
17+
**/docker-compose.yaml
18+
**/docker-compose.yml
19+
**/node_modules
20+
**/Thumbs.db
21+
Dockerfile
22+
*.Dockerfile
23+
# Ignore our own binary, but only in the root to avoid ignoring subfolders
24+
defang
25+
defang.exe
26+
# Ignore our project-level state
27+
.defang
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Deploy
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
deploy:
10+
environment: playground
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
id-token: write
15+
16+
steps:
17+
- name: Checkout Repo
18+
uses: actions/checkout@v4
19+
20+
- name: Deploy
21+
uses: DefangLabs/[email protected]
22+
with:
23+
config-env-vars: PROJECT_HONEYPOT_KEY SHARED_SECRETS
24+
env:
25+
PROJECT_HONEYPOT_KEY: ${{ secrets.PROJECT_HONEYPOT_KEY }}
26+
SHARED_SECRETS: ${{ secrets.SHARED_SECRETS }}

samples/nounly/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dump.rdb

samples/nounly/Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# syntax=docker/dockerfile:1.4
2+
ARG GOVERSION=1.22
3+
ARG BASE=busybox # for wget healtcheck
4+
5+
FROM --platform=${BUILDPLATFORM} golang:${GOVERSION} AS build
6+
# These two are automatically set by docker buildx
7+
ARG TARGETARCH
8+
ARG TARGETOS
9+
WORKDIR /src
10+
COPY --link go.mod go.sum* ./
11+
ARG GOPRIVATE=github.com/lionello/nounly-go
12+
RUN go mod download
13+
ARG GOSRC=.
14+
COPY --link ${GOSRC} ./
15+
# RUN go test -v ./... FIXME: "no required module provides package github.com/defang-io/defang-mvp/fabric/internal/util_test"
16+
ARG BUILD=./cmd/server
17+
ARG VERSION
18+
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -buildvcs=false -ldflags="-w -s -X \"main.version=${VERSION}\"" -o /server "${BUILD}"
19+
20+
FROM --platform=${TARGETPLATFORM} ${BASE}
21+
# RUN apk add --update curl ca-certificates && rm -rf /var/cache/apk* # Certificates for SSL
22+
ARG PORT=80
23+
ENV PORT=$PORT
24+
COPY --link --from=build /server /server
25+
COPY --link ./public /public
26+
ENTRYPOINT [ "/server" ]
27+
EXPOSE $PORT
28+
# USER nobody

samples/nounly/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Nounly
2+
3+
[![1-click-deploy](https://defang.io/deploy-with-defang.png)](https://portal.defang.dev/redirect?url=https%3A%2F%2Fgithub.com%2Fnew%3Ftemplate_name%3Dsample-nounly-template%26template_owner%3DDefangSamples)
4+
5+
Nounly (also known as Noun.ly) is a URL shortener website built with Go, JavaScript and Redis, and can be deployed with Defang as a sample.
6+
7+
The URL shortener appends a noun to the end of a Defang-deployed URL to create the new shortened URL. For reference, you can view the real [Noun.ly](https://noun.ly/) website here.
8+
9+
_"Share the web through words."_ - Creator of Nounly
10+
11+
## Prerequisites
12+
13+
1. Download [Defang CLI](https://github.com/DefangLabs/defang)
14+
2. (Optional) If you are using [Defang BYOC](https://docs.defang.io/docs/concepts/defang-byoc) authenticate with your cloud provider account
15+
3. (Optional for local development) [Docker CLI](https://docs.docker.com/engine/install/)
16+
17+
## Development
18+
19+
To run the application locally, you can use the following command:
20+
21+
```bash
22+
docker compose up --build
23+
```
24+
25+
## Configuration
26+
27+
For this sample, you will need to provide the following [configuration](https://docs.defang.io/docs/concepts/configuration):
28+
29+
> Note that if you are using the 1-click deploy option, you can set these values as secrets in your GitHub repository and the action will automatically deploy them for you.
30+
31+
### `PROJECT_HONEYPOT_KEY`
32+
33+
A [Project Honey Pot API](https://www.projecthoneypot.org/index.php) key that is used for anti-spamming. It is optional, but please include a non-empty string value.
34+
35+
```bash
36+
defang config set PROJECT_HONEYPOT_KEY
37+
```
38+
39+
### `SHARED_SECRETS`
40+
41+
A JSON object string of shared secrets that are used for API clients. It is optional, and the default value is `{}` if you do not have any shared secrets.
42+
43+
```bash
44+
defang config set SHARED_SECRETS
45+
```
46+
47+
## Deployment
48+
49+
> [!NOTE]
50+
> Download [Defang CLI](https://github.com/DefangLabs/defang)
51+
52+
### Defang Playground
53+
54+
Deploy your application to the Defang Playground by opening up your terminal and typing:
55+
56+
```bash
57+
defang compose up
58+
```
59+
60+
### BYOC
61+
62+
If you want to deploy to your own cloud account, you can [use Defang BYOC](https://docs.defang.io/docs/tutorials/deploy-to-your-cloud).
63+
64+
---
65+
66+
Title: Nounly
67+
68+
Short Description: A URL shortener website built with Go, JavaScript, and Redis.
69+
70+
Tags: Go, JavaScript, Redis, URL shortener
71+
72+
Languages: golang, javascript
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package api
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/base64"
7+
"encoding/json"
8+
"net/http"
9+
"os"
10+
"strings"
11+
"time"
12+
)
13+
14+
// TODO: store these in a key-value store (redis)
15+
var sharedSecrets = map[string][]byte{}
16+
17+
func init() {
18+
if val := os.Getenv("SHARED_SECRETS"); val != "" {
19+
if err := json.Unmarshal([]byte(val), &sharedSecrets); err != nil {
20+
panic(err)
21+
}
22+
}
23+
}
24+
25+
func checkAuth(r *http.Request) bool {
26+
authHeader := r.Header.Get("Authorization")
27+
if authHeader == "" {
28+
return false
29+
}
30+
31+
token, ok := strings.CutPrefix(authHeader, "SharedKey ")
32+
if !ok {
33+
return false
34+
}
35+
36+
parts := strings.SplitN(token, ":", 2)
37+
sharedSecret := sharedSecrets[parts[0]]
38+
if len(parts) != 2 || sharedSecret == nil {
39+
return false
40+
}
41+
42+
date := r.Header.Get("x-date")
43+
if date == "" {
44+
date = r.Header.Get("Date")
45+
}
46+
if !VerifyDate(date, time.Now()) {
47+
return false
48+
}
49+
50+
if sign(sharedSecret, r.Method, date, r.RequestURI) != parts[1] {
51+
return false
52+
}
53+
54+
// TODO: set CORS headers to allow authorized access from anywhere
55+
return true
56+
}
57+
58+
func signRaw(secret, fields []byte) string {
59+
hmac := hmac.New(sha256.New, secret)
60+
return base64.StdEncoding.EncodeToString(hmac.Sum(fields))
61+
}
62+
63+
func sign(secret []byte, fields ...string) string {
64+
return signRaw(secret, []byte(strings.Join(fields, "\n")))
65+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package api
2+
3+
import (
4+
"net"
5+
"net/http"
6+
"strings"
7+
"time"
8+
)
9+
10+
const clockSkew = 5 * time.Minute
11+
12+
func GetClientIP(req *http.Request) string {
13+
// get the client IP from the proxy (if any); TODO: prevent spoofing
14+
xff := req.Header.Get("x-forwarded-for")
15+
if xff != "" {
16+
comma := strings.Index(xff, ",")
17+
if comma == -1 {
18+
return canonicalIP(xff)
19+
}
20+
return canonicalIP(xff[:comma])
21+
}
22+
return canonicalIP(strings.SplitN(req.RemoteAddr, ":", 2)[0])
23+
}
24+
25+
func canonicalIP(ip string) string {
26+
return net.ParseIP(ip).String()
27+
}
28+
29+
func VerifyDate(dateHeader string, now time.Time) bool {
30+
date, err := time.Parse(time.RFC3339, dateHeader)
31+
if err != nil {
32+
return false
33+
}
34+
diff := now.Sub(date)
35+
return diff > -clockSkew && diff < clockSkew
36+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package api
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestCanonicalIP(t *testing.T) {
9+
tests := []struct {
10+
ip string
11+
want string
12+
}{
13+
{"::ffff:83.90.47.30", "83.90.47.30"},
14+
{"83.90.47.30", "83.90.47.30"},
15+
{"fe80::676d:489:1f16:2c1", "fe80::676d:489:1f16:2c1"},
16+
}
17+
for _, tt := range tests {
18+
t.Run(tt.ip, func(t *testing.T) {
19+
if got := canonicalIP(tt.ip); got != tt.want {
20+
t.Errorf("canonicalIP() = %v, want %v", got, tt.want)
21+
}
22+
})
23+
}
24+
}
25+
26+
func TestVerifyDate(t *testing.T) {
27+
now := time.Date(2023, 1, 13, 17, 51, 0, 0, time.UTC)
28+
tests := []struct {
29+
date string
30+
now time.Time
31+
want bool
32+
}{
33+
{
34+
date: "2023-01-13T17:51:09.583Z",
35+
want: true,
36+
},
37+
{
38+
date: "2023-01-13T17:59:09.583Z",
39+
want: false,
40+
},
41+
{
42+
date: "2020-01-01T17:59:09.583Z",
43+
want: false,
44+
},
45+
{
46+
date: "invalid date",
47+
want: false,
48+
},
49+
}
50+
for _, tt := range tests {
51+
t.Run(tt.date, func(t *testing.T) {
52+
if got := VerifyDate(tt.date, now); got != tt.want {
53+
t.Errorf("verifyDate() = %v, want %v", got, tt.want)
54+
}
55+
})
56+
}
57+
}
58+
59+
func TestSign(t *testing.T) {
60+
secret := []byte("secret")
61+
fields := []string{"GET", "2023-01-13T17:51:09.583Z", "/api/v1/urls"}
62+
want := "R0VUCjIwMjMtMDEtMTNUMTc6NTE6MDkuNTgzWgovYXBpL3YxL3VybHP55m4Xm2dHrlQQj4L4reizwl12/TCv3mw5WCLFMBlhaQ=="
63+
if got := sign(secret, fields...); got != want {
64+
t.Errorf("sign() = %v, want %v", got, want)
65+
}
66+
}

0 commit comments

Comments
 (0)