Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

15 changed files with 575 additions and 10 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
uploads/
README-SECRET.md

View file

@ -1,9 +0,0 @@
MIT License
Copyright (c) 2024 simono41
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,2 +1,77 @@
# picture-uploader
# Picture Uploader
This project is a simple web application written in Go for uploading and viewing images.
## Getting Started
To run this application, follow the steps below:
### Prerequisites
- [Docker](https://www.docker.com/)
- [docker-compose](https://docs.docker.com/compose/)
### Instructions
1. Clone the repository:
```bash
git clone <repository-url>
```
2. Navigate to the project directory:
```bash
cd <project-directory>
```
3. Create a folder named `uploads`:
```bash
mkdir uploads
```
4. Set permissions for the `uploads` folder:
```bash
chmod 777 uploads
```
5. Run the application using Docker Compose:
```bash
docker-compose up -d
```
The application should be accessible at [http://localhost:8080](http://localhost:8080).
## Configuration
Modify the `docker-compose.yml` file to adjust environment variables, ports, or any other configurations as needed.
## Upload via Terminal
curl -X POST -F „image=@/tmp/network-home.svg“ -F „force_name=true“ https://pick.brothertec.eu/upload
Ersetzen Sie /pfad/zur/datei/bild.jpg durch den tatsächlichen Pfad zu Ihrer Datei und http://localhost:8080/upload durch die URL Ihres Servers und den Endpunkt für den Dateiupload.
Hier ist eine Erläuterung der Optionen, die in der Curl-Anfrage verwendet werden:
-X POST: Legt die HTTP-Methode auf POST fest, was in diesem Fall verwendet wird, um die Datei hochzuladen.
-F "image=@/pfad/zur/datei/bild.jpg": Teilt Curl mit, dass es sich um ein Formular-Upload handelt (-F), und gibt den Namen des Formularfelds (“image”) sowie den Dateipfad (@/pfad/zur/datei/bild.jpg) an.
http://localhost:8080/upload: Die URL des Servers und des Endpunkts, an den die Datei hochgeladen werden soll.
Führen Sie diese Curl-Anfrage in einem Terminal aus, und die Datei wird an den angegebenen Server hochgeladen.
## Additional Information
- This project uses NGINX as a reverse proxy. Ensure that the required networks (`nginx-proxy` and `edge`) are set up externally or adjust the `docker-compose.yml` accordingly.
- If you encounter issues with image uploads, verify the permissions on the `uploads` folder.
### Support and Issues
For support or to report issues, please [open an issue](<repository-url>/issues).
### License
This project is licensed under the [MIT License](LICENSE).

43
docker-compose.yml Executable file
View file

@ -0,0 +1,43 @@
version: "3.9"
services:
# Go application service
go-app:
build:
context: go/.
args:
- GO111MODULE=on
#ports:
# - "8080:8080"
environment:
- VIRTUAL_HOST=pick.brothertec.eu
- VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=pick.brothertec.eu
- LETSENCRYPT_EMAIL=admin@brothertec.eu
volumes:
- ./uploads:/uploads
- ./templates:/templates
- ./static:/static
restart: always
labels:
- flame.type=application
- flame.name=Picture Upload
- flame.url=https://pick.brothertec.eu
- flame.icon=image
networks:
default:
proxy:
edge-tier:
networks:
proxy:
name: nginx-proxy
external: true
edge-tier:
name: edge
external: true

28
go/Dockerfile Executable file
View file

@ -0,0 +1,28 @@
# syntax=docker/dockerfile:1
# Build the application from source
FROM golang:1.20.10 AS build-stage
WORKDIR /app
COPY * ./
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o /main
# Run the tests in the container
FROM build-stage AS run-test-stage
RUN go test -v ./...
# Deploy the application binary into a lean image
FROM gcr.io/distroless/base-debian11 AS build-release-stage
WORKDIR /
COPY --from=build-stage /main /main
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/main"]

22
go/Dockerfile.old Executable file
View file

@ -0,0 +1,22 @@
# Use an official Golang runtime as a parent image
FROM golang:1.21.4
# Set the working directory in the container
WORKDIR /go/src/app
# Copy the local package files to the container's workspace
COPY . .
# Download and install any required third-party dependencies into the container.
#RUN go get -u github.com/gorilla/mux
RUN go get -u github.com/go-sql-driver/mysql
RUN go get -u github.com/sirupsen/logrus
# Build the Go application
RUN go build -o main .
# Expose port 8080 to the outside world
EXPOSE 8080
# Command to run the application with environment variables
CMD ["./main"]

3
go/go.mod Normal file
View file

@ -0,0 +1,3 @@
module picture-uploader
go 1.20

324
go/main.go Normal file
View file

@ -0,0 +1,324 @@
package main
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
)
var (
lastUploadTime time.Time
mu sync.Mutex
uploadInterval = 10 * time.Second
)
func main() {
http.HandleFunc("/", homeHandler)
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/image/", imageHandler)
http.HandleFunc("/view/", viewHandler)
// Statischen Dateipfad setzen
fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
fmt.Println("Server listening on :8080")
http.ListenAndServe(":8080", nil)
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
// Setzen der Content Security Policy
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none';")
// Verwenden von html/template zur sicheren Ausgabe von HTML
tmpl, err := template.ParseFiles("templates/homeTemplate.html")
if err != nil {
http.Error(w, "Fehler beim Laden des Templates", http.StatusInternalServerError)
return
}
data := struct {
Title string
}{
Title: "Bildupload",
}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Fehler beim Rendern des Templates", http.StatusInternalServerError)
}
}
func generateNonce() (string, error) {
nonceBytes := make([]byte, 16) // 16 Bytes generieren eine ausreichend lange Zeichenfolge für den Nonce
if _, err := rand.Read(nonceBytes); err != nil {
return "", err // Im Fehlerfall, geben Sie den Fehler zurück
}
return base64.StdEncoding.EncodeToString(nonceBytes), nil
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
nonce, err := generateNonce()
if err != nil {
http.Error(w, "Serverfehler", http.StatusInternalServerError)
log.Printf("Fehler beim Generieren des Nonce: %v", err)
return
}
w.Header().Set("Content-Security-Policy", fmt.Sprintf("script-src 'self' 'nonce-%s';", nonce))
mu.Lock()
defer mu.Unlock()
if time.Since(lastUploadTime) < uploadInterval {
http.Error(w, "Nur alle 10 Sekunden erlaubt", http.StatusTooManyRequests)
log.Printf("Bildupload zu häufig. Nur alle 10 Sekunden erlaubt.")
return
}
if r.Method == http.MethodPost {
file, handler, err := r.FormFile("image")
if err != nil {
http.Error(w, "Fehler beim Lesen der Datei", http.StatusInternalServerError)
log.Printf("Fehler beim Lesen der Datei: %v", err)
return
}
defer file.Close()
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
http.Error(w, "Fehler beim Lesen der Datei", http.StatusInternalServerError)
log.Printf("Fehler beim Lesen der Datei für MIME-Typ-Erkennung: %v", err)
return
}
forceUpload := r.FormValue("force_upload")
if forceUpload != "true" {
mimeType := http.DetectContentType(buffer)
if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "text/xml") && !strings.HasPrefix(mimeType, "image/svg+xml") {
http.Error(w, "Nur Bild-Uploads sind erlaubt", http.StatusBadRequest)
log.Printf("Versuch, eine Nicht-Bild-Datei hochzuladen: %v", mimeType)
return
}
}
_, err = file.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, "Fehler beim Zurücksetzen des Dateizeigers", http.StatusInternalServerError)
log.Printf("Fehler beim Zurücksetzen des Dateizeigers: %v", err)
return
}
forceName := r.FormValue("force_name")
var filename string
if forceName == "true" {
filename = handler.Filename
} else {
fileExtension := filepath.Ext(handler.Filename)
timestamp := time.Now().Format("20060102-150405")
filename = fmt.Sprintf("%s%s", timestamp, fileExtension)
}
uploadPath := "./uploads/" + filename
if _, err := os.Stat(uploadPath); err == nil && forceUpload != "true" {
http.Error(w, "Datei existiert bereits. Überschreiben nicht erlaubt.", http.StatusConflict)
log.Printf("Versuch, bestehende Datei ohne force_upload zu überschreiben: %v", filename)
return
}
f, err := os.Create(uploadPath)
if err != nil {
http.Error(w, "Fehler beim Erstellen der Datei", http.StatusInternalServerError)
log.Printf("Fehler beim Erstellen der Datei: %v", err)
return
}
defer f.Close()
_, copyErr := io.Copy(f, file)
if copyErr != nil {
http.Error(w, "Fehler beim Kopieren der Datei", http.StatusInternalServerError)
log.Printf("Fehler beim Kopieren der Datei: %v", copyErr)
return
}
lastUploadTime = time.Now()
responseType := r.URL.Query().Get("responseType")
if responseType == "json" {
jsonResponse(w, nonce, filename)
return
}
renderTemplate(w, nonce, filename)
} else {
tmpl, err := template.ParseFiles("templates/uploadForm.html")
if err != nil {
http.Error(w, "Fehler beim Laden des Templates", http.StatusInternalServerError)
log.Printf("Fehler beim Laden des Templates: %v", err)
return
}
err = tmpl.Execute(w, nil)
if err != nil {
http.Error(w, "Fehler beim Rendern des Templates", http.StatusInternalServerError)
log.Printf("Fehler beim Rendern des Templates: %v", err)
}
}
}
func jsonResponse(w http.ResponseWriter, nonce string, filename string) {
w.Header().Set("Content-Type", "application/json")
response := struct {
Message string `json:"message"`
Filename string `json:"filename"`
Nonce string `json:"nonce"`
}{
Message: "Bild erfolgreich hochgeladen.",
Filename: filename,
Nonce: nonce,
}
json.NewEncoder(w).Encode(response)
}
func renderTemplate(w http.ResponseWriter, nonce string, filename string) {
// Implementierung des Template-Renderings
tmpl, err := template.ParseFiles("templates/uploadSuccess.html")
if err != nil {
http.Error(w, "Fehler beim Laden des Templates", http.StatusInternalServerError)
log.Printf("Fehler beim Laden des Templates: %v", err)
return
}
data := struct {
Message string
Filename string
Nonce string
}{
Message: "Bild erfolgreich hochgeladen.",
Filename: filename, // Geändert, um den möglicherweise modifizierten Dateinamen anzuzeigen
Nonce: nonce,
}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Fehler beim Rendern des Templates", http.StatusInternalServerError)
log.Printf("Fehler beim Rendern des Templates: %v", err)
return
}
}
// Funktion zur Ermittlung des MIME-Types basierend auf der Dateiendung
func getMimeType(filePath string) string {
switch filepath.Ext(filePath) {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".svg":
return "image/svg+xml"
default:
return "application/octet-stream"
}
}
func imageHandler(w http.ResponseWriter, r *http.Request) {
// Setzen der Content Security Policy
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none';")
// Überprüfen, ob der Pfad mit einem "/" endet (was auf ein Verzeichnis hinweisen könnte)
// und ob es eine Dateiendung gibt (was darauf hindeutet, dass es eine spezifische Datei ist).
if strings.HasSuffix(r.URL.Path, "/") && !strings.Contains(r.URL.Path, ".") {
http.Error(w, "Zugriff verweigert", http.StatusForbidden)
log.Printf("Versuch, auf Ordner außerhalb des uploads-Verzeichnisses zuzugreifen")
return
}
// Extrahieren des Bildnamens aus dem URL-Pfad
imagePath := r.URL.Path[len("/image/"):]
// Reinigen des Pfades, um Directory Traversal zu verhindern
cleanedPath := path.Clean("/uploads/" + imagePath)
// Generieren des absoluten Pfads zum uploads-Verzeichnis
uploadsDir, err := filepath.Abs("./uploads")
if err != nil {
http.Error(w, "Interner Serverfehler", http.StatusInternalServerError)
log.Printf("Fehler beim Ermitteln des absoluten Pfads des uploads-Verzeichnisses: %v", err)
return
}
// Generieren des absoluten Pfads zur angeforderten Datei
absImagePath, err := filepath.Abs(cleanedPath)
if err != nil {
http.Error(w, "Interner Serverfehler", http.StatusInternalServerError)
log.Printf("Fehler beim Ermitteln des absoluten Pfads des Bildes: %v", err)
return
}
// Sicherstellen, dass das Bild im uploads-Verzeichnis liegt
if !strings.HasPrefix(absImagePath, uploadsDir) {
http.Error(w, "Zugriff verweigert", http.StatusForbidden)
log.Printf("Versuch, auf Datei außerhalb des uploads-Verzeichnisses zuzugreifen: %v", absImagePath)
return
}
// Stellen Sie sicher, dass das Bild existiert
if _, err := os.Stat(absImagePath); os.IsNotExist(err) {
http.Error(w, "Bild nicht gefunden", http.StatusNotFound)
log.Printf("Bild nicht gefunden: %v", err)
return
}
// Setzen der korrekten MIME-Type basierend auf der Dateiendung
mimeType := getMimeType(imagePath)
w.Header().Set("Content-Type", mimeType)
// Ausliefern des Bildes
http.ServeFile(w, r, absImagePath)
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
// Setzen der Content Security Policy
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none';")
filePath := r.URL.Path[len("/view/"):]
imagePath := "./uploads/" + filePath
// Überprüfen, ob die Bilddatei existiert
if _, err := os.Stat(imagePath); os.IsNotExist(err) {
http.Error(w, "Bild nicht gefunden", http.StatusNotFound)
log.Printf("Bild nicht gefunden: %v", err)
return
}
// Verwenden von html/template zur sicheren Ausgabe von HTML
tmpl, err := template.ParseFiles("templates/viewImage.html")
if err != nil {
http.Error(w, "Fehler beim Laden des Templates", http.StatusInternalServerError)
log.Printf("Fehler beim Laden des Templates: %v", err)
return
}
data := struct {
Filename string
}{
Filename: filePath,
}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Fehler beim Rendern des Templates", http.StatusInternalServerError)
log.Printf("Fehler beim Rendern des Templates: %v", err)
}
}

Binary file not shown.

Binary file not shown.

15
static/js/script.js Normal file
View file

@ -0,0 +1,15 @@
document.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('copyButton').addEventListener('click', kopiereURL);
});
function kopiereURL() {
var copyText = document.getElementById("imageURL");
copyText.select();
copyText.setSelectionRange(0, 99999); // Für mobile Geräte
navigator.clipboard.writeText(copyText.value).then(function() {
console.log('Kopieren in die Zwischenablage erfolgreich.');
}, function(err) {
console.error('Fehler beim Kopieren in die Zwischenablage: ', err);
});
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Title}}</h1>
<p>Besuchen Sie /upload, um Bilder hochzuladen.</p>
</body>
</html>

16
templates/uploadForm.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Bild hochladen</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="image" required>
<br>
<input type="checkbox" name="force_name" value="true">
<label for="force_name">Originalnamen beibehalten (Force Name)</label>
<br>
<input type="submit" value="Hochladen">
</form>
</body>
</html>

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Upload Erfolgreich</title>
</head>
<body>
<p>{{.Message}}</p>
<!-- Anzeigen des Links zum Bild, um es zu betrachten -->
<p><a href="/view/{{.Filename}}" target="_blank">Ihr Bild anzeigen</a></p>
<!-- Eingabefeld mit der URL des Bildes -->
<input type="text" value="https://pick.brothertec.eu/view/{{.Filename}}" id="imageURL" readonly>
<!-- Button, um die URL zu kopieren -->
<button id="copyButton">URL kopieren</button>
<p><a href="/upload">Zurück zum Upload</a></p>
<script src="/static/js/script.js" nonce="{{.Nonce}}"></script>
<script nonce="{{.Nonce}}">
// Rufen Sie hier Ihre Funktion auf
kopiereURL();
</script>
</body>
</html>

12
templates/viewImage.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Bild anzeigen</title>
</head>
<body>
<h1>Bildanzeige</h1>
<!-- Verwenden des neuen Handlers für die Bild-URL -->
<img src="/image/{{.Filename}}" alt="Hochgeladenes Bild">
<p><a href="/upload">Zurück zum Upload</a></p>
</body>
</html>