├── .gitignore
├── version.yaml
├── ui
├── src
│ ├── assets
│ │ ├── Images
│ │ │ ├── logo.png
│ │ │ ├── banner.png
│ │ │ └── favicon.ico
│ │ └── style.css
│ ├── stores
│ │ └── formStore.js
│ ├── utils
│ │ ├── fileDownloader.js
│ │ └── passwordGenerator.js
│ ├── router
│ │ └── index.js
│ ├── pages
│ │ ├── ErrorGeneral.vue
│ │ ├── Error404.vue
│ │ ├── View.vue
│ │ └── Create.vue
│ ├── components
│ │ ├── SecretDisplay.vue
│ │ └── PasswordInput.vue
│ ├── main.js
│ ├── App.vue
│ └── services
│ │ └── api.js
├── package.json
├── vite.config.js
├── index.html
└── package-lock.json
├── internal
├── database
│ ├── helpers.go
│ ├── database.go
│ └── database_test.go
├── models
│ └── send.go
├── security
│ ├── hash.go
│ ├── security_test.go
│ ├── hash_test.go
│ └── security.go
├── config
│ ├── config.go
│ └── config_test.go
├── routes
│ ├── routes.go
│ └── routes_test.go
└── handlers
│ ├── handlers_test.go
│ └── handlers.go
├── nginx.conf
├── Makefile
├── docker-compose.prod.sample.yaml
├── LICENSE
├── cmd
└── server
│ └── main.go
├── docker-compose.yaml
├── SECURITY.md
├── go.mod
├── Dockerfile
├── .github
└── workflows
│ ├── build&test.yaml
│ └── build-and-push-docker.yaml
├── CHANGELOG.md
├── Readme.md
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
3 | ui/dist
4 | /.vs
5 | /.vscode
6 |
--------------------------------------------------------------------------------
/version.yaml:
--------------------------------------------------------------------------------
1 | #Application version following https://semver.org/
2 | version: 1.0.9
--------------------------------------------------------------------------------
/ui/src/assets/Images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kek-Sec/GopherDrop/HEAD/ui/src/assets/Images/logo.png
--------------------------------------------------------------------------------
/ui/src/assets/Images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kek-Sec/GopherDrop/HEAD/ui/src/assets/Images/banner.png
--------------------------------------------------------------------------------
/ui/src/assets/Images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kek-Sec/GopherDrop/HEAD/ui/src/assets/Images/favicon.ico
--------------------------------------------------------------------------------
/internal/database/helpers.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import "os"
4 |
5 | // removeFile deletes a file from the filesystem.
6 | func removeFile(path string) {
7 | os.Remove(path)
8 | }
9 |
--------------------------------------------------------------------------------
/internal/models/send.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | // Send represents a stored secret send, either text or file.
6 | type Send struct {
7 | Hash string `gorm:"primary_key"` // Random hash as the unique identifier
8 | Type string
9 | Data string
10 | FilePath string
11 | FileName string
12 | Password string
13 | OneTime bool
14 | ExpiresAt time.Time
15 | CreatedAt time.Time
16 | UpdatedAt time.Time
17 | }
18 |
--------------------------------------------------------------------------------
/ui/src/stores/formStore.js:
--------------------------------------------------------------------------------
1 | import { reactive } from 'vue';
2 |
3 | /**
4 | * A simple reactive store to handle cross-component communication
5 | * for resetting the create form.
6 | */
7 | export const formStore = reactive({
8 | // A counter that components can watch. Incrementing it signals a reset.
9 | resetCounter: 0,
10 |
11 | // Function to trigger the reset from anywhere in the app.
12 | triggerReset() {
13 | this.resetCounter++;
14 | }
15 | });
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gopherdrop-ui",
3 | "version": "1.0.1",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "preview": "vite preview"
8 | },
9 | "dependencies": {
10 | "@mdi/font": "^7.4.47",
11 | "vite-plugin-vuetify": "^1.0.1",
12 | "vue": "^3.3.4",
13 | "vue-router": "^4.2.5",
14 | "vuetify": "^3.7.5"
15 | },
16 | "devDependencies": {
17 | "@vitejs/plugin-vue": "^4.0.0",
18 | "vite": "^4.5.9"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/internal/security/hash.go:
--------------------------------------------------------------------------------
1 | package security
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/base64"
6 | "errors"
7 | )
8 |
9 | // GenerateHash creates a secure random hash of the specified length.
10 | func GenerateHash(length int) (string, error) {
11 | if length < 0 {
12 | return "", errors.New("length must be a non-negative integer")
13 | }
14 |
15 | bytes := make([]byte, length)
16 | _, err := rand.Read(bytes)
17 | if err != nil {
18 | return "", err
19 | }
20 | return base64.URLEncoding.EncodeToString(bytes)[:length], nil
21 | }
22 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name localhost;
4 |
5 | client_max_body_size 0;
6 |
7 | root /usr/share/nginx/html;
8 | index index.html;
9 |
10 | # Serve the frontend
11 | location / {
12 | try_files $uri $uri/ /index.html;
13 | }
14 |
15 | # Proxy API requests to the backend service
16 | location /api/ {
17 | proxy_pass http://app:8080/;
18 | proxy_set_header Host $host;
19 | proxy_set_header X-Real-IP $remote_addr;
20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
21 | proxy_set_header X-Forwarded-Proto $scheme;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ui/src/utils/fileDownloader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @module fileDownloader
3 | * @description Provides a function for downloading files in the browser.
4 | */
5 |
6 | /**
7 | * Triggers a browser download for a file blob.
8 | * @param {Blob} blob - The file blob to download.
9 | * @param {string} filename - The desired name for the downloaded file.
10 | */
11 | export function downloadFile(blob, filename) {
12 | const url = URL.createObjectURL(blob);
13 | const a = document.createElement('a');
14 | a.href = url;
15 | a.download = filename || 'download';
16 | document.body.appendChild(a);
17 | a.click();
18 | document.body.removeChild(a);
19 | URL.revokeObjectURL(url);
20 | }
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build build-debug up up-debug down logs dbshell start-db stop-db test ui-build ui-up ui-down
2 |
3 | # Default build (production)
4 | build:
5 | docker compose build
6 |
7 | # Build in debug mode
8 | build-debug:
9 | docker compose build --build-arg DEBUG=true --build-arg GIN_MODE=debug
10 |
11 | up:
12 | docker compose up -d
13 |
14 | down:
15 | docker compose down
16 |
17 | logs:
18 | docker compose logs -f app
19 |
20 | dbshell:
21 | docker compose exec db psql -U $(DB_USER) -d $(DB_NAME)
22 |
23 | start-db:
24 | docker compose up -d db
25 |
26 | stop-db:
27 | docker compose stop db
28 |
29 | test:
30 | go test ./... -v
31 |
32 | ui-build:
33 | @echo "Building UI..."
34 | cd ui && npm install && npm run build
35 |
36 | ui-up:
37 | cd ui && npm run dev
38 |
--------------------------------------------------------------------------------
/ui/src/utils/passwordGenerator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @module passwordGenerator
3 | * @description Provides a function for generating random passwords.
4 | */
5 |
6 | /**
7 | * Generates a random password.
8 | * @param {number} length - The length of the password.
9 | * @param {string} charset - The character set to use for the password.
10 | * @returns {string} The generated password.
11 | */
12 | export function generatePassword(length = 12, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+') {
13 | let generatedPassword = '';
14 | const randomValues = new Uint32Array(length);
15 | window.crypto.getRandomValues(randomValues);
16 | for (let i = 0; i < length; i++) {
17 | const randomIndex = randomValues[i] % charset.length;
18 | generatedPassword += charset[randomIndex];
19 | }
20 | return generatedPassword;
21 | }
--------------------------------------------------------------------------------
/ui/src/router/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sets up the Vue Router.
3 | * The import paths are now relative to the `src/router/` directory.
4 | */
5 | import { createRouter, createWebHistory } from 'vue-router';
6 | import Create from '../pages/Create.vue'; // Corrected Path
7 | import View from '../pages/View.vue'; // Corrected Path
8 | import Error404 from '../pages/Error404.vue'; // Corrected Path
9 | import ErrorGeneral from '../pages/ErrorGeneral.vue'; // Corrected Path
10 |
11 | const routes = [
12 | { path: '/', component: Create, name: 'create' },
13 | { path: '/view/:hash', component: View, name: 'view' },
14 | { path: '/error', component: ErrorGeneral, name: 'error' },
15 | { path: '/:pathMatch(.*)*', component: Error404, name: 'not-found' }
16 | ];
17 |
18 | const router = createRouter({
19 | history: createWebHistory(),
20 | routes
21 | });
22 |
23 | export default router;
24 |
--------------------------------------------------------------------------------
/ui/src/pages/ErrorGeneral.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mdi-close-octagon-outline
5 | Oops! Something Went Wrong
6 |
7 | An unexpected error occurred. Please try again later.
8 |
9 |
10 | mdi-home
11 | Go Back Home
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
--------------------------------------------------------------------------------
/ui/src/assets/style.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Global styles for responsive and clean layout.
3 | */
4 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
5 |
6 | html, body {
7 | margin: 0;
8 | padding: 0;
9 | font-family: 'Inter', Arial, sans-serif;
10 | color: #333;
11 | background: #f4f4f9; /* Fallback background */
12 | }
13 |
14 | /* Add a subtle gradient background */
15 | body {
16 | background: linear-gradient(180deg, rgba(232,222,248,0.3) 0%, rgba(255,255,255,1) 30%);
17 | }
18 |
19 | .v-application {
20 | background: transparent !important;
21 | }
22 |
23 |
24 | a {
25 | color: #6750A4; /* Using the new primary color */
26 | text-decoration: none;
27 | transition: color 0.3s ease;
28 | }
29 |
30 | a:hover {
31 | text-decoration: underline;
32 | }
33 |
34 | @media (max-width: 600px) {
35 | main, header {
36 | padding: 0.5rem;
37 | }
38 | }
--------------------------------------------------------------------------------
/ui/src/pages/Error404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mdi-alert-circle-outline
5 | 404 - Page Not Found
6 |
7 | Sorry, the page you are looking for does not exist or has been moved.
8 |
9 |
10 | mdi-home
11 | Go Home
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
--------------------------------------------------------------------------------
/ui/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import vuetify from 'vite-plugin-vuetify'
4 |
5 | // Create HTML plugin instance with template parameters
6 | const htmlPlugin = () => {
7 | return {
8 | name: 'html-transform',
9 | transformIndexHtml(html) {
10 | return html.replace(
11 | /__TITLE__/g,
12 | process.env.VITE_APP_TITLE || 'GopherDrop'
13 | ).replace(
14 | /__DESCRIPTION__/g,
15 | process.env.VITE_APP_DESCRIPTION || 'Secure one-time secret and file sharing'
16 | )
17 | }
18 | }
19 | }
20 |
21 | export default defineConfig({
22 | plugins: [vue(), vuetify(), htmlPlugin()],
23 | define: {
24 | 'process.env.VITE_APP_TITLE': JSON.stringify(process.env.VITE_APP_TITLE || 'GopherDrop'),
25 | 'process.env.VITE_APP_DESCRIPTION': JSON.stringify(process.env.VITE_APP_DESCRIPTION || 'Secure one-time secret and file sharing')
26 | }
27 | })
28 |
--------------------------------------------------------------------------------
/internal/security/security_test.go:
--------------------------------------------------------------------------------
1 | package security
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestPadKey(t *testing.T) {
8 | key := PadKey("short")
9 | if len(key) != 32 {
10 | t.Fatalf("expected key length 32, got %d", len(key))
11 | }
12 | }
13 |
14 | func TestEncryptDecryptData(t *testing.T) {
15 | key := []byte(PadKey("password"))
16 | original := []byte("hello world")
17 | enc, err := EncryptData(original, key)
18 | if err != nil {
19 | t.Fatalf("encryption failed: %v", err)
20 | }
21 | dec, err := DecryptData(enc, key)
22 | if err != nil {
23 | t.Fatalf("decryption failed: %v", err)
24 | }
25 | if string(dec) != string(original) {
26 | t.Fatalf("expected '%s' got '%s'", original, dec)
27 | }
28 | }
29 |
30 | func TestGenerateHash(t *testing.T) {
31 | hash, err := GenerateHash(16)
32 | if err != nil {
33 | t.Fatalf("failed to generate hash: %v", err)
34 | }
35 | if len(hash) != 16 {
36 | t.Fatalf("expected length 16, got %d", len(hash))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/internal/database/database.go:
--------------------------------------------------------------------------------
1 | // Package database handles database connection and maintenance routines.
2 | package database
3 |
4 | import (
5 | "log"
6 | "time"
7 |
8 | "github.com/jinzhu/gorm"
9 | _ "github.com/lib/pq"
10 |
11 | "github.com/kek-Sec/gopherdrop/internal/config"
12 | "github.com/kek-Sec/gopherdrop/internal/models"
13 | )
14 |
15 | // InitDB connects to PostgreSQL using environment variables.
16 | func InitDB(cfg config.Config) *gorm.DB {
17 | dsn := "host=" + cfg.DBHost + " user=" + cfg.DBUser + " password=" + cfg.DBPass + " dbname=" + cfg.DBName + " sslmode=" + cfg.DBSSLMode
18 | db, err := gorm.Open("postgres", dsn)
19 | if err != nil {
20 | log.Fatal(err)
21 | }
22 | return db
23 | }
24 |
25 | // CleanupExpired periodically removes expired sends from the database.
26 | func CleanupExpired(db *gorm.DB) {
27 | for {
28 | time.Sleep(time.Hour)
29 | var sends []models.Send
30 | db.Where("expires_at < ?", time.Now()).Find(&sends)
31 | for _, s := range sends {
32 | if s.Type == "file" && s.FilePath != "" {
33 | removeFile(s.FilePath)
34 | }
35 | db.Delete(&s)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/docker-compose.prod.sample.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | image: postgres:17-alpine
4 | container_name: gopherdrop_db
5 | environment:
6 | POSTGRES_USER: user
7 | POSTGRES_PASSWORD: pass
8 | POSTGRES_DB: gopherdropdb
9 | volumes:
10 | - ./db_data:/var/lib/postgresql/data
11 | healthcheck:
12 | test: ["CMD-SHELL", "pg_isready -U user -d gopherdropdb"]
13 | interval: 10s
14 | timeout: 5s
15 | retries: 5
16 | networks:
17 | - gophernet
18 |
19 | app:
20 | image: petrakisg/gopherdrop:1.0.4
21 | container_name: gopherdrop_app
22 | environment:
23 | DB_HOST: db
24 | DB_USER: user
25 | DB_PASSWORD: pass
26 | DB_NAME: gopherdropdb
27 | DB_SSLMODE: disable
28 | SECRET_KEY: supersecretkeysupersecretkey32
29 | LISTEN_ADDR: :8080
30 | STORAGE_PATH: /app/storage
31 | MAX_FILE_SIZE: 10485760
32 | depends_on:
33 | db:
34 | condition: service_healthy
35 | volumes:
36 | - ./app_storage:/app/storage
37 | networks:
38 | - gophernet
39 |
40 | networks:
41 | gophernet:
42 | external: true
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2024] [Petrakis Georgios]
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 |
--------------------------------------------------------------------------------
/cmd/server/main.go:
--------------------------------------------------------------------------------
1 | // Package main starts the GopherDrop server.
2 | package main
3 |
4 | import (
5 | "log"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/kek-Sec/gopherdrop/internal/config"
10 | "github.com/kek-Sec/gopherdrop/internal/database"
11 | "github.com/kek-Sec/gopherdrop/internal/models"
12 | "github.com/kek-Sec/gopherdrop/internal/routes"
13 | )
14 |
15 | var version = "dev"
16 |
17 | // main initializes configuration, database, and the HTTP server.
18 | func main() {
19 | log.Printf("Starting GopherDrop: %s", version)
20 |
21 | // Load configuration
22 | cfg := config.LoadConfig()
23 |
24 | // Initialize database
25 | db := database.InitDB(cfg)
26 | db.AutoMigrate(&models.Send{})
27 | go database.CleanupExpired(db)
28 |
29 | // Setup router
30 | r := routes.SetupRouter(cfg, db)
31 |
32 | // Configure HTTP server with timeouts
33 | server := &http.Server{
34 | Addr: cfg.ListenAddr,
35 | Handler: r,
36 | ReadTimeout: 15 * time.Second,
37 | WriteTimeout: 15 * time.Second,
38 | IdleTimeout: 60 * time.Second,
39 | }
40 |
41 | // Start the server
42 | log.Printf("Listening on %s", cfg.ListenAddr)
43 | log.Fatal(server.ListenAndServe())
44 | }
45 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | image: postgres:17-alpine
4 | container_name: gopherdrop_db
5 | environment:
6 | POSTGRES_USER: user
7 | POSTGRES_PASSWORD: pass
8 | POSTGRES_DB: gopherdropdb
9 | volumes:
10 | - db_data:/var/lib/postgresql/data
11 | healthcheck:
12 | test: ["CMD-SHELL", "pg_isready -U user -d gopherdropdb"]
13 | interval: 10s
14 | timeout: 5s
15 | retries: 5
16 | networks:
17 | - gopherdrop
18 |
19 | app:
20 | build:
21 | context: .
22 | dockerfile: Dockerfile
23 | args:
24 | - VITE_API_URL=/api
25 | - VITE_APP_TITLE=TEST
26 | - VITE_APP_DESCRIPTION=TEST_DESCR
27 | container_name: gopherdrop_app
28 | environment:
29 | DB_HOST: db
30 | DB_USER: user
31 | DB_PASSWORD: pass
32 | DB_NAME: gopherdropdb
33 | DB_SSLMODE: disable
34 | SECRET_KEY: supersecretkeysupersecretkey32
35 | LISTEN_ADDR: :8080
36 | STORAGE_PATH: /app/storage
37 | MAX_FILE_SIZE: 10485760
38 | depends_on:
39 | db:
40 | condition: service_healthy
41 | ports:
42 | - "8081:80"
43 | networks:
44 | - gopherdrop
45 |
46 | networks:
47 | gopherdrop:
48 | driver: bridge
49 |
50 | volumes:
51 | db_data: {}
52 |
--------------------------------------------------------------------------------
/ui/src/components/SecretDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ textContent }}
5 |
6 |
7 |
8 |
9 | mdi-download Download File
10 |
11 |
12 |
13 |
14 | mdi-content-copy Copy Secret
15 |
16 |
17 |
18 | Text copied to clipboard!
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/internal/security/hash_test.go:
--------------------------------------------------------------------------------
1 | package security
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGenerateHash_ValidLength(t *testing.T) {
10 | length := 16
11 | hash, err := GenerateHash(length)
12 |
13 | assert.NoError(t, err, "GenerateHash should not return an error")
14 | assert.Equal(t, length, len(hash), "Generated hash should have the correct length")
15 | }
16 |
17 | func TestGenerateHash_ZeroLength(t *testing.T) {
18 | length := 0
19 | hash, err := GenerateHash(length)
20 |
21 | assert.NoError(t, err, "GenerateHash should not return an error for zero length")
22 | assert.Equal(t, 0, len(hash), "Generated hash should be empty for zero length")
23 | }
24 | func TestGenerateHash_NegativeLength(t *testing.T) {
25 | length := -1
26 | hash, err := GenerateHash(length)
27 |
28 | assert.Error(t, err, "GenerateHash should return an error for negative length")
29 | assert.Empty(t, hash, "Generated hash should be empty for negative length")
30 | }
31 |
32 |
33 | func TestGenerateHash_UniqueHashes(t *testing.T) {
34 | length := 16
35 | hash1, err1 := GenerateHash(length)
36 | hash2, err2 := GenerateHash(length)
37 |
38 | assert.NoError(t, err1, "First GenerateHash call should not return an error")
39 | assert.NoError(t, err2, "Second GenerateHash call should not return an error")
40 | assert.NotEqual(t, hash1, hash2, "Generated hashes should be unique")
41 | }
42 |
--------------------------------------------------------------------------------
/ui/src/components/PasswordInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 | {{ showPassword ? 'mdi-eye-off' : 'mdi-eye' }}
15 |
16 |
17 |
18 |
19 |
20 |
21 | mdi-refresh
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | // Package config provides application configuration loading from environment variables.
2 | package config
3 |
4 | import (
5 | "os"
6 | "strconv"
7 | )
8 |
9 | // Config holds all environment-based configuration.
10 | type Config struct {
11 | DBHost string
12 | DBUser string
13 | DBPass string
14 | DBName string
15 | DBSSLMode string
16 | SecretKey string
17 | ListenAddr string
18 | StoragePath string
19 | MaxFileSize int64
20 | }
21 |
22 | // LoadConfig returns a Config object populated from environment variables.
23 | func LoadConfig() Config {
24 | cfg := Config{
25 | DBHost: os.Getenv("DB_HOST"),
26 | DBUser: os.Getenv("DB_USER"),
27 | DBPass: os.Getenv("DB_PASSWORD"),
28 | DBName: os.Getenv("DB_NAME"),
29 | DBSSLMode: os.Getenv("DB_SSLMODE"),
30 | SecretKey: os.Getenv("SECRET_KEY"),
31 | ListenAddr: os.Getenv("LISTEN_ADDR"),
32 | StoragePath: os.Getenv("STORAGE_PATH"),
33 | }
34 |
35 | if cfg.ListenAddr == "" {
36 | cfg.ListenAddr = ":8080"
37 | }
38 | if cfg.StoragePath == "" {
39 | cfg.StoragePath = "/tmp"
40 | }
41 |
42 | maxFileSizeStr := os.Getenv("MAX_FILE_SIZE")
43 | if maxFileSizeStr == "" {
44 | // Default 10MB if not set
45 | cfg.MaxFileSize = 10 * 1024 * 1024
46 | } else {
47 | size, err := strconv.ParseInt(maxFileSizeStr, 10, 64)
48 | if err != nil {
49 | cfg.MaxFileSize = 10 * 1024 * 1024
50 | } else {
51 | cfg.MaxFileSize = size
52 | }
53 | }
54 |
55 | return cfg
56 | }
57 |
--------------------------------------------------------------------------------
/internal/routes/routes.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-contrib/cors"
7 | "github.com/gin-gonic/gin"
8 | "github.com/jinzhu/gorm"
9 | "golang.org/x/time/rate"
10 |
11 | "github.com/kek-Sec/gopherdrop/internal/handlers"
12 | "github.com/kek-Sec/gopherdrop/internal/config"
13 | )
14 |
15 | // Define a rate limiter with 1 request per second and a burst of 5.
16 | var limiter = rate.NewLimiter(1, 6)
17 |
18 | // rateLimiterMiddleware applies rate limiting to the endpoint.
19 | func rateLimiterMiddleware(c *gin.Context) {
20 | if !limiter.Allow() {
21 | c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
22 | c.Abort()
23 | return
24 | }
25 | c.Next()
26 | }
27 |
28 | // SetupRouter initializes the Gin router with routes and middleware.
29 | func SetupRouter(cfg config.Config, db *gorm.DB) *gin.Engine {
30 | r := gin.Default()
31 |
32 | // Enable CORS middleware
33 | r.Use(cors.New(cors.Config{
34 | AllowOrigins: []string{"*"},
35 | AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
36 | AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
37 | ExposeHeaders: []string{"Content-Length", "Content-Disposition"},
38 | AllowCredentials: true,
39 | }))
40 |
41 | // Apply rate limiting only to the POST /send endpoint
42 | r.POST("/send", rateLimiterMiddleware, handlers.CreateSend(cfg, db))
43 |
44 | // Other routes without rate limiting
45 | r.GET("/send/:id", handlers.GetSend(cfg, db))
46 | r.GET("/send/:id/check", handlers.CheckPasswordProtection(db))
47 |
48 | return r
49 | }
50 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 1.0.x | :white_check_mark: |
8 | | < 1.0.0 | :x: |
9 |
10 | ## Reporting a Vulnerability
11 |
12 | We take security seriously and appreciate your efforts to responsibly disclose vulnerabilities. If you discover a security issue in **GopherDrop**, please report it through GitHub’s security advisories:
13 |
14 | 1. **Go to the Repository**: Visit the [GopherDrop repository](https://github.com/kek-Sec/gopherdrop).
15 |
16 | 2. **Report a Vulnerability**:
17 | - Click on **"Security"** in the top navigation bar.
18 | - Select **"Report a vulnerability"**.
19 | - Fill in the details of the vulnerability, including:
20 | - Steps to reproduce the issue.
21 | - Potential impact or severity.
22 | - Any relevant code snippets, screenshots, or logs.
23 |
24 | ### What to Expect
25 |
26 | - **Acknowledgment**: We will acknowledge your report within **48 hours**.
27 | - **Initial Assessment**: We aim to provide an initial assessment within **5 business days**.
28 | - **Updates**: You will receive regular updates as we investigate and resolve the issue.
29 | - **Resolution**: Once resolved, we will notify you and credit you in the release notes unless you wish to remain anonymous.
30 |
31 | ### Responsible Disclosure
32 |
33 | Please **do not publicly disclose** the vulnerability until we have addressed it. We are committed to resolving security issues promptly and responsibly.
34 |
35 | ---
36 |
37 | Thank you for helping us keep **GopherDrop** secure!
38 |
--------------------------------------------------------------------------------
/ui/src/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Main application entry point.
3 | * Initializes Vue, Vuetify, and the Router.
4 | */
5 | import { createApp } from 'vue'
6 | import App from './App.vue'
7 | import router from './router'
8 |
9 | // Import Vuetify and styles
10 | import 'vuetify/styles'
11 | import { createVuetify } from 'vuetify'
12 | import '@mdi/font/css/materialdesignicons.css'
13 |
14 | // Create a custom light theme
15 | const customLightTheme = {
16 | dark: false,
17 | colors: {
18 | primary: '#6750A4', // A modern purple
19 | secondary: '#E8DEF8', // A light, complementary purple
20 | background: '#FFFFFF',
21 | surface: '#FEF7FF', // Off-white for cards and surfaces
22 | info: '#6750A4', // Using primary color for info state for consistency
23 | error: '#B3261E',
24 | success: '#4CAF50',
25 | warning: '#FB8C00',
26 | }
27 | };
28 |
29 | // Create a custom dark theme
30 | const customDarkTheme = {
31 | dark: true,
32 | colors: {
33 | primary: '#D0BCFF', // A lighter purple for dark mode contrast
34 | secondary: '#4A4458',
35 | background: '#141218',
36 | surface: '#262329',
37 | info: '#D0BCFF', // Lighter purple for info state
38 | error: '#F2B8B5',
39 | success: '#B7F3B9',
40 | warning: '#FFD6A8',
41 | }
42 | }
43 |
44 | // Create Vuetify instance
45 | const vuetify = createVuetify({
46 | theme: {
47 | defaultTheme: 'customLightTheme',
48 | themes: {
49 | customLightTheme,
50 | customDarkTheme
51 | },
52 | },
53 | icons: {
54 | defaultSet: 'mdi',
55 | },
56 | })
57 |
58 | // Create and mount the Vue application
59 | const app = createApp(App)
60 | app.use(router)
61 | app.use(vuetify)
62 | app.mount('#app')
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | __TITLE__
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/internal/security/security.go:
--------------------------------------------------------------------------------
1 | // Package security provides encryption and decryption functions.
2 | package security
3 |
4 | import (
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "crypto/rand"
8 | "encoding/base64"
9 | "errors"
10 | "io"
11 | )
12 |
13 | // PadKey ensures the key is 32 bytes.
14 | func PadKey(k string) string {
15 | for len(k) < 32 {
16 | k += "0"
17 | }
18 | return k[:32]
19 | }
20 |
21 | // EncryptData encrypts data using AES-256 CFB mode.
22 | func EncryptData(data []byte, key []byte) (string, error) {
23 | block, err := aes.NewCipher(key)
24 | if err != nil {
25 | return "", err
26 | }
27 |
28 | // Generate a random IV
29 | iv := make([]byte, aes.BlockSize)
30 | if _, err := io.ReadFull(rand.Reader, iv); err != nil {
31 | return "", err
32 | }
33 |
34 | if len(data) > (1<<31 - 1 - aes.BlockSize) {
35 | return "", errors.New("data too large to encrypt")
36 | }
37 | ciphertext := make([]byte, aes.BlockSize+len(data))
38 | copy(ciphertext[:aes.BlockSize], iv)
39 |
40 | stream := cipher.NewCFBEncrypter(block, iv)
41 | stream.XORKeyStream(ciphertext[aes.BlockSize:], data)
42 |
43 | return base64.StdEncoding.EncodeToString(ciphertext), nil
44 | }
45 |
46 | // DecryptData decrypts data previously encrypted with AES-256 CFB.
47 | func DecryptData(enc string, key []byte) ([]byte, error) {
48 | data, err := base64.StdEncoding.DecodeString(enc)
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | block, err := aes.NewCipher(key)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | if len(data) < aes.BlockSize {
59 | return nil, errors.New("ciphertext too short")
60 | }
61 |
62 | iv := data[:aes.BlockSize]
63 | data = data[aes.BlockSize:]
64 |
65 | stream := cipher.NewCFBDecrypter(block, iv)
66 | stream.XORKeyStream(data, data)
67 |
68 | return data, nil
69 | }
70 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/kek-Sec/gopherdrop
2 |
3 | go 1.22.4
4 |
5 | require (
6 | github.com/gin-contrib/cors v1.7.2
7 | github.com/gin-gonic/gin v1.10.0
8 | github.com/jinzhu/gorm v1.9.16
9 | github.com/lib/pq v1.10.9
10 | github.com/stretchr/testify v1.10.0
11 | )
12 |
13 | require (
14 | github.com/bytedance/sonic v1.11.6 // indirect
15 | github.com/bytedance/sonic/loader v0.1.1 // indirect
16 | github.com/cloudwego/base64x v0.1.4 // indirect
17 | github.com/cloudwego/iasm v0.2.0 // indirect
18 | github.com/davecgh/go-spew v1.1.1 // indirect
19 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
20 | github.com/gin-contrib/sse v0.1.0 // indirect
21 | github.com/go-playground/locales v0.14.1 // indirect
22 | github.com/go-playground/universal-translator v0.18.1 // indirect
23 | github.com/go-playground/validator/v10 v10.20.0 // indirect
24 | github.com/goccy/go-json v0.10.2 // indirect
25 | github.com/jinzhu/inflection v1.0.0 // indirect
26 | github.com/json-iterator/go v1.1.12 // indirect
27 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect
28 | github.com/kr/text v0.2.0 // indirect
29 | github.com/leodido/go-urn v1.4.0 // indirect
30 | github.com/mattn/go-isatty v0.0.20 // indirect
31 | github.com/mattn/go-sqlite3 v1.14.0 // indirect
32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
33 | github.com/modern-go/reflect2 v1.0.2 // indirect
34 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
35 | github.com/pmezard/go-difflib v1.0.0 // indirect
36 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
37 | github.com/ugorji/go/codec v1.2.12 // indirect
38 | golang.org/x/arch v0.8.0 // indirect
39 | golang.org/x/crypto v0.31.0 // indirect
40 | golang.org/x/net v0.25.0 // indirect
41 | golang.org/x/sys v0.28.0 // indirect
42 | golang.org/x/text v0.21.0 // indirect
43 | golang.org/x/time v0.8.0
44 | google.golang.org/protobuf v1.34.1 // indirect
45 | gopkg.in/yaml.v3 v3.0.1 // indirect
46 | )
47 |
--------------------------------------------------------------------------------
/internal/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestLoadConfig_Defaults(t *testing.T) {
11 | // Clear environment variables to test defaults
12 | os.Clearenv()
13 |
14 | cfg := LoadConfig()
15 |
16 | assert.Equal(t, ":8080", cfg.ListenAddr, "ListenAddr should default to :8080")
17 | assert.Equal(t, "/tmp", cfg.StoragePath, "StoragePath should default to /tmp")
18 | assert.Equal(t, int64(10*1024*1024), cfg.MaxFileSize, "MaxFileSize should default to 10MB")
19 | }
20 |
21 | func TestLoadConfig_EnvVariables(t *testing.T) {
22 | // Set environment variables
23 | os.Setenv("DB_HOST", "localhost")
24 | os.Setenv("DB_USER", "testuser")
25 | os.Setenv("DB_PASSWORD", "testpass")
26 | os.Setenv("DB_NAME", "testdb")
27 | os.Setenv("DB_SSLMODE", "disable")
28 | os.Setenv("SECRET_KEY", "supersecret")
29 | os.Setenv("LISTEN_ADDR", ":9090")
30 | os.Setenv("STORAGE_PATH", "/var/data")
31 | os.Setenv("MAX_FILE_SIZE", "5242880") // 5MB
32 |
33 | cfg := LoadConfig()
34 |
35 | assert.Equal(t, "localhost", cfg.DBHost, "DBHost should match the environment variable")
36 | assert.Equal(t, "testuser", cfg.DBUser, "DBUser should match the environment variable")
37 | assert.Equal(t, "testpass", cfg.DBPass, "DBPass should match the environment variable")
38 | assert.Equal(t, "testdb", cfg.DBName, "DBName should match the environment variable")
39 | assert.Equal(t, "disable", cfg.DBSSLMode, "DBSSLMode should match the environment variable")
40 | assert.Equal(t, "supersecret", cfg.SecretKey, "SecretKey should match the environment variable")
41 | assert.Equal(t, ":9090", cfg.ListenAddr, "ListenAddr should match the environment variable")
42 | assert.Equal(t, "/var/data", cfg.StoragePath, "StoragePath should match the environment variable")
43 | assert.Equal(t, int64(5242880), cfg.MaxFileSize, "MaxFileSize should match the environment variable")
44 | }
45 |
46 | func TestLoadConfig_InvalidMaxFileSize(t *testing.T) {
47 | // Set invalid MAX_FILE_SIZE
48 | os.Setenv("MAX_FILE_SIZE", "invalid")
49 |
50 | cfg := LoadConfig()
51 |
52 | assert.Equal(t, int64(10*1024*1024), cfg.MaxFileSize, "MaxFileSize should default to 10MB on invalid input")
53 | }
54 |
--------------------------------------------------------------------------------
/internal/database/database_test.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/jinzhu/gorm"
8 | _ "github.com/jinzhu/gorm/dialects/sqlite"
9 |
10 | "github.com/kek-Sec/gopherdrop/internal/config"
11 | "github.com/kek-Sec/gopherdrop/internal/models"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | // TestInitDB tests the InitDB function.
16 | func TestInitDB(t *testing.T) {
17 | cfg := config.Config{
18 | DBHost: "",
19 | DBUser: "",
20 | DBPass: "",
21 | DBName: ":memory:",
22 | DBSSLMode: "",
23 | }
24 |
25 | db, err := gorm.Open("sqlite3", cfg.DBName)
26 | if err != nil {
27 | t.Fatalf("Failed to initialize test database: %v", err)
28 | }
29 | defer db.Close()
30 |
31 | assert.NotNil(t, db, "Database connection should not be nil")
32 | }
33 |
34 | // TestCleanupExpired tests the CleanupExpired function.
35 | func TestCleanupExpired(t *testing.T) {
36 | // Setup an in-memory database
37 | db, err := gorm.Open("sqlite3", ":memory:")
38 | if err != nil {
39 | t.Fatalf("Failed to initialize test database: %v", err)
40 | }
41 | defer db.Close()
42 |
43 | // Migrate the Send model
44 | db.AutoMigrate(&models.Send{})
45 |
46 | // Create some test data
47 | now := time.Now()
48 | expiredSend := models.Send{
49 | Hash: "expired1",
50 | Type: "text",
51 | Data: "some data",
52 | ExpiresAt: now.Add(-1 * time.Hour),
53 | }
54 |
55 | validSend := models.Send{
56 | Hash: "valid1",
57 | Type: "text",
58 | Data: "valid data",
59 | ExpiresAt: now.Add(1 * time.Hour),
60 | }
61 |
62 | db.Create(&expiredSend)
63 | db.Create(&validSend)
64 |
65 | // Run the cleanup function (without the infinite loop)
66 | func() {
67 | var sends []models.Send
68 | db.Where("expires_at < ?", time.Now()).Find(&sends)
69 | for _, s := range sends {
70 | db.Delete(&s)
71 | }
72 | }()
73 |
74 | // Verify that the expired send was deleted
75 | var result models.Send
76 | err = db.Where("hash = ?", "expired1").First(&result).Error
77 | assert.Equal(t, gorm.ErrRecordNotFound, err, "Expired send should be deleted")
78 |
79 | // Verify that the valid send still exists
80 | err = db.Where("hash = ?", "valid1").First(&result).Error
81 | assert.Nil(t, err, "Valid send should still exist")
82 | }
83 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1: Build the Go Backend
2 | FROM golang:1.22-alpine AS backend-builder
3 |
4 | WORKDIR /app
5 | COPY . .
6 |
7 | # Add build arguments for debug mode and versioning
8 | ARG DEBUG=false
9 | ARG GIN_MODE=release
10 | ENV GIN_MODE=${GIN_MODE}
11 | ARG VERSION
12 | ENV VERSION=${VERSION}
13 |
14 | # Set build tags based on the DEBUG flag and include versioning information
15 | RUN if [ "$DEBUG" = "true" ]; then \
16 | go mod download && go build -o server -tags debug -ldflags="-X main.version=DEBUG" ./cmd/server/main.go; \
17 | else \
18 | go mod download && go build -o server -ldflags="-X main.version=${VERSION}" ./cmd/server/main.go; \
19 | fi
20 |
21 | # Stage 2: Build the Vue.js Frontend
22 | FROM node:23-alpine AS frontend-builder
23 |
24 | WORKDIR /app
25 | COPY ui/package.json ui/package-lock.json ./
26 | RUN npm install --legacy-peer-deps
27 |
28 | # Add build arguments for customization
29 | ARG VITE_API_URL="/api"
30 | ARG VITE_APP_TITLE="GopherDrop"
31 | ARG VITE_APP_DESCRIPTION="Secure one-time secret and file sharing"
32 |
33 | ENV VITE_API_URL=${VITE_API_URL}
34 | ENV VITE_APP_TITLE=${VITE_APP_TITLE}
35 | ENV VITE_APP_DESCRIPTION=${VITE_APP_DESCRIPTION}
36 |
37 | COPY ui ./
38 | RUN npm run build
39 |
40 | # Stage 3: Combine Backend and Frontend into a Single Image
41 | FROM nginx:alpine
42 |
43 | # Add OCI Image Spec labels
44 | ARG GIT_COMMIT_SHA
45 | ARG GIT_VERSION
46 |
47 | LABEL org.opencontainers.image.title="GopherDrop" \
48 | org.opencontainers.image.description="GopherDrop - Secure one-time secret sharing service" \
49 | org.opencontainers.image.source="https://github.com/kek-Sec/gopherdrop" \
50 | org.opencontainers.image.url="https://github.com/kek-Sec/gopherdrop" \
51 | org.opencontainers.image.documentation="https://github.com/kek-Sec/gopherdrop" \
52 | org.opencontainers.image.licenses="MIT"
53 |
54 | # Copy the Go server binary
55 | COPY --from=backend-builder /app/server /app/server
56 |
57 | # Copy the frontend static files
58 | COPY --from=frontend-builder /app/dist /usr/share/nginx/html
59 |
60 | # Copy Nginx configuration
61 | COPY nginx.conf /etc/nginx/conf.d/default.conf
62 |
63 | # Create the storage directory for the backend
64 | RUN mkdir -p /app/storage
65 |
66 | # Expose the ports for Nginx and the Go server
67 | EXPOSE 80 8080
68 |
69 | # Run both the Go server and Nginx using a simple script
70 | CMD ["/bin/sh", "-c", "/app/server & nginx -g 'daemon off;'"]
71 |
--------------------------------------------------------------------------------
/ui/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | mdi-plus Create New
14 |
15 |
16 |
17 |
18 | {{ isDarkMode ? 'mdi-weather-sunny' : 'mdi-weather-night' }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | © {{ new Date().getFullYear() }} GopherDrop |
32 |
37 | GitHub Repository
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
80 |
81 |
--------------------------------------------------------------------------------
/.github/workflows/build&test.yaml:
--------------------------------------------------------------------------------
1 | name: 🛠️ Build, Test & Scan
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build-go:
10 | name: 🐹 Build Go Project
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: 📥 Checkout Code
15 | uses: actions/checkout@v4
16 |
17 | - name: 🐹 Set Up Go
18 | uses: actions/setup-go@v5
19 | with:
20 | go-version: '1.22'
21 |
22 | - name: 📦 Install Go Dependencies
23 | run: go mod tidy
24 |
25 | - name: 🛠️ Build Go Project
26 | run: go build -o server ./cmd/server/main.go
27 |
28 | build-vue:
29 | name: 🌐 Build Vue Project
30 | runs-on: ubuntu-latest
31 |
32 | steps:
33 | - name: 📥 Checkout Code
34 | uses: actions/checkout@v4
35 |
36 | - name: 🌐 Set Up Node.js
37 | uses: actions/setup-node@v4
38 | with:
39 | node-version: '20'
40 |
41 | - name: 📦 Install Vue Dependencies
42 | run: |
43 | cd ui
44 | npm install
45 |
46 | - name: 🖥️ Build Vue Project
47 | run: |
48 | cd ui
49 | npm run build
50 |
51 | test-go:
52 | name: ✅ Run Go Tests
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - name: 📥 Checkout Code
57 | uses: actions/checkout@v4
58 |
59 | - name: 🐹 Set Up Go
60 | uses: actions/setup-go@v5
61 | with:
62 | go-version: '1.22'
63 |
64 | - name: 📦 Install Go Dependencies
65 | run: go mod tidy
66 |
67 | - name: ✅ Run Go Tests with Coverage
68 | run: go test ./... -v -coverprofile=coverage.out
69 |
70 | - name: 📊 Upload Code Coverage Report
71 | uses: actions/upload-artifact@v4
72 | with:
73 | name: go-code-coverage
74 | path: coverage.out
75 |
76 | - name: 📝 Publish Coverage to Coveralls
77 | uses: shogo82148/actions-goveralls@v1
78 | with:
79 | path-to-profile: coverage.out
80 | github-token: ${{ secrets.GITHUB_TOKEN }}
81 |
82 | scan-vulnerabilities:
83 | name: 🔍 Scan for Vulnerabilities
84 | runs-on: ubuntu-latest
85 |
86 | steps:
87 | - name: 📥 Checkout Code
88 | uses: actions/checkout@v4
89 |
90 | - name: 🐹 Set Up Go
91 | uses: actions/setup-go@v5
92 | with:
93 | go-version: '1.22'
94 |
95 | - name: 📦 Install Go Dependencies
96 | run: go mod tidy
97 |
98 | - name: 🔍 Run Golang Security Scanner
99 | uses: securego/gosec@v2.21.4
100 | with:
101 | args: '-no-fail -fmt sarif -out results.sarif ./...'
102 |
103 | - name: 📝 Upload SARIF Results
104 | uses: github/codeql-action/upload-sarif@v3
105 | with:
106 | sarif_file: results.sarif
107 |
108 | - name: 🌐 Set Up Node.js
109 | uses: actions/setup-node@v4
110 | with:
111 | node-version: '20'
112 |
113 | - name: 📦 Install Vue Dependencies
114 | run: |
115 | cd ui
116 | npm install
117 |
118 | - name: 🔍 Run NPM Audit
119 | run: |
120 | cd ui
121 | npm audit --audit-level=high || true
122 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-push-docker.yaml:
--------------------------------------------------------------------------------
1 | name: 🐳 Build and Push Docker Images
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | workflow_dispatch:
11 |
12 | permissions:
13 | contents: read
14 | packages: write
15 |
16 | jobs:
17 | build-backend:
18 | name: 🛠️ Build and Push
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: 🚀 Checkout Code
22 | uses: actions/checkout@v4
23 |
24 | # Get version from version.yaml
25 | - name: 📁 Get version
26 | run: |
27 | echo "VERSION=$(cat version.yaml | sed -n 's/version: //p')" >> $GITHUB_ENV
28 | echo $VERSION
29 |
30 | # Set build tags based on branch
31 | - name: 🏗️ Set Build tags
32 | run: |
33 | if [ $GITHUB_REF = 'refs/heads/main' ]; then
34 | echo "IMAGE_TAGS=$VERSION" >> $GITHUB_ENV
35 | else
36 | echo "IMAGE_TAGS=rc-$VERSION-$GITHUB_RUN_NUMBER" >> $GITHUB_ENV
37 | fi
38 |
39 | - name: 🛠️ Set up Docker Buildx
40 | uses: docker/setup-buildx-action@v2
41 |
42 | # Login to GitHub Container Registry
43 | - name: 🔐 Login to GitHub Container Registry
44 | uses: docker/login-action@v2
45 | with:
46 | registry: ghcr.io
47 | username: ${{ github.actor }}
48 | password: ${{ secrets.TOKEN }}
49 |
50 | # Login to Docker Hub
51 | - name: 🔐 Login to Docker Hub
52 | if: github.ref == 'refs/heads/main'
53 | uses: docker/login-action@v2
54 | with:
55 | username: ${{ secrets.DOCKERHUB_USERNAME }}
56 | password: ${{ secrets.DOCKERHUB_TOKEN }}
57 |
58 | - name: 📦 Build and Push Image to GHCR (PR)
59 | if: github.event_name == 'pull_request'
60 | uses: docker/build-push-action@v4
61 | with:
62 | context: .
63 | file: ./Dockerfile
64 | push: true
65 | tags: ghcr.io/kek-sec/gopherdrop:${{ env.IMAGE_TAGS }}
66 | annotations: |
67 | org.opencontainers.image.description=GopherDrop, a secure one-time secret sharing service
68 | build-args: |
69 | VERSION=${{ env.VERSION }}
70 | labels: |
71 | org.opencontainers.image.source=https://github.com/kek-sec/gopherdrop
72 | cache-from: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest
73 | cache-to: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest
74 |
75 | - name: 📦 Build and Push Image to GHCR and Docker Hub (Main)
76 | if: github.ref == 'refs/heads/main'
77 | uses: docker/build-push-action@v4
78 | with:
79 | context: .
80 | file: ./Dockerfile
81 | push: true
82 | tags: |
83 | ghcr.io/kek-sec/gopherdrop:${{ env.IMAGE_TAGS }}
84 | petrakisg/gopherdrop:${{ env.IMAGE_TAGS }}
85 | annotations: |
86 | org.opencontainers.image.description=GopherDrop, a secure one-time secret sharing service
87 | build-args: |
88 | VERSION=${{ env.VERSION }}
89 | labels: |
90 | org.opencontainers.image.source=https://github.com/kek-sec/gopherdrop
91 | cache-from: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest
92 | cache-to: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest
--------------------------------------------------------------------------------
/ui/src/pages/View.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | View Secret 🔒
5 |
6 |
7 | {{ errorMessage }}
8 |
9 |
10 |
11 | Secret not found or has expired.
12 |
13 |
14 |
15 |
25 |
26 | Load Secret
27 |
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
95 |
96 |
--------------------------------------------------------------------------------
/ui/src/services/api.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @module api
3 | * @description Provides functions to interact with the backend API.
4 | */
5 |
6 | const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080';
7 | console.log('API_URL:', API_URL);
8 |
9 | /**
10 | * Maps HTTP status codes to error messages for the createSend function.
11 | * @type {Object}
12 | */
13 | const createSendErrorMessages = {
14 | 413: 'File too large',
15 | 422: 'Invalid form data',
16 | 429: 'Too many requests – please try again later'
17 | };
18 |
19 | /**
20 | * Creates a new "send" with the provided form data.
21 | * @param {FormData} formData - The data for creating the send.
22 | * @returns {Promise