Compare commits
No commits in common. "61c219cc0bd1decb2750a210b8d0900734d424f7" and "ef4367ace7859a53b58407737ef36a62525e7416" have entirely different histories.
61c219cc0b
...
ef4367ace7
33 changed files with 1200 additions and 286 deletions
25
.golangci.yaml
Normal file
25
.golangci.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
# Deprecated
|
||||||
|
- varcheck
|
||||||
|
- ifshort
|
||||||
|
- interfacer
|
||||||
|
- maligned
|
||||||
|
- deadcode
|
||||||
|
- scopelint
|
||||||
|
- golint
|
||||||
|
- structcheck
|
||||||
|
- exhaustivestruct
|
||||||
|
- nosnakecase
|
||||||
|
# Too extremist/unusable
|
||||||
|
- depguard
|
||||||
|
- varnamelen
|
||||||
|
- exhaustruct
|
||||||
|
- wsl
|
||||||
|
- contextcheck
|
||||||
|
- wrapcheck
|
||||||
|
linters-settings:
|
||||||
|
lll:
|
||||||
|
# Too short byt default
|
||||||
|
line-length: 160
|
32
.woodpecker/build.yaml
Normal file
32
.woodpecker/build.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
steps:
|
||||||
|
- name: publish_image
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: git.gnous.eu/${CI_REPO_OWNER}/plakken
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64/v8,linux/arm
|
||||||
|
registry: https://git.gnous.eu
|
||||||
|
tag: ${CI_COMMIT}
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
when:
|
||||||
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
|
event: push
|
||||||
|
- name: publish_image_tag
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: git.gnous.eu/${CI_REPO_OWNER}/plakken
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64/v8,linux/arm
|
||||||
|
registry: https://git.gnous.eu
|
||||||
|
tags:
|
||||||
|
- ${CI_COMMIT_TAG##v} # Remove v from tag
|
||||||
|
- stable
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
when:
|
||||||
|
event: tag
|
11
.woodpecker/lint.yml
Normal file
11
.woodpecker/lint.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
steps:
|
||||||
|
lint:
|
||||||
|
image: golang:1.22
|
||||||
|
commands:
|
||||||
|
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
- golangci-lint run
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
repo: gnouseu/plakken
|
||||||
|
- event: push
|
||||||
|
branch: main
|
24
.woodpecker/release.yaml
Normal file
24
.woodpecker/release.yaml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
steps:
|
||||||
|
- name: Build
|
||||||
|
image: golang:1.22
|
||||||
|
commands:
|
||||||
|
- go mod download
|
||||||
|
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -s" -o plakken-linux-amd64 # Enable static binary, target Linux, remove debug information and strip binary
|
||||||
|
- CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-w -s" -o plakken-linux-arm64
|
||||||
|
- CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "-w -s" -o plakken-linux-arm
|
||||||
|
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -s" -o plakken-windows-amd64.exe
|
||||||
|
- CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-w -s" -o plakken-windows-arm64.exe
|
||||||
|
- CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "-w -s" -o plakken-windows-arm.exe
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
- name: Release
|
||||||
|
image: woodpeckerci/plugin-gitea-release
|
||||||
|
settings:
|
||||||
|
base_url: https://git.gnous.eu
|
||||||
|
files:
|
||||||
|
- "plakken*"
|
||||||
|
api_key:
|
||||||
|
from_secret: release_token
|
||||||
|
target: main
|
||||||
|
when:
|
||||||
|
event: tag
|
28
docker/Dockerfile
Normal file
28
docker/Dockerfile
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Build
|
||||||
|
FROM golang:1.22 AS build
|
||||||
|
LABEL authors="gnousEU"
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY main.go ./
|
||||||
|
COPY internal/ ./internal
|
||||||
|
COPY static/ ./static
|
||||||
|
COPY templates/ ./templates
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" # Enable static binary, target Linux, remove debug information and strip binary
|
||||||
|
|
||||||
|
# Copy to our image
|
||||||
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /build/plakken ./
|
||||||
|
|
||||||
|
ENV PLAKKEN_LISTEN ":3000"
|
||||||
|
|
||||||
|
EXPOSE 3000/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/plakken"]
|
31
docker/docker-compose.dev.yaml
Normal file
31
docker/docker-compose.dev.yaml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
plakken:
|
||||||
|
external: false
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
restart: always
|
||||||
|
container_name: plakken
|
||||||
|
networks:
|
||||||
|
- plakken
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- PLAKKEN_REDIS_ADDRESS=redis:6379
|
||||||
|
- POSTGRES_PASSWORD=gitea
|
||||||
|
- PLAKKEN_REDIS_DB=0
|
||||||
|
- PLAKKEN_URL_LENGTH=5
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
|
networks:
|
||||||
|
- plakken
|
36
docker/docker-compose.yaml
Normal file
36
docker/docker-compose.yaml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
plakken:
|
||||||
|
external: false
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: git.gnous.eu/gnouseu/plakken:latest
|
||||||
|
restart: always
|
||||||
|
container_name: plakken
|
||||||
|
read_only: true
|
||||||
|
networks:
|
||||||
|
- plakken
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- PLAKKEN_REDIS_ADDRESS=redis:6379
|
||||||
|
- POSTGRES_PASSWORD=gitea
|
||||||
|
- PLAKKEN_REDIS_DB=0
|
||||||
|
- PLAKKEN_URL_LENGTH=5
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
|
networks:
|
||||||
|
- plakken
|
||||||
|
volumes:
|
||||||
|
- redis:/data
|
|
@ -10,16 +10,16 @@
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<link href="/static/favicon.svg" rel="icon">
|
<link href="/static/favicon.svg" rel="icon">
|
||||||
<title>New paste</title>
|
<title>New paste</title>
|
||||||
<link href="../static/style.css" rel="stylesheet">
|
<link href="style.css" rel="stylesheet">
|
||||||
<script async src="../static/app.js"></script>
|
<script async src="../static/app.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<form class="fr" method="post">
|
<form action="/create/" class="fr" method="post">
|
||||||
<div id="line-numbers">1</div>
|
<div id="line-numbers">1</div>
|
||||||
<label for="content"><textarea autofocus id="content" name="content"
|
<label for="content"><textarea autofocus id="content" name="content"
|
||||||
placeholder="Type your paste here"></textarea></label>
|
placeholder="Type your paste here"></textarea></label>
|
||||||
</form>
|
|
||||||
<div class="menu fr">
|
<div class="menu fr">
|
||||||
<label for="filename"><input id="filename" name="filename" placeholder="Filename" type="text"></label>
|
<label for="filename"><input id="filename" name="filename" placeholder="Filename" type="text"></label>
|
||||||
<!--<label for="password"><input id="password" placeholder="Password" type="password"></label>-->
|
<!--<label for="password"><input id="password" placeholder="Password" type="password"></label>-->
|
||||||
|
@ -46,10 +46,11 @@
|
||||||
</label>
|
</label>
|
||||||
<button title="Save plak" type="submit">Save</button>
|
<button title="Save plak" type="submit">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<section id="recent-plaks">
|
<section id="recent-plaks">
|
||||||
<div class="title fr">
|
<div class="title fr">
|
||||||
<h3>Recent plaks</h2>
|
<h3>Recent plaks</h3>
|
||||||
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2.5">
|
<svg height="32" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" width="32">
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
202
front/style.css
Normal file
202
front/style.css
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
section#recent-plaks {
|
||||||
|
bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
gap: 9px;
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
transition: 0.15s ease-in-out;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
section#recent-plaks .title {
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
transition: 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
section#recent-plaks .title:hover {
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
section#recent-plaks .recent-plak {
|
||||||
|
align-items: center;
|
||||||
|
border: 3px solid #363636;
|
||||||
|
font-weight: 500;
|
||||||
|
justify-content: space-between;
|
||||||
|
outline: none;
|
||||||
|
padding: 8px 18px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: 0.15s ease-in-out;
|
||||||
|
width: 12rem;
|
||||||
|
}
|
||||||
|
section#recent-plaks .recent-plak:focus-visible {
|
||||||
|
border: 2px solid #bbb;
|
||||||
|
}
|
||||||
|
section#recent-plaks .recent-plak svg {
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 22px;
|
||||||
|
padding: 5px;
|
||||||
|
stroke: #e5e5e7;
|
||||||
|
stroke-width: 2.25;
|
||||||
|
width: 22px;
|
||||||
|
transition: 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
section#recent-plaks .recent-plak svg:hover {
|
||||||
|
background-color: #363636;
|
||||||
|
}
|
||||||
|
section#recent-plaks a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
transition: 0.15s ease-in-out;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: #d30f45;
|
||||||
|
color: #e5e5e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: #0f0f0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #e5e5e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(inter.woff2) format("woff2");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(jetbrainsmono.woff2) format("woff2");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #0f0f0f;
|
||||||
|
color: #e5e5e7;
|
||||||
|
font: 400 16px/2 "Inter", "system-ui", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#line-numbers,
|
||||||
|
#content {
|
||||||
|
font: 400 14px/1.6 "JetBrains Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#line-numbers {
|
||||||
|
color: #888;
|
||||||
|
padding: 16px 1px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: pre;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
color: #e5e5e7;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 90vh;
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
width: calc(100vw - 65px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
gap: 18px;
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
|
.menu svg {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 24px;
|
||||||
|
fill: none;
|
||||||
|
margin-bottom: -6px;
|
||||||
|
stroke: #e5e5e7;
|
||||||
|
stroke-width: 2;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
background-color: #0f0f0f;
|
||||||
|
border: 2px solid #363636;
|
||||||
|
color: #e5e5e7;
|
||||||
|
font: 500 14px/2 "Inter", "system-ui", sans-serif;
|
||||||
|
max-width: 145px;
|
||||||
|
outline: none;
|
||||||
|
padding: 3px 10px;
|
||||||
|
transition: border 0.15s ease;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
button::placeholder,
|
||||||
|
input::placeholder,
|
||||||
|
select::placeholder {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
button:hover,
|
||||||
|
input:hover,
|
||||||
|
select:hover {
|
||||||
|
border-color: #777;
|
||||||
|
}
|
||||||
|
button:focus-visible,
|
||||||
|
input:focus-visible,
|
||||||
|
select:focus-visible {
|
||||||
|
border: 2px solid #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 8px 10px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
background-color: #0f0f0f;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
option {
|
||||||
|
background-color: #0f0f0f;
|
||||||
|
color: #e5e5e7;
|
||||||
|
}
|
||||||
|
option:focus {
|
||||||
|
background-color: #d30f45;
|
||||||
|
color: #0f0f0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
background-color: inherit;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=style.css.map */
|
|
@ -4,7 +4,24 @@
|
||||||
@use 'recents';
|
@use 'recents';
|
||||||
@use 'misc';
|
@use 'misc';
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=JetBrains+Mono&display=swap');
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(inter.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(jetbrainsmono.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: $background;
|
background-color: $background;
|
||||||
|
@ -93,7 +110,7 @@ select:focus {
|
||||||
background-color: $background;
|
background-color: $background;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
²
|
|
||||||
option {
|
option {
|
||||||
background-color: $background;
|
background-color: $background;
|
||||||
color: $text;
|
color: $text;
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -4,10 +4,12 @@ go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/redis/go-redis/v9 v9.4.0
|
github.com/redis/go-redis/v9 v9.5.1
|
||||||
|
golang.org/x/crypto v0.22.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
12
go.sum
12
go.sum
|
@ -2,11 +2,15 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
|
|
@ -5,25 +5,29 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/constant"
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/utils"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitConfig Structure for program initialisation
|
// InitConfig Structure for program initialisation.
|
||||||
type InitConfig struct {
|
type InitConfig struct {
|
||||||
ListenAddress string
|
ListenAddress string
|
||||||
RedisAddress string
|
RedisAddress string
|
||||||
RedisUser string
|
RedisUser string
|
||||||
RedisPassword string
|
RedisPassword string
|
||||||
RedisDB int
|
RedisDB int
|
||||||
UrlLength uint8
|
URLLength uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig Initialise configuration form .env
|
// GetConfig Initialise configuration form .env.
|
||||||
func GetConfig() InitConfig {
|
func GetConfig() InitConfig {
|
||||||
|
if utils.FileExist(".env") {
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error loading .env file: %v", err)
|
log.Fatalf("Error loading .env file: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listenAddress := os.Getenv("PLAKKEN_LISTEN")
|
listenAddress := os.Getenv("PLAKKEN_LISTEN")
|
||||||
redisAddress := os.Getenv("PLAKKEN_REDIS_ADDRESS")
|
redisAddress := os.Getenv("PLAKKEN_REDIS_ADDRESS")
|
||||||
|
@ -44,7 +48,7 @@ func GetConfig() InitConfig {
|
||||||
log.Fatal("Invalid PLAKKEN_URL_LENGTH")
|
log.Fatal("Invalid PLAKKEN_URL_LENGTH")
|
||||||
}
|
}
|
||||||
|
|
||||||
if urlLength > 255 {
|
if urlLength > constant.MaxURLLength {
|
||||||
log.Fatal("PLAKKEN_URL_LENGTH cannot be greater than 255")
|
log.Fatal("PLAKKEN_URL_LENGTH cannot be greater than 255")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +58,6 @@ func GetConfig() InitConfig {
|
||||||
RedisUser: os.Getenv("PLAKKEN_REDIS_USER"),
|
RedisUser: os.Getenv("PLAKKEN_REDIS_USER"),
|
||||||
RedisPassword: os.Getenv("PLAKKEN_REDIS_PASSWORD"),
|
RedisPassword: os.Getenv("PLAKKEN_REDIS_PASSWORD"),
|
||||||
RedisDB: redisDB,
|
RedisDB: redisDB,
|
||||||
UrlLength: uint8(urlLength),
|
URLLength: uint8(urlLength),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,15 @@ import "time"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HTTPTimeout = 3 * time.Second
|
HTTPTimeout = 3 * time.Second
|
||||||
|
ExpirationCurlCreate = 604800 * time.Second // Second in one week
|
||||||
|
TokenLength = 32
|
||||||
|
ArgonSaltSize = 16
|
||||||
|
ArgonMemory = 64 * 1024
|
||||||
|
ArgonThreads = 4
|
||||||
|
ArgonKeyLength = 32
|
||||||
|
ArgonIterations = 2
|
||||||
|
MaxURLLength = 255
|
||||||
|
SecondsInDay = 86400
|
||||||
|
SecondsInHour = 3600
|
||||||
|
SecondsInMinute = 60
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/secret"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,9 +13,9 @@ type DBConfig struct {
|
||||||
DB *redis.Client
|
DB *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx = context.Background()
|
var ctx = context.Background() //nolint:gochecknoglobals
|
||||||
|
|
||||||
// InitDB initialise redis connection settings
|
// InitDB initialise redis connection settings.
|
||||||
func InitDB(addr string, user string, password string, db int) *redis.Options {
|
func InitDB(addr string, user string, password string, db int) *redis.Options {
|
||||||
DBConfig := &redis.Options{
|
DBConfig := &redis.Options{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
|
@ -26,10 +27,21 @@ func InitDB(addr string, user string, password string, db int) *redis.Options {
|
||||||
return DBConfig
|
return DBConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectDB make new database connection
|
// ConnectDB make new database connection.
|
||||||
func ConnectDB(dbConfig *redis.Options) *redis.Client {
|
func ConnectDB(dbConfig *redis.Options) *redis.Client {
|
||||||
localDb := redis.NewClient(dbConfig)
|
localDB := redis.NewClient(dbConfig)
|
||||||
return localDb
|
|
||||||
|
return localDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping test connection to Redis database.
|
||||||
|
func Ping(db *redis.Client) error {
|
||||||
|
status := db.Ping(ctx)
|
||||||
|
if status.String() != "ping: PONG" {
|
||||||
|
return &pingError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config DBConfig) InsertPaste(key string, content string, secret string, ttl time.Duration) {
|
func (config DBConfig) InsertPaste(key string, content string, secret string, ttl time.Duration) {
|
||||||
|
@ -55,10 +67,17 @@ func (config DBConfig) InsertPaste(key string, content string, secret string, tt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config DBConfig) UrlExist(url string) bool {
|
func (config DBConfig) URLExist(url string) bool {
|
||||||
return config.DB.Exists(ctx, url).Val() == 1
|
return config.DB.Exists(ctx, url).Val() == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config DBConfig) VerifySecret(url string, secret string) bool {
|
func (config DBConfig) VerifySecret(url string, token string) (bool, error) {
|
||||||
return secret == config.DB.HGet(ctx, url, "secret").Val()
|
storedHash := config.DB.HGet(ctx, url, "secret").Val()
|
||||||
|
|
||||||
|
result, err := secret.VerifyPassword(token, storedHash)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
7
internal/database/error.go
Normal file
7
internal/database/error.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
type pingError struct{}
|
||||||
|
|
||||||
|
func (m *pingError) Error() string {
|
||||||
|
return "Connection to redis not work"
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
package httpServer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/constant"
|
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/web/plak"
|
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/web/static"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServerConfig struct {
|
|
||||||
HTTPServer *http.Server
|
|
||||||
UrlLength uint8
|
|
||||||
DB *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure HTTP router
|
|
||||||
func (config ServerConfig) router(_ http.ResponseWriter, _ *http.Request) {
|
|
||||||
WebConfig := plak.WebConfig{
|
|
||||||
DB: config.DB,
|
|
||||||
UrlLength: config.UrlLength,
|
|
||||||
}
|
|
||||||
|
|
||||||
http.HandleFunc("GET /{$}", static.Home)
|
|
||||||
http.HandleFunc("GET /{key}/{settings...}", WebConfig.View)
|
|
||||||
http.HandleFunc("GET /static/{file}", static.ServeStatic)
|
|
||||||
http.HandleFunc("POST /{$}", WebConfig.Create)
|
|
||||||
http.HandleFunc("DELETE /{key}", WebConfig.Delete)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config Configure HTTP server
|
|
||||||
func Config(listenAddress string) *http.Server {
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: listenAddress,
|
|
||||||
ReadTimeout: constant.HTTPTimeout,
|
|
||||||
WriteTimeout: constant.HTTPTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server Start HTTP server
|
|
||||||
func (config ServerConfig) Server() {
|
|
||||||
log.Println("Listening on " + config.HTTPServer.Addr)
|
|
||||||
|
|
||||||
http.HandleFunc("/", config.router)
|
|
||||||
|
|
||||||
log.Fatal(config.HTTPServer.ListenAndServe())
|
|
||||||
}
|
|
67
internal/httpserver/server.go
Normal file
67
internal/httpserver/server.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/constant"
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/web/plak"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
HTTPServer *http.Server
|
||||||
|
URLLength uint8
|
||||||
|
DB *redis.Client
|
||||||
|
Static embed.FS
|
||||||
|
Templates embed.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config ServerConfig) home(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
index, err := config.Static.ReadFile("static/index.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
_, err = w.Write(index)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure HTTP router.
|
||||||
|
func (config ServerConfig) router() {
|
||||||
|
WebConfig := plak.WebConfig{
|
||||||
|
DB: config.DB,
|
||||||
|
URLLength: config.URLLength,
|
||||||
|
Templates: config.Templates,
|
||||||
|
}
|
||||||
|
staticFiles := http.FS(config.Static)
|
||||||
|
|
||||||
|
http.HandleFunc("GET /{$}", config.home)
|
||||||
|
http.Handle("GET /static/{file}", http.FileServer(staticFiles))
|
||||||
|
http.HandleFunc("GET /{key}/{settings...}", WebConfig.View)
|
||||||
|
http.HandleFunc("POST /{$}", WebConfig.CurlCreate)
|
||||||
|
http.HandleFunc("POST /create/{$}", WebConfig.PostCreate)
|
||||||
|
http.HandleFunc("DELETE /{key}", WebConfig.DeleteRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config Configure HTTP server.
|
||||||
|
func Config(listenAddress string) *http.Server {
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: listenAddress,
|
||||||
|
ReadTimeout: constant.HTTPTimeout,
|
||||||
|
WriteTimeout: constant.HTTPTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server Start HTTP server.
|
||||||
|
func (config ServerConfig) Server() {
|
||||||
|
log.Println("Listening on " + config.HTTPServer.Addr)
|
||||||
|
|
||||||
|
config.router()
|
||||||
|
|
||||||
|
log.Fatal(config.HTTPServer.ListenAndServe())
|
||||||
|
}
|
161
internal/secret/crypto.go
Normal file
161
internal/secret/crypto.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
// Package secret implement all crypto utils like password hashing and secret generation
|
||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/constant"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type argon2idHash struct {
|
||||||
|
salt []byte
|
||||||
|
hash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argon2id config.
|
||||||
|
type config struct {
|
||||||
|
saltLength uint8
|
||||||
|
memory uint32
|
||||||
|
threads uint8
|
||||||
|
keyLength uint32
|
||||||
|
iterations uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSecret for password hashing or token generation.
|
||||||
|
func generateSecret(length uint8) ([]byte, error) {
|
||||||
|
secret := make([]byte, length)
|
||||||
|
|
||||||
|
_, err := rand.Read(secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken generate hexadecimal token.
|
||||||
|
func GenerateToken() (string, error) {
|
||||||
|
secret, err := generateSecret(constant.TokenLength)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token := hex.EncodeToString(secret)
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateArgon2ID Generate an argon2id hash from source string and specified salt.
|
||||||
|
func (config config) generateArgon2ID(source string, salt []byte) []byte {
|
||||||
|
hash := argon2.IDKey([]byte(source), salt, config.iterations, config.memory, config.threads, config.keyLength)
|
||||||
|
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password hash a source string with argon2id, return properly encoded hash.
|
||||||
|
func Password(password string) (string, error) {
|
||||||
|
config := config{
|
||||||
|
saltLength: constant.ArgonSaltSize,
|
||||||
|
memory: constant.ArgonMemory,
|
||||||
|
threads: constant.ArgonThreads,
|
||||||
|
keyLength: constant.ArgonKeyLength,
|
||||||
|
iterations: constant.ArgonIterations,
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := generateSecret(config.saltLength)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := config.generateArgon2ID(password, salt)
|
||||||
|
|
||||||
|
base64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||||
|
base64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||||
|
|
||||||
|
formatted := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, config.memory, config.iterations, config.threads, base64Salt, base64Hash)
|
||||||
|
|
||||||
|
return formatted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPassword check is source password and stored password is similar, take password and a properly encoded hash.
|
||||||
|
func VerifyPassword(password string, hash string) (bool, error) {
|
||||||
|
argon2Hash, config, err := parseHash(hash)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := config.generateArgon2ID(password, argon2Hash.salt)
|
||||||
|
|
||||||
|
return bytes.Equal(result, argon2Hash.hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHash parse existing encoded argon2id string.
|
||||||
|
func parseHash(source string) (argon2idHash, config, error) {
|
||||||
|
separateItem := strings.Split(source, "$")
|
||||||
|
if len(separateItem) != 6 { //nolint:gomnd
|
||||||
|
return argon2idHash{}, config{}, &parseError{message: "Hash format is not valid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if separateItem[1] != "argon2id" {
|
||||||
|
return argon2idHash{}, config{}, &parseError{message: "Algorithm is not valid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
separateParam := strings.Split(separateItem[3], ",")
|
||||||
|
if len(separateParam) != 3 { //nolint:gomnd
|
||||||
|
return argon2idHash{}, config{}, &parseError{message: "Hash config is not valid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := base64.RawStdEncoding.Strict().DecodeString(separateItem[4])
|
||||||
|
if err != nil {
|
||||||
|
return argon2idHash{}, config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash []byte
|
||||||
|
hash, err = base64.RawStdEncoding.Strict().DecodeString(separateItem[5])
|
||||||
|
if err != nil {
|
||||||
|
return argon2idHash{}, config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
saltLength := uint8(len(salt))
|
||||||
|
keyLength := uint32(len(hash))
|
||||||
|
|
||||||
|
var memory int
|
||||||
|
memory, err = strconv.Atoi(strings.ReplaceAll(separateParam[0], "m=", ""))
|
||||||
|
if err != nil {
|
||||||
|
return argon2idHash{}, config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var iterations int
|
||||||
|
iterations, err = strconv.Atoi(strings.ReplaceAll(separateParam[1], "t=", ""))
|
||||||
|
if err != nil {
|
||||||
|
return argon2idHash{}, config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var threads int
|
||||||
|
threads, err = strconv.Atoi(strings.ReplaceAll(separateParam[2], "p=", ""))
|
||||||
|
if err != nil {
|
||||||
|
return argon2idHash{}, config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
argon2idStruct := argon2idHash{
|
||||||
|
salt: salt,
|
||||||
|
hash: hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
hashConfig := config{
|
||||||
|
saltLength: saltLength,
|
||||||
|
memory: uint32(memory),
|
||||||
|
threads: uint8(threads),
|
||||||
|
iterations: uint32(iterations),
|
||||||
|
keyLength: keyLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
return argon2idStruct, hashConfig, nil
|
||||||
|
}
|
9
internal/secret/error.go
Normal file
9
internal/secret/error.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package secret
|
||||||
|
|
||||||
|
type parseError struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *parseError) Error() string {
|
||||||
|
return "parseHash: " + m.message
|
||||||
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
type ParseIntBeforeSeparatorError struct {
|
type parseIntBeforeSeparatorError struct {
|
||||||
Message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ParseIntBeforeSeparatorError) Error() string {
|
func (m *parseIntBeforeSeparatorError) Error() string {
|
||||||
return "parseIntBeforeSeparator: " + m.Message
|
return "parseIntBeforeSeparator: " + m.message
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParseExpirationError struct {
|
type ParseExpirationError struct {
|
||||||
Message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ParseExpirationError) Error() string {
|
func (m *ParseExpirationError) Error() string {
|
||||||
return "parseIntBeforeSeparator: " + m.Message
|
return "parseIntBeforeSeparator: " + m.message
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,36 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"errors"
|
||||||
"encoding/hex"
|
|
||||||
"log"
|
"log"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand/v2"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateUrl generate random string for plak url
|
// GenerateURL generate random string for plak url.
|
||||||
func GenerateUrl(length uint8) string {
|
func GenerateURL(length uint8) string {
|
||||||
listChars := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
listChars := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||||
b := make([]rune, length)
|
b := make([]rune, length)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = listChars[mathrand.Intn(len(listChars))]
|
b[i] = listChars[mathrand.IntN(len(listChars))]
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSecret generate random secret (32 bytes hexadecimal)
|
// CheckCharRedundant verify is a character is redundant in a string.
|
||||||
func GenerateSecret() string {
|
|
||||||
key := make([]byte, 32)
|
|
||||||
_, err := rand.Read(key)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to generate secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.EncodeToString(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckCharRedundant verify is a character is redundant in a string
|
|
||||||
func CheckCharRedundant(source string, char string) bool { // Verify if a char is redundant
|
func CheckCharRedundant(source string, char string) bool { // Verify if a char is redundant
|
||||||
if strings.Count(source, char) > 1 {
|
return strings.Count(source, char) > 1
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseIntBeforeSeparator(source *string, sep string) (int, error) { // return 0 & error if error, only accept positive number
|
func parseIntBeforeSeparator(source *string, sep string) (int, error) { // return 0 & error if error, only accept positive number
|
||||||
if CheckCharRedundant(*source, sep) {
|
if CheckCharRedundant(*source, sep) {
|
||||||
return 0, &ParseIntBeforeSeparatorError{Message: *source + ": cannot parse value as int"}
|
return 0, &parseIntBeforeSeparatorError{message: *source + ": cannot parse value as int"}
|
||||||
}
|
}
|
||||||
var value int
|
var value int
|
||||||
var err error
|
var err error
|
||||||
|
@ -49,22 +38,24 @@ func parseIntBeforeSeparator(source *string, sep string) (int, error) { // retur
|
||||||
value, err = strconv.Atoi(strings.Split(*source, sep)[0])
|
value, err = strconv.Atoi(strings.Split(*source, sep)[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 0, &ParseIntBeforeSeparatorError{Message: *source + ": cannot parse value as int"}
|
|
||||||
|
return 0, &parseIntBeforeSeparatorError{message: *source + ": cannot parse value as int"}
|
||||||
}
|
}
|
||||||
if value < 0 { // Only positive value is correct
|
if value < 0 { // Only positive value is correct
|
||||||
return 0, &ParseIntBeforeSeparatorError{Message: *source + ": format only take positive value"}
|
return 0, &parseIntBeforeSeparatorError{message: *source + ": format only take positive value"}
|
||||||
}
|
}
|
||||||
|
|
||||||
if value > 99 {
|
if value > 99 { //nolint:gomnd
|
||||||
return 0, &ParseIntBeforeSeparatorError{Message: *source + ": Format only take two number"}
|
return 0, &parseIntBeforeSeparatorError{message: *source + ": Format only take two number"}
|
||||||
}
|
}
|
||||||
|
|
||||||
*source = strings.Join(strings.Split(*source, sep)[1:], "")
|
*source = strings.Join(strings.Split(*source, sep)[1:], "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseExpiration Parse "1d1h1m1s" duration format. Return 0 & error if error
|
// ParseExpiration Parse "1d1h1m1s" duration format. Return 0 & error if error.
|
||||||
func ParseExpiration(source string) (int, error) {
|
func ParseExpiration(source string) (int, error) {
|
||||||
var expiration int
|
var expiration int
|
||||||
var tempOutput int
|
var tempOutput int
|
||||||
|
@ -76,29 +67,51 @@ func ParseExpiration(source string) (int, error) {
|
||||||
source = strings.ToLower(source)
|
source = strings.ToLower(source)
|
||||||
|
|
||||||
tempOutput, err = parseIntBeforeSeparator(&source, "d")
|
tempOutput, err = parseIntBeforeSeparator(&source, "d")
|
||||||
expiration = tempOutput * 86400
|
expiration = tempOutput * constant.SecondsInDay
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 0, &ParseExpirationError{Message: "Invalid syntax"}
|
|
||||||
|
return 0, &ParseExpirationError{message: "Invalid syntax"}
|
||||||
}
|
}
|
||||||
tempOutput, err = parseIntBeforeSeparator(&source, "h")
|
tempOutput, err = parseIntBeforeSeparator(&source, "h")
|
||||||
expiration += tempOutput * 3600
|
expiration += tempOutput * constant.SecondsInHour
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 0, &ParseExpirationError{Message: "Invalid syntax"}
|
|
||||||
|
return 0, &ParseExpirationError{message: "Invalid syntax"}
|
||||||
}
|
}
|
||||||
tempOutput, err = parseIntBeforeSeparator(&source, "m")
|
tempOutput, err = parseIntBeforeSeparator(&source, "m")
|
||||||
expiration += tempOutput * 60
|
expiration += tempOutput * constant.SecondsInMinute
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 0, &ParseExpirationError{Message: "Invalid syntax"}
|
|
||||||
|
return 0, &ParseExpirationError{message: "Invalid syntax"}
|
||||||
}
|
}
|
||||||
tempOutput, err = parseIntBeforeSeparator(&source, "s")
|
tempOutput, err = parseIntBeforeSeparator(&source, "s")
|
||||||
expiration += tempOutput * 1
|
expiration += tempOutput * 1
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return 0, &ParseExpirationError{Message: "Invalid syntax"}
|
|
||||||
|
return 0, &ParseExpirationError{message: "Invalid syntax"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return expiration, nil
|
return expiration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidKey Verify if a key is valid (only letter, number, - and _).
|
||||||
|
func ValidKey(key string) bool {
|
||||||
|
result, err := regexp.MatchString("^[a-zA-Z0-9_.-]*$", key)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
log.Println(key, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExist verify if a file exist.
|
||||||
|
func FileExist(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
|
||||||
|
return !errors.Is(err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
package plak
|
package plak
|
||||||
|
|
||||||
type DeletePlakError struct {
|
type deletePlakError struct {
|
||||||
Name string
|
name string
|
||||||
Err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DeletePlakError) Error() string {
|
func (m *deletePlakError) Error() string {
|
||||||
return "Cannot delete: " + m.Name + " : " + m.Err.Error()
|
return "Cannot delete: " + m.name + " : " + m.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
type createError struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *createError) Error() string {
|
||||||
|
return "create: cannot create plak: " + m.message
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,87 +2,198 @@ package plak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/constant"
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/database"
|
"git.gnous.eu/gnouseu/plakken/internal/database"
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/secret"
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/utils"
|
"git.gnous.eu/gnouseu/plakken/internal/utils"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
"html/template"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ctx = context.Background()
|
var ctx = context.Background() //nolint:gochecknoglobals
|
||||||
|
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
DB *redis.Client
|
DB *redis.Client
|
||||||
UrlLength uint8
|
URLLength uint8
|
||||||
|
Templates embed.FS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plak "Object" for plak
|
// plak "Object" for plak.
|
||||||
type Plak struct {
|
type plak struct {
|
||||||
Key string
|
Key string
|
||||||
Content string
|
Content string
|
||||||
Expiration time.Duration
|
Expiration time.Duration
|
||||||
DB *redis.Client
|
DB *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create manage POST request for create Plak
|
// create a plak.
|
||||||
func (config WebConfig) Create(w http.ResponseWriter, r *http.Request) {
|
func (plak plak) create() (string, error) {
|
||||||
|
dbConf := database.DBConfig{
|
||||||
|
DB: plak.DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := secret.GenerateToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbConf.URLExist(plak.Key) {
|
||||||
|
return "", &createError{message: "key already exist"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashedSecret string
|
||||||
|
hashedSecret, err = secret.Password(token)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConf.InsertPaste(plak.Key, plak.Content, hashedSecret, plak.Expiration)
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostCreate manage POST request for create plak.
|
||||||
|
func (config WebConfig) PostCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
content := r.FormValue("content")
|
content := r.FormValue("content")
|
||||||
if content == "" {
|
if content == "" {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dbConf := database.DBConfig{
|
filename := r.FormValue("filename")
|
||||||
|
var key string
|
||||||
|
if len(filename) == 0 {
|
||||||
|
key = utils.GenerateURL(config.URLLength)
|
||||||
|
} else {
|
||||||
|
if !utils.ValidKey(filename) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
plak := plak{
|
||||||
|
Key: key,
|
||||||
|
Content: content,
|
||||||
DB: config.DB,
|
DB: config.DB,
|
||||||
}
|
}
|
||||||
|
|
||||||
secret := utils.GenerateSecret()
|
|
||||||
key := utils.GenerateUrl(config.UrlLength)
|
|
||||||
rawExpiration := r.FormValue("exp")
|
rawExpiration := r.FormValue("exp")
|
||||||
expiration, err := utils.ParseExpiration(rawExpiration)
|
expiration, err := utils.ParseExpiration(rawExpiration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
log.Println(err)
|
||||||
} else if expiration == 0 {
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
dbConf.InsertPaste(key, content, secret, -1)
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if expiration == 0 {
|
||||||
|
plak.Expiration = -1
|
||||||
} else {
|
} else {
|
||||||
dbConf.InsertPaste(key, content, secret, time.Duration(expiration*int(time.Second)))
|
plak.Expiration = time.Duration(expiration * int(time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, key, http.StatusSeeOther)
|
_, err = plak.create()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// View for plak
|
http.Redirect(w, r, "/"+key, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurlCreate PostCreate plak with minimum param, ideal for curl. Force 7 day expiration.
|
||||||
|
func (config WebConfig) CurlCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.ContentLength == 0 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, _ := io.ReadAll(r.Body)
|
||||||
|
err := r.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := utils.GenerateURL(config.URLLength)
|
||||||
|
|
||||||
|
plak := plak{
|
||||||
|
Key: key,
|
||||||
|
Content: string(content),
|
||||||
|
Expiration: constant.ExpirationCurlCreate,
|
||||||
|
DB: config.DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
token, err = plak.create()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseURL string
|
||||||
|
if r.TLS == nil {
|
||||||
|
baseURL = "http://" + r.Host + "/" + key
|
||||||
|
} else {
|
||||||
|
baseURL = "https://" + r.Host + "/" + key
|
||||||
|
}
|
||||||
|
|
||||||
|
message := baseURL + "\n" + "Delete with : 'curl -X DELETE " + baseURL + "?secret\\=" + token + "'" + "\n"
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
_, err = io.WriteString(w, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// View for plak.
|
||||||
func (config WebConfig) View(w http.ResponseWriter, r *http.Request) {
|
func (config WebConfig) View(w http.ResponseWriter, r *http.Request) {
|
||||||
dbConf := database.DBConfig{
|
dbConf := database.DBConfig{
|
||||||
DB: config.DB,
|
DB: config.DB,
|
||||||
}
|
}
|
||||||
var plak Plak
|
var currentPlak plak
|
||||||
key := r.PathValue("key")
|
key := r.PathValue("key")
|
||||||
|
|
||||||
if dbConf.UrlExist(key) {
|
//nolint:nestif
|
||||||
plak = Plak{
|
if dbConf.URLExist(key) {
|
||||||
|
currentPlak = plak{
|
||||||
Key: key,
|
Key: key,
|
||||||
DB: config.DB,
|
DB: config.DB,
|
||||||
}
|
}
|
||||||
plak = plak.GetContent()
|
currentPlak = currentPlak.getContent()
|
||||||
if r.PathValue("settings") == "raw" {
|
if r.PathValue("settings") == "raw" {
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
_, err := io.WriteString(w, plak.Content)
|
_, err := io.WriteString(w, currentPlak.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t, err := template.ParseFiles("templates/paste.html")
|
t, err := template.ParseFS(config.Templates, "templates/paste.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
err = t.Execute(w, plak)
|
err = t.Execute(w, currentPlak)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -90,45 +201,61 @@ func (config WebConfig) View(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete manage plak deletion endpoint
|
// DeleteRequest manage plak deletion endpoint.
|
||||||
func (config WebConfig) Delete(w http.ResponseWriter, r *http.Request) {
|
func (config WebConfig) DeleteRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
dbConf := database.DBConfig{
|
dbConf := database.DBConfig{
|
||||||
DB: config.DB,
|
DB: config.DB,
|
||||||
}
|
}
|
||||||
key := r.PathValue("key")
|
key := r.PathValue("key")
|
||||||
|
var valid bool
|
||||||
|
|
||||||
if dbConf.UrlExist(key) {
|
//nolint:nestif
|
||||||
secret := r.URL.Query().Get("secret")
|
if dbConf.URLExist(key) {
|
||||||
if dbConf.VerifySecret(key, secret) {
|
var err error
|
||||||
plak := Plak{
|
token := r.URL.Query().Get("secret")
|
||||||
|
|
||||||
|
valid, err = dbConf.VerifySecret(key, token)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if valid {
|
||||||
|
plak := plak{
|
||||||
Key: key,
|
Key: key,
|
||||||
DB: config.DB,
|
DB: config.DB,
|
||||||
}
|
}
|
||||||
err := plak.deletePlak()
|
err := plak.delete()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deletePlak Delete plak from database
|
// delete DeleteRequest plak from database.
|
||||||
func (plak Plak) deletePlak() error {
|
func (plak plak) delete() error {
|
||||||
err := plak.DB.Del(ctx, plak.Key).Err()
|
err := plak.DB.Del(ctx, plak.Key).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return &DeletePlakError{Name: plak.Key, Err: err}
|
|
||||||
|
return &deletePlakError{name: plak.Key, err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContent get plak content
|
// getContent get plak content.
|
||||||
func (plak Plak) GetContent() Plak {
|
func (plak plak) getContent() plak {
|
||||||
plak.Content = plak.DB.HGet(ctx, plak.Key, "content").Val()
|
plak.Content = plak.DB.HGet(ctx, plak.Key, "content").Val()
|
||||||
|
|
||||||
return plak
|
return plak
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
package static
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServeStatic Serve static file from static
|
|
||||||
func ServeStatic(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, "./static/"+r.PathValue("file")) // TODO: vérifier si c'est safe
|
|
||||||
}
|
|
||||||
|
|
||||||
// Home Serve index.html
|
|
||||||
func Home(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, "./static/index.html")
|
|
||||||
}
|
|
24
main.go
24
main.go
|
@ -1,20 +1,36 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
|
"log"
|
||||||
|
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/config"
|
"git.gnous.eu/gnouseu/plakken/internal/config"
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/database"
|
"git.gnous.eu/gnouseu/plakken/internal/database"
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/httpServer"
|
"git.gnous.eu/gnouseu/plakken/internal/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed templates
|
||||||
|
templates embed.FS
|
||||||
|
//go:embed static
|
||||||
|
static embed.FS
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
initConfig := config.GetConfig()
|
initConfig := config.GetConfig()
|
||||||
dbConfig := database.InitDB(initConfig.RedisAddress, initConfig.RedisUser, initConfig.RedisPassword, initConfig.RedisDB)
|
dbConfig := database.InitDB(initConfig.RedisAddress, initConfig.RedisUser, initConfig.RedisPassword, initConfig.RedisDB)
|
||||||
db := database.ConnectDB(dbConfig)
|
db := database.ConnectDB(dbConfig)
|
||||||
|
err := database.Ping(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
serverConfig := httpServer.ServerConfig{
|
serverConfig := httpserver.ServerConfig{
|
||||||
HTTPServer: httpServer.Config(initConfig.ListenAddress),
|
HTTPServer: httpserver.Config(initConfig.ListenAddress),
|
||||||
UrlLength: initConfig.UrlLength,
|
URLLength: initConfig.URLLength,
|
||||||
DB: db,
|
DB: db,
|
||||||
|
Static: static,
|
||||||
|
Templates: templates,
|
||||||
}
|
}
|
||||||
|
|
||||||
serverConfig.Server()
|
serverConfig.Server()
|
||||||
|
|
3
renovate.json
Normal file
3
renovate.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
|
@ -15,11 +15,11 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<form class="fr" method="post">
|
<form action="/create/" class="fr" method="post">
|
||||||
<div id="line-numbers">1</div>
|
<div id="line-numbers">1</div>
|
||||||
<label for="content"><textarea autofocus id="content" name="content"
|
<label for="content"><textarea autofocus id="content" name="content"
|
||||||
placeholder="Type your paste here"></textarea></label>
|
placeholder="Type your paste here"></textarea></label>
|
||||||
</form>
|
|
||||||
<div class="menu fr">
|
<div class="menu fr">
|
||||||
<label for="filename"><input id="filename" name="filename" placeholder="Filename" type="text"></label>
|
<label for="filename"><input id="filename" name="filename" placeholder="Filename" type="text"></label>
|
||||||
<!--<label for="password"><input id="password" placeholder="Password" type="password"></label>-->
|
<!--<label for="password"><input id="password" placeholder="Password" type="password"></label>-->
|
||||||
|
@ -44,17 +44,13 @@
|
||||||
<option value="yml">YAML</option>
|
<option value="yml">YAML</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button title="Save plak" type="submit">
|
<button title="Save plak" type="submit">Save</button>
|
||||||
<svg viewBox="0 0 24 24">
|
|
||||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
||||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<section id="recent-plaks">
|
<section id="recent-plaks">
|
||||||
<div class="title fr">
|
<div class="title fr">
|
||||||
<h3>Recent plaks</h2>
|
<h3>Recent plaks</h3>
|
||||||
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2.5">
|
<svg height="32" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" width="32">
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
BIN
static/inter.woff2
Normal file
BIN
static/inter.woff2
Normal file
Binary file not shown.
BIN
static/jetbrainsmono.woff2
Normal file
BIN
static/jetbrainsmono.woff2
Normal file
Binary file not shown.
|
@ -1,5 +1,3 @@
|
||||||
@charset "UTF-8";
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=JetBrains+Mono&display=swap");
|
|
||||||
section#recent-plaks {
|
section#recent-plaks {
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -79,6 +77,22 @@ h3 {
|
||||||
background-color: #e5e5e7;
|
background-color: #e5e5e7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(inter.woff2) format("woff2");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(jetbrainsmono.woff2) format("woff2");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
background-color: #0f0f0f;
|
background-color: #0f0f0f;
|
||||||
color: #e5e5e7;
|
color: #e5e5e7;
|
||||||
|
@ -168,11 +182,11 @@ select:focus {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
² option {
|
option {
|
||||||
background-color: #0f0f0f;
|
background-color: #0f0f0f;
|
||||||
color: #e5e5e7;
|
color: #e5e5e7;
|
||||||
}
|
}
|
||||||
² option:focus {
|
option:focus {
|
||||||
background-color: #d30f45;
|
background-color: #d30f45;
|
||||||
color: #0f0f0f;
|
color: #0f0f0f;
|
||||||
}
|
}
|
||||||
|
|
58
test/secret/secret_test.go
Normal file
58
test/secret/secret_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package secret_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/constant"
|
||||||
|
"git.gnous.eu/gnouseu/plakken/internal/secret"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecret(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testPasswordFormat(t)
|
||||||
|
testVerifyPassword(t)
|
||||||
|
testVerifyPasswordInvalid(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPasswordFormat(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
regex := fmt.Sprintf("\\$argon2id\\$v=%d\\$m=%d,t=%d,p=%d\\$[A-Za-z0-9+/]*\\$[A-Za-z0-9+/]*$", argon2.Version, constant.ArgonMemory, constant.ArgonIterations, constant.ArgonThreads) //nolint:lll
|
||||||
|
|
||||||
|
got, err := secret.Password("Password!")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := regexp.MatchString(regex, got)
|
||||||
|
if !result {
|
||||||
|
t.Fatal("Error in Password, format is not valid "+": ", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testVerifyPassword(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
result, err := secret.VerifyPassword("Password!", "$argon2id$v=19$m=65536,t=2,p=4$A+t5YGpyy1BHCbvk/LP1xQ$eNuUj6B2ZqXlGi6KEqep39a7N4nysUIojuPXye+Ypp0") //nolint:lll
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result {
|
||||||
|
t.Fatal("Error in VerifyPassword, got:", result, "want: ", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testVerifyPasswordInvalid(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
result, err := secret.VerifyPassword("notsamepassword", "$argon2id$v=19$m=65536,t=2,p=4$A+t5YGpyy1BHCbvk/LP1xQ$eNuUj6B2ZqXlGi6KEqep39a7N4nysUIojuPXye+Ypp0") //nolint:lll
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result {
|
||||||
|
t.Fatal("Error in VerifyPassword, got:", result, "want: ", false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,25 @@ import (
|
||||||
"git.gnous.eu/gnouseu/plakken/internal/utils"
|
"git.gnous.eu/gnouseu/plakken/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckCharNotRedundantTrue(t *testing.T) { // Test CheckCharRedundant with redundant char
|
func TestUtils(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCheckCharNotRedundantTrue(t)
|
||||||
|
testCheckCharNotRedundantFalse(t)
|
||||||
|
testParseExpirationFull(t)
|
||||||
|
testParseExpirationMissing(t)
|
||||||
|
testParseExpirationWithCaps(t)
|
||||||
|
testParseExpirationNull(t)
|
||||||
|
testParseExpirationNegative(t)
|
||||||
|
testParseExpirationInvalid(t)
|
||||||
|
testParseExpirationInvalidRedundant(t)
|
||||||
|
testParseExpirationInvalidTooHigh(t)
|
||||||
|
testValidKey(t)
|
||||||
|
testInValidKey(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCheckCharNotRedundantTrue(t *testing.T) { // Test CheckCharRedundant with redundant char
|
||||||
|
t.Helper()
|
||||||
want := true
|
want := true
|
||||||
got := utils.CheckCharRedundant("2d1h3md4h7s", "h")
|
got := utils.CheckCharRedundant("2d1h3md4h7s", "h")
|
||||||
if got != want {
|
if got != want {
|
||||||
|
@ -15,7 +33,8 @@ func TestCheckCharNotRedundantTrue(t *testing.T) { // Test CheckCharRedundant wi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckCharNotRedundantFalse(t *testing.T) { // Test CheckCharRedundant with not redundant char
|
func testCheckCharNotRedundantFalse(t *testing.T) { // Test CheckCharRedundant with not redundant char
|
||||||
|
t.Helper()
|
||||||
want := false
|
want := false
|
||||||
got := utils.CheckCharRedundant("2d1h3m47s", "h")
|
got := utils.CheckCharRedundant("2d1h3m47s", "h")
|
||||||
if got != want {
|
if got != want {
|
||||||
|
@ -23,7 +42,8 @@ func TestCheckCharNotRedundantFalse(t *testing.T) { // Test CheckCharRedundant w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExpirationFull(t *testing.T) { // test parseExpirationFull with all valid separator
|
func testParseExpirationFull(t *testing.T) { // test parseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
result, _ := utils.ParseExpiration("2d1h3m47s")
|
result, _ := utils.ParseExpiration("2d1h3m47s")
|
||||||
correctValue := 176627
|
correctValue := 176627
|
||||||
if result != correctValue {
|
if result != correctValue {
|
||||||
|
@ -31,7 +51,8 @@ func TestParseExpirationFull(t *testing.T) { // test parseExpirationFull with al
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExpirationMissing(t *testing.T) { // test parseExpirationFull with all valid separator
|
func testParseExpirationMissing(t *testing.T) { // test parseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
result, _ := utils.ParseExpiration("1h47s")
|
result, _ := utils.ParseExpiration("1h47s")
|
||||||
correctValue := 3647
|
correctValue := 3647
|
||||||
if result != correctValue {
|
if result != correctValue {
|
||||||
|
@ -39,7 +60,8 @@ func TestParseExpirationMissing(t *testing.T) { // test parseExpirationFull with
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExpirationWithCaps(t *testing.T) { // test parseExpirationFull with all valid separator
|
func testParseExpirationWithCaps(t *testing.T) { // test parseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
result, _ := utils.ParseExpiration("2D1h3M47s")
|
result, _ := utils.ParseExpiration("2D1h3M47s")
|
||||||
correctValue := 176627
|
correctValue := 176627
|
||||||
if result != correctValue {
|
if result != correctValue {
|
||||||
|
@ -47,7 +69,8 @@ func TestParseExpirationWithCaps(t *testing.T) { // test parseExpirationFull wit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExpirationNull(t *testing.T) { // test ParseExpirationFull with all valid separator
|
func testParseExpirationNull(t *testing.T) { // test ParseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
result, _ := utils.ParseExpiration("0")
|
result, _ := utils.ParseExpiration("0")
|
||||||
correctValue := 0
|
correctValue := 0
|
||||||
if result != correctValue {
|
if result != correctValue {
|
||||||
|
@ -55,7 +78,8 @@ func TestParseExpirationNull(t *testing.T) { // test ParseExpirationFull with al
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExpirationNegative(t *testing.T) { // test ParseExpirationFull with all valid separator
|
func testParseExpirationNegative(t *testing.T) { // test ParseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
_, got := utils.ParseExpiration("-42h1m4s")
|
_, got := utils.ParseExpiration("-42h1m4s")
|
||||||
want := &utils.ParseExpirationError{}
|
want := &utils.ParseExpirationError{}
|
||||||
if !errors.As(got, &want) {
|
if !errors.As(got, &want) {
|
||||||
|
@ -63,19 +87,49 @@ func TestParseExpirationNegative(t *testing.T) { // test ParseExpirationFull wit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExpirationInvalid(t *testing.T) { // test ParseExpirationFull with all valid separator
|
func testParseExpirationInvalid(t *testing.T) { // test ParseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
_, got := utils.ParseExpiration("8h42h1m1d4s")
|
_, got := utils.ParseExpiration("8h42h1m1d4s")
|
||||||
want := &utils.ParseExpirationError{}
|
want := &utils.ParseExpirationError{}
|
||||||
if !errors.As(got, &want) {
|
if !errors.As(got, &want) {
|
||||||
t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got)
|
t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseExpirationInvalidRedundant(t *testing.T) { // test ParseExpirationFull with all valid separator
|
func testParseExpirationInvalidRedundant(t *testing.T) { // test ParseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
_, got := utils.ParseExpiration("8h42h1m1h4s")
|
_, got := utils.ParseExpiration("8h42h1m1h4s")
|
||||||
want := &utils.ParseExpirationError{}
|
want := &utils.ParseExpirationError{}
|
||||||
if !errors.As(got, &want) {
|
if !errors.As(got, &want) {
|
||||||
t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got)
|
t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testParseExpirationInvalidTooHigh(t *testing.T) { // test ParseExpirationFull with all valid separator
|
||||||
|
t.Helper()
|
||||||
|
_, got := utils.ParseExpiration("2d1h3m130s")
|
||||||
|
want := &utils.ParseExpirationError{}
|
||||||
|
if !errors.As(got, &want) {
|
||||||
|
t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testValidKey(t *testing.T) { // test ValidKey with a valid key
|
||||||
|
t.Helper()
|
||||||
|
got := utils.ValidKey("ab_a-C42")
|
||||||
|
want := true
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Fatal("Error in ValidKey, want : ", want, "got : ", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInValidKey(t *testing.T) { // test ValidKey with invalid key
|
||||||
|
t.Helper()
|
||||||
|
got := utils.ValidKey("ab_?a-C42")
|
||||||
|
want := false
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Fatal("Error in ValidKey, want : ", want, "got : ", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue