Compare commits

...

79 Commits

Author SHA1 Message Date
Knut Ahlers
45184a496e
Cleanup test workflow
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-02-12 20:27:27 +01:00
Knut Ahlers
1f52d8056e
Update Node dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-02-12 20:23:26 +01:00
Knut Ahlers
86af345d40
Update Go dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2025-02-12 20:15:50 +01:00
Knut Ahlers
8fadf7205f
Release: OTS v1.15.1 2024-12-12 10:33:02 +01:00
Knut Ahlers
0be304ffbe
Update Node dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 10:26:48 +01:00
Knut Ahlers
8a804d93eb
Update Go dependencies
fixes CVE-2024-45337

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-12-12 10:25:56 +01:00
Knut Ahlers
ee46f22447
Release: OTS v1.15.0 2024-12-05 15:19:04 +01:00
Knut Ahlers
9942abc02b
Add alternative appIcon for dark-mode (#204) 2024-12-05 14:59:20 +01:00
Knut Ahlers
6f59345316
CI: Configure commit author
in order to prevent "untrusted" commits

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-11-21 12:49:48 +01:00
Knut Ahlers
87f9b05f6f
Docs: Update badges in README
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-11-21 12:42:35 +01:00
Knut Ahlers
8c5c74f271
Release: OTS v1.14.0 2024-11-21 12:27:19 +01:00
Piotr Icikowski
395a9cea22
Update Polish translation (#201)
Co-authored-by: Luzifer <Luzifer@users.noreply.github.com>
2024-11-21 12:21:52 +01:00
Knut Ahlers
5d6396b8dd
CI: Switch back to pull_request_target event
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-11-21 12:17:59 +01:00
Knut Ahlers
e1db6a7bf6
CI: Switch to different action for commit/push
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-11-21 12:11:00 +01:00
Knut Ahlers
f416dd910b
Fix: Target correct branch for PRs
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-11-21 11:31:57 +01:00
Knut Ahlers
314af1a5f6
CI: Fix workflow not being able to update a pull-request
should work now, though untested

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-10-28 11:00:58 +01:00
James Park-Watt
0049bc03ce
Add periodic in-memory store pruner (#200) 2024-10-27 12:33:53 +01:00
James Park-Watt
8a21dad603
Add 'log-requests' option to disable request logging (#199) 2024-10-27 01:36:35 +02:00
Knut Ahlers
257e87f76f
Deps: Update Go dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-10-26 17:18:21 +02:00
Knut Ahlers
b41db78745
[#181] Add paste ability for files to textarea
closes #181

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-10-26 14:47:16 +02:00
Knut Ahlers
6e2f20aa53
[#184] Add error message when subtle crypto is unavailable
closes #184

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-10-26 14:47:15 +02:00
Knut Ahlers
f796c6d4dc
CI: Only update translation file when changed
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-10-26 14:47:15 +02:00
Knut Ahlers
8cc6f23b04
Add Tiltfile for local development
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-10-26 14:47:15 +02:00
Knut Ahlers
51313d02aa
Fix: Use no-cache to satisfy Trivy
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-10-26 00:11:31 +02:00
James Park-Watt
93cbaff2e6
refactor: modified build container to use golang:1-alpine (#198) 2024-10-26 00:03:15 +02:00
Knut Ahlers
e9371c90a0
CI: Add automated regeneration of translations (#195) 2024-09-23 21:32:57 +02:00
Piotr Icikowski
3a29041652
Update Polish translation (#194)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2024-09-23 20:53:57 +02:00
Knut Ahlers
277a3845ac
[#170] Add button to burn secrets immediately (#193) 2024-09-23 19:50:42 +02:00
Knut Ahlers
28c8eda61e
Lint: Update linter config
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-22 13:10:07 +02:00
Knut Ahlers
4c2b5441d8
Deps: Update Go dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-22 13:07:11 +02:00
Knut Ahlers
48bf8c9ca4
[#191] Add customization to add footer-links (#192) 2024-09-22 12:55:11 +02:00
Knut Ahlers
73209fc52c
CI: Add multi-platform image build
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-09-22 11:53:45 +02:00
Chen Xi
496ace34f4
Add TLS configuration for server (#190)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2024-09-22 11:26:04 +02:00
Knut Ahlers
59efc1c23e
Release: OTS v1.13.0 2024-08-27 18:17:26 +02:00
Knut Ahlers
91c5ae3115
Restore old nl translation as nl-BE
As result of the discussion in #168 this restores the "old" Dutch
translation as `nl-BE` as it seems there are major differences between
the Dutch spoken in NL and the one in BE. This way the differences in
the language are used for the different countries.

The `nl-NL` translation is used as "standard NL" as there are plenty
more `nl-*` language keys. Those are served by the translation for the
Netherlands.

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 17:58:22 +02:00
Knut Ahlers
833cdfb44e
Update Node dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 17:41:41 +02:00
Knut Ahlers
eb2adff4d5
Update Go dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-08-27 17:36:12 +02:00
Stefano Arlandini
5b0ec2ef5b
Add Italian translation (#173) 2024-03-14 10:40:25 +01:00
Knut Ahlers
c9be49d86d
Update dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-03-14 10:33:50 +01:00
Knut Ahlers
3df42cbd32
Lint: Resolve unused-parameter error
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-03-02 11:19:11 +01:00
Knut Ahlers
dd40078301
[ci] Add GHCR publishing
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2024-03-02 11:14:14 +01:00
Martin Gumucio
aa30368612
fixed a spelling error (#171)
Co-authored-by: Your Name <you@example.com>
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2024-02-14 00:54:18 +01:00
MichelB
564cbe530c
Update Dutch translation (#168)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2024-01-30 11:36:48 +01:00
Romain Prévost
d05261987d
Update French translation (#167)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2024-01-29 15:06:58 +01:00
Knut Ahlers
e8f289eda1
Release: OTS v1.12.0 2024-01-24 20:21:22 +01:00
Piotr Icikowski
dfa27ea7c5
Update Polish translation (#166)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2024-01-03 18:21:31 +01:00
Knut Ahlers
fdb528a69f
Update dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-19 13:14:51 +01:00
Knut Ahlers
d9fe7df6cb
[#160] Make success indicator more clear
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-15 22:36:47 +01:00
Martin Reinhardt
142ca55074
Use OCI Label defaults on Docker images (#145)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2023-12-15 18:10:14 +01:00
Knut Ahlers
21f295cfc1
[#160] Add hover tooltips for buttons
except QR-display as that one cannot carry a title in the current usage
of the tooltip component for displaying the QRCode image

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-15 17:28:42 +01:00
Knut Ahlers
98268f1457
[#160] Add auto-resizing textareas
in order to prevent users from having to scroll in secrets

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-15 17:13:58 +01:00
Knut Ahlers
f2a7af30b2
[#159] Add version-command for ots-cli
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-13 16:28:20 +01:00
Knut Ahlers
47a7a686ef
Release: OTS v1.11.1 2023-12-12 23:23:24 +01:00
Knut Ahlers
52a34745d6
[#158] Disable Vue Devtools in release builds
by setting NODE_ENV to production - which should the case for all
published builds anyways

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-12 23:18:19 +01:00
Knut Ahlers
0ca6e4fc5f
Release: OTS v1.11.0 2023-12-10 13:32:50 +01:00
Knut Ahlers
7c74a7c47c
[client] Fix wrong method when creating secrets
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-10 13:29:37 +01:00
Knut Ahlers
391e9a85ce
Fix: Baked in version-string empty in build-local
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-08 10:20:41 +01:00
Knut Ahlers
3ebc896169
[#154] Improve UX for rejected / allowed files
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-01 18:54:46 +01:00
Knut Ahlers
dc47bf0861
[#154] Add debug logging for rejected attachment types & strip meta-info from mime-type (#155) 2023-11-23 10:36:36 +01:00
Knut Ahlers
eb2bce3119
[#149] Make attachments stand out more (#152) 2023-11-22 13:04:23 +01:00
Yo***ie
4bbff364c9
Update Chinese translations (#151)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2023-11-20 17:20:35 +01:00
Knut Ahlers
ce5a61d69e
Add tool to update translations in PRs
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-11-20 17:17:09 +01:00
Knut Ahlers
8d29e5f6ed
[#148] Make secret optional when files are attached (#150) 2023-11-20 15:52:06 +01:00
Knut Ahlers
62d2897e44
Update dependencies
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-11-18 19:10:45 +01:00
Kevin Schley
eaa4b4445a
Add docker-compose example (#114)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2023-11-18 13:42:07 +01:00
Knut Ahlers
616d400f74
Add git-changerelease config
to update versions before tagging

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-11-18 13:16:52 +01:00
Vic Demuzere
3a4e2a58bb
Update K8s example and include customize.yaml (#147) 2023-11-17 17:16:31 +01:00
Knut Ahlers
a71563b19b
prepare release v1.10.0 2023-11-11 14:18:13 +01:00
Knut Ahlers
e5bcfaa79f
[CI] Fix: npm@latest cannot run with Node 18
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-11-08 16:23:38 +01:00
Knut Ahlers
136c0e2c96
Fix: Use a default maxSecretSize and limit the payload read (#144) 2023-10-25 10:11:54 +02:00
Knut Ahlers
5ad6449757
Implement metrics collection for API server (#143) 2023-10-23 14:05:20 +02:00
Knut Ahlers
1623e09225
Add server side check for maximum secret size
closes #138

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-10-21 19:35:57 +02:00
Knut Ahlers
9a530e1c66
Implement attachment checking in CLI (#141) 2023-10-21 17:12:08 +02:00
Piotr Icikowski
34275baa2f
Update Polish translation (#140)
Co-authored-by: Knut Ahlers <knut@ahlers.me>
2023-10-20 23:50:30 +02:00
Knut Ahlers
8a7b9afaa3
Add frontend check for invalid attached files (#139) 2023-10-20 19:01:32 +02:00
Knut Ahlers
e3f790e92e
Fix: Clean error on component navigation
fixes #136

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-10-18 19:14:13 +02:00
Knut Ahlers
3651636a06
prepare release v1.9.2 2023-10-18 15:31:54 +02:00
Knut Ahlers
9322acfcb5
Fix: Remove path from filename if given
fixes #135

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-10-18 15:09:52 +02:00
Knut Ahlers
a83ae9ceb3
Add basic-auth / header addition to OTS-CLI
closes #134

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-10-18 15:09:44 +02:00
60 changed files with 4072 additions and 1371 deletions

28
.git_changerelease.yaml Normal file
View File

@ -0,0 +1,28 @@
---
# Template to format the commit message containing the changelog change
# which will be used to add the tag to.
release_commit_message: "Release: OTS {{.Version}}"
# Commands to run before committing the changelog and adding the tag.
# Therefore these can add content to be included into the release-
# commit. These commands have access to the `TAG_VERSION` variable
# which contains the tag to be applied after the commit. If the
# command specified here is prefixed with a `-` sign, the exit status
# will not fail the release process. If it is not prefixed with a `-`
# a non-zero exit status will terminate the release process. The
# commands will be run from the repostory root, so sub-dirs MUST be
# specified. All commands are run as `bash -ec "..."` so you can use
# bash inside the commands.
pre_commit_commands:
- |-
yq -iP "(select(.spec.template.spec | has(\"containers\")) | .spec.template.spec.containers[] | select(.name == \"ots\").image) = \"ghcr.io/luzifer/ots:v${TAG_VERSION}\"" docs/k8s_example.yml
git add docs/k8s_example.yml
- |-
yq -iP ".services.app.build.context = \"https://github.com/Luzifer/ots.git#v${TAG_VERSION}\"" docker-compose.yml
git add docker-compose.yml
- |-
sed -i -E "s@org.opencontainers.image.version='[^']*'@org.opencontainers.image.version='${TAG_VERSION}'@" Dockerfile Dockerfile.minimal
git add Dockerfile Dockerfile.minimal
...

View File

@ -30,6 +30,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2

51
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,51 @@
---
name: docker-publish
on:
push:
branches: ['master']
tags: ['v*']
permissions:
packages: write
jobs:
docker-publish:
defaults:
run:
shell: bash
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: actions/checkout@v4
with:
lfs: true
show-progress: false
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Build & Publish
id: taggen
run: bash ci/docker-gen-tagnames.sh
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.taggen.outputs.docker_build_tags }}
...

View File

@ -0,0 +1,44 @@
---
name: pull-request-ci
on:
pull_request_target:
paths: ["i18n.yaml"]
jobs:
generate-translations:
defaults:
run:
shell: bash
container:
image: luzifer/gh-arch-env
env:
CGO_ENABLED: 0
GOPATH: /go
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.head_ref }}
- name: Marking workdir safe
run: git config --global --add safe.directory /__w/ots/ots
- name: Re-Generate embedded translations file
working-directory: ./ci/translate
run: go run .
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_author: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'
commit_message: 'CI: Update embedded translations'
file_pattern: 'src/langs/langs.js'
...

View File

@ -17,7 +17,7 @@ jobs:
shell: bash
container:
image: luzifer/archlinux
image: ghcr.io/luzifer-docker/gh-arch-env
env:
CGO_ENABLED: 0
GOPATH: /go
@ -25,27 +25,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Enable custom AUR package repo
run: echo -e "[luzifer]\nSigLevel = Never\nServer = https://archrepo.hub.luzifer.io/\$arch" >>/etc/pacman.conf
- name: Install required packages
run: |
pacman -Syy --noconfirm \
awk \
curl \
diffutils \
git \
go \
golangci-lint-bin \
make \
nodejs-lts-hydrogen \
npm \
tar \
trivy \
unzip \
which \
zip
- uses: actions/checkout@v3
- name: Marking workdir safe

View File

@ -9,7 +9,9 @@ run:
modules-download-mode: readonly
output:
format: tab
formats:
- format: tab
path: stdout
issues:
# This disables the included exclude-list in golangci-lint as that
@ -29,11 +31,11 @@ linters:
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false]
- contextcheck # check the function whether use a non-inherited context [fast: false, auto-fix: false]
- copyloopvar # copyloopvar is a linter detects places where loop variables are copied [fast: true, auto-fix: false]
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
- exportloopref # checks for pointers to enclosing loop variables [fast: true, auto-fix: false]
- forbidigo # Forbids identifiers [fast: true, auto-fix: false]
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
@ -44,12 +46,12 @@ linters:
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
- gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
- gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
- gosec # Inspects source code for security problems [fast: true, auto-fix: false]
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
- mnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
@ -75,9 +77,7 @@ linters-settings:
min-complexity: 15
gomnd:
settings:
mnd:
ignored-functions: 'strconv.(?:Format|Parse)\B+'
ignored-functions: 'strconv.(?:Format|Parse)\B+'
revive:
rules:

View File

@ -1,4 +1,4 @@
FROM luzifer/archlinux as builder
FROM golang:1-alpine AS builder
ENV CGO_ENABLED=0 \
GOPATH=/go \
@ -8,12 +8,11 @@ COPY . /go/src/github.com/Luzifer/ots
WORKDIR /go/src/github.com/Luzifer/ots
RUN set -ex \
&& pacman --noconfirm -Syy \
&& apk --no-cache add \
curl \
git \
go \
make \
nodejs-lts-hydrogen \
nodejs-lts \
npm \
tar \
unzip \
@ -25,7 +24,12 @@ RUN set -ex \
FROM alpine:latest
LABEL maintainer "Knut Ahlers <knut@ahlers.me>"
LABEL org.opencontainers.image.authors='Knut Ahlers <knut@ahlers.me>' \
org.opencontainers.image.version='1.15.1' \
org.opencontainers.image.url='https://github.com/Luzifer/ots/pkgs/container/ots' \
org.opencontainers.image.documentation='https://github.com/Luzifer/ots/wiki' \
org.opencontainers.image.source='https://github.com/Luzifer/ots' \
org.opencontainers.image.licenses='Apache-2.0'
RUN set -ex \
&& apk --no-cache add \

View File

@ -1,4 +1,4 @@
FROM luzifer/archlinux as builder
FROM golang:1-alpine AS builder
ENV CGO_ENABLED=0 \
GOPATH=/go \
@ -8,12 +8,11 @@ COPY . /go/src/github.com/Luzifer/ots
WORKDIR /go/src/github.com/Luzifer/ots
RUN set -ex \
&& pacman --noconfirm -Syy \
&& apk --no-cache add \
curl \
git \
go \
make \
nodejs-lts-hydrogen \
nodejs-lts \
npm \
tar \
unzip \
@ -25,7 +24,12 @@ RUN set -ex \
FROM scratch
LABEL maintainer "Knut Ahlers <knut@ahlers.me>"
LABEL org.opencontainers.image.authors='Knut Ahlers <knut@ahlers.me>' \
org.opencontainers.image.version='1.15.1' \
org.opencontainers.image.url='https://github.com/Luzifer/ots/pkgs/container/ots' \
org.opencontainers.image.documentation='https://github.com/Luzifer/ots/wiki' \
org.opencontainers.image.source='https://github.com/Luzifer/ots' \
org.opencontainers.image.licenses='Apache-2.0'
COPY --from=builder /go/bin/ots /usr/local/bin/ots

View File

@ -1,3 +1,105 @@
# 1.15.1 / 2024-12-12
* Bugfixes
* Update Node dependencies
* Update Go dependencies
# 1.15.0 / 2024-12-05
* Improvements
* Add alternative `appIcon` for dark-mode (#204)
# 1.14.0 / 2024-11-21
* Improvements
* Add ability to paste files into textarea
* Add button to burn secrets immediately (#193)
* Add customization to add footer-links (#192)
* Add error message when subtle crypto is unavailable
* Add 'log-requests' option to disable request logging (#199) (Thanks @jimmypw)
* Add multi-platform image build
* Add periodic in-memory store pruner (#200) (Thanks @jimmypw)
* Add TLS configuration for server (#190) (Thanks @hixichen)
* Bugfixes
* Fix: Use no-cache to satisfy Trivy
* Translations
* Update Polish translation (#194, #201) (Thanks @Icikowski)
# 1.13.0 / 2024-08-27
* Bugfixes
* Update Node dependencies
* Update Go dependencies
* Lint: Resolve unused-parameter error
* Translations
* Add Italian translation (#173) (Thanks @ste93cry)
* Update Dutch translation (#168) (Thanks @mboeren & @sorcix)
* Restore old `nl` translation as `nl-BE`
* Update French translation (#167) (Thanks @toindev)
* Update Swedish translation (#171) (Thank @artingu)
# 1.12.0 / 2024-01-24
* Improvements
* [#159] Add version-command for ots-cli
* [#160] Add auto-resizing textareas
* [#160] Add hover tooltips for buttons
* [#160] Make success indicator more clear
* Use OCI Label defaults on Docker images (#145)
* Bugfixes
* Update dependencies
* Translations
* Update Polish translation (#166) (Thanks @Icikowski)
# 1.11.1 / 2023-12-12
* Bugfixes
* [#158] Disable Vue Devtools in release builds
# 1.11.0 / 2023-12-10
* Improvements
* [#148] Make secret optional when files are attached (#150)
* [#149] Make attachments stand out more (#152)
* [#154] Add debug logging for rejected attachment types & strip meta-info from mime-type (#155)
* [#154] Improve UX for rejected / allowed files
* Bugfixes
* [client] Fix wrong method when creating secrets
* Fix: Baked in version-string empty in build-local
* Update dependencies
* Translations
* Add tool to update translations in PRs
* Update Chinese translations (#151) (Thanks @YongJie-Xie)
# 1.10.0 / 2023-11-11
* New Features
* Add server side check for maximum secret size
* Implement metrics collection for API server (#143)
* Improvements
* Add frontend check for invalid attached files (#139)
* Implement attachment checking in CLI (#141)
* Bugfixes
* Fix: Clean error on component navigation
* [CI] Fix: npm@latest cannot run with Node 18
* Translations
* Update Polish translation (#140) (Thanks @Icikowski)
# 1.9.2 / 2023-10-18
* Add basic-auth / header addition to OTS-CLI
* Fix: Remove path from filename if given
# 1.9.1 / 2023-10-18
* Fix: Customize to disable powered by was ignored
@ -315,4 +417,4 @@ Many thanks to [@sorcix](https://github.com/sorcix) for the contributions to thi
# 0.1.0 / 2017-08-03
* Initial Version
* Initial Version

View File

@ -6,12 +6,12 @@ default: generate download_libs
build-local: download_libs generate-inner generate-apidocs
go build \
-buildmode=pie \
-ldflags "-s -w -X main.version=$(git describe --tags --always || echo dev)" \
-ldflags "-s -w -X main.version=$(shell git describe --tags --always || echo dev)" \
-mod=readonly \
-trimpath
generate:
docker run --rm -i -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:18-alpine \
docker run --rm -i -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:22-alpine \
sh -exc "apk add make && make generate-inner generate-apidocs && chown -R $(shell id -u) frontend node_modules"
generate-apidocs:
@ -19,9 +19,10 @@ generate-apidocs:
mv /tmp/api.html frontend/
generate-inner:
npx --yes npm@latest ci --include=dev
npm ci --include=dev
node ./ci/build.mjs
publish: export NODE_ENV=production
publish: download_libs generate-inner generate-apidocs
bash ./ci/build.sh

View File

@ -1,7 +1,6 @@
![](https://badges.fyi/github/license/Luzifer/ots)
![](https://badges.fyi/github/latest-release/Luzifer/ots)
![](https://badges.fyi/github/downloads/Luzifer/ots)
[![Go Report Card](https://goreportcard.com/badge/github.com/Luzifer/ots)](https://goreportcard.com/report/github.com/Luzifer/ots)
![](https://img.shields.io/github/license/Luzifer/ots)
![](https://img.shields.io/github/v/release/Luzifer/ots)
![](https://img.shields.io/github/downloads/Luzifer/ots/total)
# Luzifer / OTS
@ -64,6 +63,10 @@ Both commands can be used in scripts:
- `fetch` prints the secret to `STDOUT` and stores files to the given directory
- both sends logs to `STDERR` which you can disable (`--log-level=fatal`) or ignore in your script
In case your instance needs credentials to use the `/api/create` endpoint you can pass them to OTS-CLI like you would do with curl:
- `ots-cli create --instance ... -u myuser:mypass` for basic-auth
- `ots-cli create --instance ... -H 'Authorization: Token abcde'` for token-auth (you can set any header you need, just repeat `-H ...`)
### Bash: Sharing an encrypted secret (strongly recommended!)
This is slightly more complex as you first need to encrypt your secret before sending it to the API but in this case you can be sure the server will in no case be able to access the secret. Especially if you are using ots.fyi (my public hosted instance) you should not trust me with your secret but use an encrypted secret:
@ -87,6 +90,17 @@ You will now need to supply the web application with the password in addition to
In this case due to how browsers are handling hashes in URLs (the part after the `#`) the only URL the server gets to know is `https://ots.fyi/` which loads the frontend. Afterwards the Javascript executed in the browser fetches the encrypted secret at the given ID and decrypts it with the given password (in this case `mypass`). I will not be able to tell the content of your secret and just see the AES 256bit encrypted content.
## Local development
This repo contains a `Tilefile` to be used with [tilt v0.33+](https://tilt.dev/) to build and start the server for development.
Requirements:
- Go v1.23+
- Node v22+
- Tilt v0.33+
Just run `tilt up` and visit `http://localhost:15641/` for the development server.
## Localize to your own language
If you want to help translating the application to your own language please see the [`i18n.yaml`](https://github.com/Luzifer/ots/blob/master/i18n.yaml) file from this repository and translate the English strings inside. Afterwards please [open an issue](https://github.com/Luzifer/ots/issues/new) and attach your translation including the information which language you translated the strings into.

46
Tiltfile Normal file
View File

@ -0,0 +1,46 @@
# Install Node deps on change of package.json
local_resource(
'npm',
cmd='npm i',
deps=['package.json'],
)
# Rebuild frontend if source files change
local_resource(
'frontend',
cmd='node ./ci/build.mjs',
deps=['src'],
resource_deps=['npm'],
)
# Generate translation files on source change
local_resource(
'translations',
cmd='make translate',
deps=['i18n.yaml'],
)
# Rebuild and run Go webserver on code changes
local_resource(
'server',
cmd='go build .',
deps=[
'api.go',
'frontend',
'helpers.go',
'main.go',
'pkg',
'storage.go',
'tplFuncs.go',
],
ignore=['ots', 'src'],
serve_cmd='./ots --listen=:15641',
serve_env={
'CUSTOMIZE': 'customize.yaml',
},
readiness_probe=probe(
http_get=http_get_action(15641, path='/api/healthz'),
initial_delay_secs=1,
),
resource_deps=['frontend', 'translations'],
)

59
api.go
View File

@ -8,13 +8,24 @@ import (
"strings"
"time"
"github.com/Luzifer/ots/pkg/metrics"
"github.com/Luzifer/ots/pkg/storage"
"github.com/gofrs/uuid"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
const (
errorReasonInvalidJSON = "invalid_json"
errorReasonSecretMissing = "secret_missing"
errorReasonSecretSize = "secret_size"
errorReasonStorageError = "storage_error"
errorReasonSecretNotFound = "secret_not_found"
)
type apiServer struct {
store storage
collector *metrics.Collector
store storage.Storage
}
type apiResponse struct {
@ -29,9 +40,10 @@ type apiRequest struct {
Secret string `json:"secret"`
}
func newAPI(s storage) *apiServer {
func newAPI(s storage.Storage, c *metrics.Collector) *apiServer {
return &apiServer{
store: s,
collector: c,
store: s,
}
}
@ -39,9 +51,18 @@ func (a apiServer) Register(r *mux.Router) {
r.HandleFunc("/create", a.handleCreate)
r.HandleFunc("/get/{id}", a.handleRead)
r.HandleFunc("/isWritable", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
r.HandleFunc("/settings", a.handleSettings).Methods(http.MethodGet)
r.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })
}
func (a apiServer) handleCreate(res http.ResponseWriter, r *http.Request) {
if cust.MaxSecretSize > 0 {
// As a safeguard against HUGE payloads behind a misconfigured
// proxy we take double the maximum secret size after which we
// just close the read and cut the connection to the sender.
r.Body = http.MaxBytesReader(res, r.Body, cust.MaxSecretSize*2) //nolint:mnd
}
var (
expiry = cfg.SecretExpiry
secret string
@ -56,6 +77,15 @@ func (a apiServer) handleCreate(res http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
tmp := apiRequest{}
if err := json.NewDecoder(r.Body).Decode(&tmp); err != nil {
if _, ok := err.(*http.MaxBytesError); ok {
a.collector.CountSecretCreateError(errorReasonSecretSize)
// We don't do an error response here as the MaxBytesReader
// automatically cuts the ResponseWriter and we simply cannot
// answer them.
return
}
a.collector.CountSecretCreateError(errorReasonInvalidJSON)
a.errorResponse(res, http.StatusBadRequest, err, "decoding request body")
return
}
@ -65,12 +95,20 @@ func (a apiServer) handleCreate(res http.ResponseWriter, r *http.Request) {
}
if secret == "" {
a.collector.CountSecretCreateError(errorReasonSecretMissing)
a.errorResponse(res, http.StatusBadRequest, errors.New("secret missing"), "")
return
}
if cust.MaxSecretSize > 0 && len(secret) > int(cust.MaxSecretSize) {
a.collector.CountSecretCreateError(errorReasonSecretSize)
a.errorResponse(res, http.StatusBadRequest, errors.New("secret size exceeds maximum"), "")
return
}
id, err := a.store.Create(secret, time.Duration(expiry)*time.Second)
if err != nil {
a.collector.CountSecretCreateError(errorReasonStorageError)
a.errorResponse(res, http.StatusInternalServerError, err, "creating secret")
return
}
@ -80,6 +118,8 @@ func (a apiServer) handleCreate(res http.ResponseWriter, r *http.Request) {
expiresAt = func(v time.Time) *time.Time { return &v }(time.Now().UTC().Add(time.Duration(expiry) * time.Second))
}
a.collector.CountSecretCreated()
go updateStoredSecretsCount(a.store, a.collector)
a.jsonResponse(res, http.StatusCreated, apiResponse{
ExpiresAt: expiresAt,
Success: true,
@ -98,19 +138,28 @@ func (a apiServer) handleRead(res http.ResponseWriter, r *http.Request) {
secret, err := a.store.ReadAndDestroy(id)
if err != nil {
status := http.StatusInternalServerError
if err == errSecretNotFound {
if errors.Is(err, storage.ErrSecretNotFound) {
a.collector.CountSecretReadError(errorReasonSecretNotFound)
status = http.StatusNotFound
} else {
a.collector.CountSecretReadError(errorReasonStorageError)
}
a.errorResponse(res, status, err, "reading & destroying secret")
return
}
a.collector.CountSecretRead()
go updateStoredSecretsCount(a.store, a.collector)
a.jsonResponse(res, http.StatusOK, apiResponse{
Success: true,
Secret: secret,
})
}
func (a apiServer) handleSettings(w http.ResponseWriter, _ *http.Request) {
a.jsonResponse(w, http.StatusOK, cust)
}
func (a apiServer) errorResponse(res http.ResponseWriter, status int, err error, desc string) {
errID := uuid.Must(uuid.NewV4()).String()
@ -124,7 +173,7 @@ func (a apiServer) errorResponse(res http.ResponseWriter, status int, err error,
})
}
func (apiServer) jsonResponse(res http.ResponseWriter, status int, response apiResponse) {
func (apiServer) jsonResponse(res http.ResponseWriter, status int, response any) {
res.Header().Set("Content-Type", "application/json")
res.Header().Set("Cache-Control", "no-store, max-age=0")
res.WriteHeader(status)

32
ci/docker-gen-tagnames.sh Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
function log() {
echo "[$(date +%H:%M:%S)] $@" >&2
}
[[ -n ${GITHUB_REF_NAME:-} ]] || {
log "ERR: This script is intended to run on a Github Action only."
exit 1
}
repo="ghcr.io/${GITHUB_REPOSITORY,,}"
tags=()
case "${GITHUB_REF_TYPE}" in
branch)
# Generic build to develop: Workflow has to limit branches to master
tags+=("${repo}:develop")
;;
tag)
# Build to latest & tag: Older tags are not intended to rebuild
tags+=("${repo}:latest" "${repo}:${GITHUB_REF_NAME}")
;;
*)
log "ERR: The ref type ${GITHUB_REF_TYPE} is not handled."
exit 1
;;
esac
export IFS=,
echo "docker_build_tags=${tags[*]}" >>${GITHUB_OUTPUT}

65
ci/pr-regen-translations.sh Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
files=(
i18n.yaml
src/langs/langs.js
)
translation_branch=upd-translate
function log() {
echo "$@" >&2
}
PR_REMOTE_URL=${1:-}
[[ -n $PR_REMOTE_URL ]] || {
log "Usage: $(basename $0) <github branch URL i.e. https://github.com/Luzifer/ots/tree/translate-de>"
exit 1
}
remote="$(cut -d '/' -f 1-5 <<<"${PR_REMOTE_URL}").git"
branch=$(cut -d '/' -f 7 <<<"${PR_REMOTE_URL}")
git diff --exit-code >/dev/null || {
log "FATAL: Local changes detected, stopping now."
exit 1
}
switch_back_branch=$(git branch --show-current)
trap "git switch ${switch_back_branch}" EXIT
log "Updating branch '${branch}' of remote '${remote}'..."
log "+ Fetching remote..."
git fetch "${remote}" "${branch}"
log "+ Creating work-branch..."
if git branch | grep -q ${translation_branch}; then
git branch -D ${translation_branch}
fi
git branch ${translation_branch} FETCH_HEAD
log "+ Switching to work-branch..."
git switch ${translation_branch}
log "+ Updating translations..."
make translate
if git diff --exit-code "${files[@]}" >/dev/null; then
log "No changed introduced, stopping now."
fi
log "+ Committing changes..."
git add "${files[@]}"
git commit -m 'CI: Update embedded translations'
log "+ Please review these changes:"
git show
log "[Enter] to continue, [Ctrl+C] to cancel..."
read
log "+ Updating remote branch..."
git push ${remote} ${translation_branch}:${branch}
log "Updated remote PR, switching back to previous branch..."

View File

@ -1,29 +1,31 @@
module translate
go 1.21
go 1.23
toolchain go1.23.0
require (
github.com/Luzifer/go_helpers/v2 v2.20.0
github.com/Luzifer/rconfig/v2 v2.4.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/Luzifer/go_helpers/v2 v2.25.0
github.com/Luzifer/rconfig/v2 v2.5.2
github.com/Masterminds/sprig/v3 v3.3.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v3 v3.0.1
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.13.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/validator.v2 v2.0.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

View File

@ -1,118 +1,64 @@
github.com/Luzifer/go_helpers/v2 v2.20.0 h1:OyCUs7TFGwfJpGqD21KEKKOXy92jetw2l7dlmG7HZnA=
github.com/Luzifer/go_helpers/v2 v2.20.0/go.mod h1:KPGjImwm51SmOTZMd9XUsT241gHYJuEyLrS/omQ4/Dw=
github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o=
github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Luzifer/go_helpers/v2 v2.25.0 h1:k1J4gd1+BfuokTDoWgcgib9P5mdadjzKEgbtKSVe46k=
github.com/Luzifer/go_helpers/v2 v2.25.0/go.mod h1:KSVUdAJAav5cWGyB5oKGxmC27HrKULVTOxwPS/Kr+pc=
github.com/Luzifer/rconfig/v2 v2.5.2 h1:4Bfp8mTrCCK/xghUmUbh/qtKiLZA6RC0tHTgqkNw1m4=
github.com/Luzifer/rconfig/v2 v2.5.2/go.mod h1:HnqUWg+NQh60/neUqfMDDDo5d1v8UPuhwKR1HqM4VWQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/Masterminds/sprig/v3"
"github.com/mitchellh/hashstructure/v2"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
@ -71,6 +72,11 @@ func main() {
logrus.WithError(err).Fatal("loading translation file")
}
tfHash, err := hashstructure.Hash(tf, hashstructure.FormatV2, nil)
if err != nil {
logrus.WithError(err).Fatal("hashing source translations")
}
if cfg.AutoTranslate {
logrus.Info("auto-translating new strings...")
@ -87,10 +93,16 @@ func main() {
}
}
logrus.Info("saving translation file...")
tfHashNew, err := hashstructure.Hash(tf, hashstructure.FormatV2, nil)
if err != nil {
logrus.WithError(err).Fatal("hashing processed translations")
}
if err = saveTranslationFile(tf); err != nil {
logrus.WithError(err).Fatal("saving translation file")
if tfHash != tfHashNew {
logrus.Info("saving translation file...")
if err = saveTranslationFile(tf); err != nil {
logrus.WithError(err).Fatal("saving translation file")
}
}
logrus.Info("updating JS embedded translations...")
@ -276,7 +288,7 @@ func saveTranslationFile(tf translationFile) error {
}
encoder := yaml.NewEncoder(f)
encoder.SetIndent(2) //nolint:gomnd
encoder.SetIndent(2) //nolint:mnd
if err = encoder.Encode(tf); err != nil {
f.Close() //nolint:errcheck,gosec,revive // Short-lived fd-leak

View File

@ -11,8 +11,6 @@ import (
var langKeyFormat = regexp.MustCompile(`^[a-z]{2}(-[A-Z]{2})?$`)
func verify(tf translationFile) error {
var err error
if !langKeyFormat.MatchString(tf.Reference.LanguageKey) {
return errors.New("reference contains invalid languageKey")
}
@ -29,7 +27,7 @@ func verify(tf translationFile) error {
tf.Reference.FormalTranslations,
tf.Reference.Translations,
false,
); err != nil {
) {
return errors.New("reference contains error in formalTranslations")
}
}

View File

@ -1,17 +1,29 @@
package main
import (
"bytes"
"fmt"
"io"
"mime"
"net/http"
"os"
"path"
"strings"
"github.com/Luzifer/ots/pkg/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type (
authRoundTripper struct {
http.RoundTripper
headers http.Header
user, pass string
}
)
var createCmd = &cobra.Command{
Use: "create [-f file]... [--instance url] [--secret-from file]",
Short: "Create a new encrypted secret in the given OTS instance",
@ -23,40 +35,30 @@ var createCmd = &cobra.Command{
func init() {
createCmd.Flags().Duration("expire", 0, "When to expire the secret (0 to use server-default)")
createCmd.Flags().StringSliceP("header", "H", nil, "Headers to include in the request (i.e. 'Authorization: Token ...')")
createCmd.Flags().String("instance", "https://ots.fyi/", "Instance to create the secret with")
createCmd.Flags().StringSliceP("file", "f", nil, "File(s) to attach to the secret")
createCmd.Flags().Bool("no-text", false, "Disable secret read (create a secret with only files)")
createCmd.Flags().String("secret-from", "-", `File to read the secret content from ("-" for STDIN)`)
createCmd.Flags().StringP("user", "u", "", "Username / Password for basic auth, specified as 'user:pass'")
rootCmd.AddCommand(createCmd)
}
func createRunE(cmd *cobra.Command, _ []string) error {
func createRunE(cmd *cobra.Command, _ []string) (err error) {
cmd.SilenceUsage = true
var secret client.Secret
if client.HTTPClient, err = constructHTTPClient(cmd); err != nil {
return fmt.Errorf("constructing authorized HTTP client: %w", err)
}
// Read the secret content
logrus.Info("reading secret content...")
secretSourceName, err := cmd.Flags().GetString("secret-from")
if err != nil {
return fmt.Errorf("getting secret-from flag: %w", err)
if secret.Secret, err = getSecretContent(cmd); err != nil {
return fmt.Errorf("getting secret content: %w", err)
}
var secretSource io.Reader
if secretSourceName == "-" {
secretSource = os.Stdin
} else {
f, err := os.Open(secretSourceName) //#nosec:G304 // Opening user specified file is intended
if err != nil {
return fmt.Errorf("opening secret-from file: %w", err)
}
defer f.Close() //nolint:errcheck // The file will be force-closed by program exit
secretSource = f
}
secretContent, err := io.ReadAll(secretSource)
if err != nil {
return fmt.Errorf("reading secret content: %w", err)
}
secret.Secret = string(secretContent)
// Attach any file given
files, err := cmd.Flags().GetStringSlice("file")
if err != nil {
@ -70,13 +72,17 @@ func createRunE(cmd *cobra.Command, _ []string) error {
}
secret.Attachments = append(secret.Attachments, client.SecretAttachment{
Name: f,
Name: path.Base(f),
Type: mime.TypeByExtension(path.Ext(f)),
Content: content,
})
}
// Create the secret
if secret.Secret == "" && secret.Attachments == nil {
return fmt.Errorf("secret has no content and no attachments")
}
// Get flags for creation
logrus.Info("creating the secret...")
instanceURL, err := cmd.Flags().GetString("instance")
if err != nil {
@ -88,6 +94,12 @@ func createRunE(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("getting expire flag: %w", err)
}
// Execute sanity checks
if err = client.SanityCheck(instanceURL, secret); err != nil {
return fmt.Errorf("sanity checking secret: %w", err)
}
// Create the secret
secretURL, expiresAt, err := client.Create(instanceURL, secret, expire)
if err != nil {
return fmt.Errorf("creating secret: %w", err)
@ -103,3 +115,91 @@ func createRunE(cmd *cobra.Command, _ []string) error {
return nil
}
func constructHTTPClient(cmd *cobra.Command) (*http.Client, error) {
basic, _ := cmd.Flags().GetString("user")
headers, _ := cmd.Flags().GetStringSlice("header")
if basic == "" && headers == nil {
// No authorization needed
return http.DefaultClient, nil
}
t := authRoundTripper{RoundTripper: http.DefaultTransport, headers: http.Header{}}
// Set basic auth if available
user, pass, ok := strings.Cut(basic, ":")
if ok {
t.user = user
t.pass = pass
}
// Parse and set headers if available
for _, hdr := range headers {
key, value, ok := strings.Cut(hdr, ":")
if !ok {
logrus.WithField("header", hdr).Warn("invalid header format, skipping")
continue
}
t.headers.Add(key, strings.TrimSpace(value))
}
return &http.Client{Transport: t}, nil
}
func getSecretContent(cmd *cobra.Command) (string, error) {
secretSourceName, err := cmd.Flags().GetString("secret-from")
if err != nil {
return "", fmt.Errorf("getting secret-from flag: %w", err)
}
noSecret, err := cmd.Flags().GetBool("no-text")
if err != nil {
return "", fmt.Errorf("getting no-text flag: %w", err)
}
var secretSource io.Reader
switch {
case noSecret:
secretSource = bytes.NewReader(nil)
case secretSourceName == "-":
secretSource = os.Stdin
default:
f, err := os.Open(secretSourceName) //#nosec:G304 // Opening user specified file is intended
if err != nil {
return "", fmt.Errorf("opening secret-from file: %w", err)
}
defer f.Close() //nolint:errcheck // The file will be force-closed by program exit
secretSource = f
}
secretContent, err := io.ReadAll(secretSource)
if err != nil {
return "", fmt.Errorf("reading secret content: %w", err)
}
return strings.TrimSpace(string(secretContent)), nil
}
func (a authRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
if a.user != "" {
r.SetBasicAuth(a.user, a.pass)
}
for key, values := range a.headers {
if r.Header == nil {
r.Header = http.Header{}
}
for _, value := range values {
r.Header.Add(key, value)
}
}
resp, err := a.RoundTripper.RoundTrip(r)
if err != nil {
return nil, fmt.Errorf("executing round-trip: %w", err)
}
return resp, nil
}

View File

@ -16,7 +16,7 @@ import (
const storeFileMode = 0o600 // We assume the attached file to be a secret
var fetchCmd = &cobra.Command{
Use: "fetch url",
Use: "fetch <url>",
Short: "Retrieves a secret from the instance by its URL",
Long: "",
Args: cobra.ExactArgs(1),
@ -39,6 +39,8 @@ func checkDirWritable(dir string) error {
}
func fetchRunE(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
fileDir, err := cmd.Flags().GetString("file-dir")
if err != nil {
return fmt.Errorf("getting file-dir parameter: %w", err)
@ -70,7 +72,7 @@ func fetchRunE(cmd *cobra.Command, args []string) error {
func storeAttachment(dir string, f client.SecretAttachment) error {
// First lets find a free file name to save the file as
var (
fileNameFragments = strings.SplitN(f.Name, ".", 2) //nolint:gomnd
fileNameFragments = strings.SplitN(f.Name, ".", 2) //nolint:mnd
i int
storeName = path.Join(dir, f.Name)
storeNameTpl string

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"github.com/Luzifer/ots/pkg/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -28,5 +29,7 @@ func rootPersistentPreRunE(cmd *cobra.Command, _ []string) error {
}
logrus.SetLevel(ll)
client.Logger = logrus.NewEntry(logrus.StandardLogger())
return nil
}

View File

@ -0,0 +1,19 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Displays the tool version",
Run: func(*cobra.Command, []string) {
fmt.Printf("ots-cli %s\n", version) //nolint:forbidigo
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}

View File

@ -1,19 +1,26 @@
module github.com/Luzifer/ots/cmd/ots-cli
go 1.21.1
go 1.23
replace github.com/Luzifer/ots/pkg/client => ../../pkg/client
replace (
github.com/Luzifer/ots/pkg/client => ../../pkg/client
github.com/Luzifer/ots/pkg/customization => ../../pkg/customization
)
require (
github.com/Luzifer/ots/pkg/client v0.0.0-20231009165114-f1e303cbe5a7
github.com/Luzifer/ots/pkg/client v0.0.0-20241212093302-8fadf7205f51
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/cobra v1.8.1
)
require (
github.com/Luzifer/go-openssl/v4 v4.2.1 // indirect
github.com/Luzifer/go-openssl/v4 v4.2.4 // indirect
github.com/Luzifer/ots/pkg/customization v0.0.0-20241212093302-8fadf7205f51 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.13.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

View File

@ -1,30 +1,38 @@
github.com/Luzifer/go-openssl/v4 v4.2.1 h1:0+/gaQ5TcBhGmVqGrfyA21eujlbbaNwj0VlOA3nh4ts=
github.com/Luzifer/go-openssl/v4 v4.2.1/go.mod h1:CZZZWY0buCtkxrkqDPQYigC4Kn55UuO97TEoV+hwz2s=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/Luzifer/go-openssl/v4 v4.2.4 h1:3Eu3gSeZpr8Ha+IofVnSWttCL1xejRr/lda4l4TZRWk=
github.com/Luzifer/go-openssl/v4 v4.2.4/go.mod h1:ykquxaR0R1Vor83/FAtGBJZZO5zswuSQTVx1FQc1bJY=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,6 +1,10 @@
package main
import "os"
import (
"os"
)
var version = "dev"
func main() {
if err := rootCmd.Execute(); err != nil {

24
docker-compose.yml Normal file
View File

@ -0,0 +1,24 @@
version: "3.8"
services:
app:
build:
context: https://github.com/Luzifer/ots.git#v1.15.1
restart: always
environment:
# Optional, see "Customization" in README
#CUSTOMIZE: '/etc/ots/customize.yaml'
# See README for details
REDIS_URL: redis://redis:6379/0
# 168h = 1w
SECRET_EXPIRY: "604800"
# "mem" or "redis" (See README)
STORAGE_TYPE: redis
depends_on:
- redis
ports:
- 3000:3000
redis:
image: redis:alpine
restart: always
volumes:
- ./data:/data

View File

@ -1,51 +1,42 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: ots
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis
namespace: ots
name: ots-redis
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: ots
name: ots-redis
labels:
app: redis
app: ots-redis
role: leader
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: redis
app: ots-redis
template:
metadata:
labels:
app: redis
app: ots-redis
role: leader
tier: backend
spec:
volumes:
- name: redis-storage
persistentVolumeClaim:
claimName: redis
claimName: ots-redis
containers:
- name: leader
image: "docker.io/redis:6.2.5-alpine"
image: docker.io/redis:6.2.5-alpine
resources:
requests:
cpu: 100m
@ -53,7 +44,7 @@ spec:
ports:
- containerPort: 6379
volumeMounts:
- mountPath: "/data"
- mountPath: /data
name: redis-storage
livenessProbe:
tcpSocket:
@ -65,15 +56,13 @@ spec:
- redis-cli
- ping
initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: ots
name: ots-redis
labels:
app: redis
app: ots-redis
role: leader
tier: backend
spec:
@ -81,16 +70,25 @@ spec:
- port: 6379
targetPort: 6379
selector:
app: redis
app: ots-redis
role: leader
tier: backend
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ots-customize
data:
customize.yml: |
appTitle: "My very customized OTS"
disableQRSupport: true
maxAttachmentSizeTotal: 1048576
maxSecretSize: 2097152
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ots
namespace: ots
spec:
replicas: 2
selector:
@ -103,17 +101,28 @@ spec:
app: ots
tier: frontend
spec:
volumes:
- name: custom
configMap:
name: ots-cutomize
containers:
- name: ots
image: "luzifer/ots:v0.19.0"
args: ["--storage-type", "redis"]
image: ghcr.io/luzifer/ots:v1.15.1
args:
- --storage-type
- redis
- --customize
- /custom/customize.yml
env:
- name: REDIS_URL
value: "tcp://redis.ots.svc.cluster.local:6379"
value: tcp://ots-redis:6379
- name: REDIS_KEY
value: "ots"
value: ots
- name: SECRET_EXPIRY
value: "172800"
volumeMounts:
- mountPath: /custom
name: custom
resources:
requests:
cpu: 100m
@ -125,13 +134,11 @@ spec:
path: /
port: 3000
initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: ots
namespace: ots
labels:
app: ots
tier: frontend
@ -142,13 +149,11 @@ spec:
selector:
app: ots
tier: frontend
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ots
namespace: ots
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
@ -167,4 +172,4 @@ spec:
tls:
- hosts:
- ots.example.com
secretName: ingress-tls
secretName: ots-ingress-tls

48
go.mod
View File

@ -1,33 +1,47 @@
module github.com/Luzifer/ots
go 1.20
go 1.23
toolchain go1.23.0
replace github.com/Luzifer/ots/pkg/customization => ./pkg/customization
require (
github.com/Luzifer/go_helpers/v2 v2.20.0
github.com/Luzifer/rconfig/v2 v2.4.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/Luzifer/go_helpers/v2 v2.25.0
github.com/Luzifer/ots/pkg/customization v0.0.0-20241212093302-8fadf7205f51
github.com/Luzifer/rconfig/v2 v2.5.2
github.com/Masterminds/sprig/v3 v3.3.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gorilla/mux v1.8.0
github.com/gorilla/mux v1.8.1
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.2.1
github.com/prometheus/client_golang v1.20.5
github.com/redis/go-redis/v9 v9.7.0
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v2 v2.4.0
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.13.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/sys v0.30.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/validator.v2 v2.0.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

142
go.sum
View File

@ -1,114 +1,94 @@
github.com/Luzifer/go_helpers/v2 v2.20.0 h1:OyCUs7TFGwfJpGqD21KEKKOXy92jetw2l7dlmG7HZnA=
github.com/Luzifer/go_helpers/v2 v2.20.0/go.mod h1:KPGjImwm51SmOTZMd9XUsT241gHYJuEyLrS/omQ4/Dw=
github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o=
github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Luzifer/go_helpers/v2 v2.25.0 h1:k1J4gd1+BfuokTDoWgcgib9P5mdadjzKEgbtKSVe46k=
github.com/Luzifer/go_helpers/v2 v2.25.0/go.mod h1:KSVUdAJAav5cWGyB5oKGxmC27HrKULVTOxwPS/Kr+pc=
github.com/Luzifer/rconfig/v2 v2.5.2 h1:4Bfp8mTrCCK/xghUmUbh/qtKiLZA6RC0tHTgqkNw1m4=
github.com/Luzifer/rconfig/v2 v2.5.2/go.mod h1:HnqUWg+NQh60/neUqfMDDDo5d1v8UPuhwKR1HqM4VWQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

52
helpers.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"net"
"net/http"
"github.com/Luzifer/ots/pkg/metrics"
"github.com/Luzifer/ots/pkg/storage"
"github.com/sirupsen/logrus"
)
func requestInSubnetList(r *http.Request, subnets []string) bool {
if len(subnets) == 0 {
// No subnets specififed: None allowed (without doing the parsing)
return false
}
remote, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
logrus.WithError(err).Error("parsing remote address")
return false
}
remoteIP := net.ParseIP(remote)
if remoteIP == nil {
logrus.WithError(err).Error("parsing remote address")
return false
}
for _, sn := range subnets {
_, netw, err := net.ParseCIDR(sn)
if err != nil {
logrus.WithError(err).WithField("subnet", sn).Warn("invalid subnet specified")
continue
}
if netw.Contains(remoteIP) {
return true
}
}
return false
}
func updateStoredSecretsCount(store storage.Storage, collector *metrics.Collector) {
n, err := store.Count()
if err != nil {
logrus.WithError(err).Error("counting stored secrets")
return
}
collector.UpdateSecretsCount(n)
}

134
i18n.yaml
View File

@ -4,6 +4,7 @@ reference:
translators:
- Luzifer
translations:
alert-insecure-environment: You are accessing this instance using an insecure connection. You will not be able to create or read secrets.
alert-secret-not-found: This is not the secret you are looking for&hellip; - If you expected the secret to be here it might be compromised as someone else might have opened the link already.
alert-something-went-wrong: Something went wrong. I'm very sorry about this&hellip;
btn-create-secret: Create the secret!
@ -32,17 +33,22 @@ reference:
text-burn-hint: Please remember not to go to this URL yourself as that would destroy the secret. Just pass it to someone else!
text-burn-time: 'If not viewed before, this secret will automatically be deleted:'
text-hint-burned: <strong>Attention:</strong> You're only seeing this once. As soon as you reload the page the secret will be gone so maybe copy it now&hellip;
text-invalid-files-selected: At least one of the selected files is not allowed as an attachment.
text-max-filesize: 'Maximum size: {maxSize}'
text-max-filesize-exceeded: 'The file(s) you chose are too big to attach: {curSize} / {maxSize}'
text-powered-by: Powered by
text-pre-reveal-hint: To reveal the secret click this button but be aware doing so will destroy the secret. You can only view it once!
text-pre-url: 'Your secret was created and stored using this URL:'
text-secret-burned: The secret was successfully destroyed.
text-secret-create-disabled: The creation of new secrets is disabled in this instance.
title-explanation: This is how it works&hellip;
title-new-secret: Create a new secret
title-reading-secret: Reading your secret&hellip;
title-secret-create-disabled: Secret creation disabled…
title-secret-created: Secret created!
tooltip-burn-secret: Burn Secret now!
tooltip-copy-to-clipboard: Copy to Clipboard
tooltip-download-as-file: Download as File
translations:
ca:
translators:
@ -92,6 +98,7 @@ translations:
translators:
- Luzifer
translations:
alert-insecure-environment: Du besuchst diese Instanz über eine unsichere Verbindung. Du kannst deswegen keine Secrets erstellen oder lesen.
alert-secret-not-found: Das ist nicht das Secret, was du suchst&hellip; - Falls du diesen Link noch nicht selbst geöffnet hast, könnte das Secret kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.
alert-something-went-wrong: Irgendwas ging schief. Entschuldigung&hellip;
btn-create-secret: Secret erstellen!
@ -120,18 +127,24 @@ translations:
text-burn-hint: Bitte rufe die URL nicht selbst auf, da das Secret dadurch zerstört würde. Gib sie einfach weiter!
text-burn-time: 'Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:'
text-hint-burned: <strong>Achtung:</strong> Du kannst das nur einmal ansehen! Sobald du die Seite neu lädst, ist das Secret verschwunden, also besser direkt kopieren und sicher abspeichern&hellip;
text-invalid-files-selected: Mindestens eine der ausgewählten Dateien ist nicht als Anhang erlaubt.
text-max-filesize: 'Maximale Größe: {maxSize}'
text-max-filesize-exceeded: 'Die ausgewählten Dateien übersteigen die maximale Größe: {curSize} / {maxSize}'
text-powered-by: Läuft mit
text-pre-reveal-hint: Um das Secret anzuzeigen klicke diesen Button aber denk dran, dass das Secret nur einmal angezeigt und dabei gelöscht wird.
text-pre-url: 'Dein Secret wurde angelegt und unter folgender URL gespeichert:'
text-secret-burned: Das Secret wurde zerstört.
text-secret-create-disabled: Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.
title-explanation: So funktioniert es&hellip;
title-new-secret: Erstelle ein neues Secret
title-reading-secret: Secret auslesen&hellip;
title-secret-create-disabled: Erstellen von Secrets deaktiviert…
title-secret-created: Secret erstellt!
tooltip-burn-secret: Secret jetzt zerstören!
tooltip-copy-to-clipboard: In die Zwischenablage kopieren
tooltip-download-as-file: Als Datei herunterladen
formalTranslations:
alert-insecure-environment: Sie besuchen diese Instanz über eine unsichere Verbindung. Sie können deswegen keine Secrets erstellen oder lesen.
alert-secret-not-found: Dieses Secret existiert nicht. - Falls Sie diesen Link noch nicht selbst geöffnet haben, könnte der Inhalt kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.
btn-reveal-secret: Secret anzeigen
items-explanation:
@ -199,9 +212,12 @@ translations:
alert-secret-not-found: Ce secret n'est pas celui que vous cherchez&hellip; - Si vous comptiez trouvez ce secret ici, il a pu être compromis car quelqu'un a probablement déjà ouvert le lien.
alert-something-went-wrong: Un problème est survenu. Nous en sommes désolés&hellip;
btn-create-secret: Créer le secret!
btn-create-secret-processing: Secret en cours de création ...
btn-new-secret: Nouveau secret
btn-reveal-secret: Voir le secret!
btn-reveal-secret-processing: Secret en cours de déchiffrement ...
btn-show-explanation: Comment ça fonctionne?
expire-default: Expiration par défaut
expire-n-days: '{n} jour | {n} jours'
expire-n-hours: '{n} heure | {n} heures'
expire-n-minutes: '{n} minute | {n} minutes'
@ -212,11 +228,18 @@ translations:
- Seul le secret chiffré est envoyé au serveur (ni le secret en clair, ni le mot de passe ne sont envoyés!)
- Le serveur stocke le secret chiffré pendant un certain temps
- Vous fournissez l'URL affichée contenant l'identifiant et le mot de passe de déchiffrage au destinataire
- 'Le destintaire ne peut voir le secret qu''une fois: si cela ne fonctionne pas, c''est que le secret a été consulté par quelqu''un d''autre!'
- 'Le destinataire ne peut voir le secret qu''une fois: si cela ne fonctionne pas, c''est que le secret a été consulté par quelqu''un d''autre!'
- Dès que le secret chiffré a été récupéré, il est supprimé du serveur
label-expiry: 'Expiration dans:'
label-secret-data: 'Données secrètes:'
label-secret-files: 'Attacher des fichiers:'
text-attached-files: L'émetteur a attaché des fichiers au secret. Assurez-vous d'avoir confiance en l'émetteur, les fichiers n'ont pas été vérifiés !
text-burn-hint: Attention de ne pas ouvrir cette URL vous-même, cela détruirait le secret. Fournissez-la à quelqu'un d'autre!
text-burn-time: 'S''il n''a pas été vu auparavant, ce secret sera automatiquement supprimé :'
text-hint-burned: <strong>Attention:</strong> Vous ne pouvez consulter ce contenu qu'une fois. Le secret sera détruit dès que vous rechargez la page, donc copiez le maintenant&hellip;
text-invalid-files-selected: Au moins l'un des fichiers sélectionnés n'est pas autorisé comme pièce-jointe.
text-max-filesize: 'Taille maximum: {maxSize}'
text-max-filesize-exceeded: 'Le(s) fichier(s) que vous avez choisis sont trop volumineux pour être attachés : {curSize} / {maxSize}'
text-powered-by: Propulsé par
text-pre-reveal-hint: Pour afficher le secret, cliquez sur ce bouton, mais soyez conscient que cela le détruira. Vous ne pouvez l'afficher qu'une fois!
text-pre-url: 'Votre secret a été créé et stocké à cette URL:'
@ -226,6 +249,54 @@ translations:
title-reading-secret: Lecture du secret&hellip;
title-secret-create-disabled: Création secrète désactivée...
title-secret-created: Secret créé!
tooltip-copy-to-clipboard: Copier dans le presse-papiers
tooltip-download-as-file: Télécharger en tant que fichier
it:
deeplLanguage: it
translators: []
translations:
alert-secret-not-found: Questo non è il secret che stai cercando&hellip; - Se ti aspettavi di vedere il secret allora potrebbe essere stato compromesso poichè qualcun altro potrebbe aver già aperto il link.
alert-something-went-wrong: Qualcosa non ha funzionato. Mi dispiace davvero&hellip;
btn-create-secret: Crea il secret!
btn-create-secret-processing: Creazione del secret in corso…
btn-new-secret: Nuovo secret
btn-reveal-secret: Mostrami il secret!
btn-reveal-secret-processing: Decrittazione del secret in corso…
btn-show-explanation: Come funziona?
expire-default: Scadenza predefinita
expire-n-days: '{n} giorno | {n} giorni'
expire-n-hours: '{n} ora | {n} ore'
expire-n-minutes: '{n} minuto | {n} minuti'
expire-n-seconds: '{n} secondo | {n} secondi'
items-explanation:
- Inserisci un secret nel campo di testo di questa pagina
- Il tuo browser critta il secret usando una password generata in modo casuale
- Solo il secret crittato viene inviato al server (nè il secret decrittato nè la password vengono mai inviati!)
- Il server conserva il secret crittato per un certo periodo di tempo
- Passi l'URL visualizzato contenente l'ID e la password di decrittazione al destinatario
- 'Il destinatario può vedere il secret esattamente una sola volta: se non può, il secret potrebbe essere stato visto da qualcun altro!'
- Dopo che il secret crittato è stato visualizzato la prima volta, viene cancellato dal server
label-expiry: 'Scade in:'
label-secret-data: 'Dati del secret:'
label-secret-files: 'Allega files:'
text-attached-files: Il mittente ha allegato alcuni files al secret. Assicurati di fidarti del mittente perchè i files non sono stati controllati!
text-burn-hint: Per favore ricorda di non visitare questo URL perchè il secret verrebbe cancellato. Passalo semplicemente a qualcun altro!
text-burn-time: 'Se non viene visualizzato prima, questo secret verrà cancellato automaticamente:'
text-hint-burned: <strong>Attenzione:</strong> Vedrai il secret solo questa volta. Non appena ricaricherai la pagina verrà cancellato, quindi magari copialo ora&hellip;
text-invalid-files-selected: Almeno uno dei files selezionati non è consentito come allegato.
text-max-filesize: 'Dimensione massima: {maxSize}'
text-max-filesize-exceeded: 'Il/I file(s) che hai scelto ha/hanno una dimensione troppo grande per essere allegato/allegati: {curSize} / {maxSize}'
text-powered-by: Realizzato con
text-pre-reveal-hint: Per rivelare il secret clicca su questo pulsante, ma attenzione perchè farlo lo cancellerà. Lo puoi vedere solo una volta!
text-pre-url: 'Il tuo secret è stato creato e salvato usando questo URL:'
text-secret-create-disabled: La creazione di nuovi secrets è disabilitata su questa istanza.
title-explanation: Questo è come funziona&hellip;
title-new-secret: Crea un nuovo secret
title-reading-secret: Leggi il tuo secret&hellip;
title-secret-create-disabled: Creazione secret disabilitata…
title-secret-created: Secret creato!
tooltip-copy-to-clipboard: Copia nella clipboard
tooltip-download-as-file: Scarica come file
lv:
deeplLanguage: lv
translators: []
@ -263,6 +334,49 @@ translations:
nl:
deeplLanguage: nl
translators: []
translations:
alert-secret-not-found: De gegevens die je zocht bestaan niet (meer)&hellip; - Als je hier informatie verwachtte dan is de link mogelijk al door iemand anders bekeken!
alert-something-went-wrong: Er ging iets verkeerd, sorry&hellip;
btn-create-secret: Geheim aanmaken!
btn-create-secret-processing: Geheim wordt aangemaakt...
btn-new-secret: Nieuw
btn-reveal-secret: Toon mij het geheim!
btn-reveal-secret-processing: Geheim wordt ontsleuteld...
btn-show-explanation: Hoe werkt dit?
expire-default: Standaard vervaltermijn
expire-n-days: '{n} dag | {n} dagen'
expire-n-hours: '{n} uur | {n} uur'
expire-n-minutes: '{n} minuut | {n} minuten'
expire-n-seconds: '{n} seconde | {n} seconden'
items-explanation:
- Je vult vertrouwelijke informatie in op deze pagina.
- Je browser versleutelt de ingevulde tekst via een automatisch gegenereerd wachtwoord.
- Alleen de versleutelde data wordt naar de server gestuurd (de onversleutelde data of het wachtwoord worden nooit verstuurd!)
- De server slaat de versleutelde data voor een bepaalde periode op
- Je geeft de URL met ID en het gegenereerde wachtwoord aan de ontvanger.
- 'De ontvanger kan de vertrouwelijke informatie exact eenmaal bekijken: indien het niet lukt heeft mogelijk iemand anders de info gezien!'
- Nadat het versleutelde geheim eenmalig bekeken is, wordt deze van de server verwijderd
label-expiry: 'Verwijder na:'
label-secret-data: 'Vertrouwelijke info:'
label-secret-files: 'Bestanden toevoegen:'
text-attached-files: De afzender heeft bestanden toegevoegd. Deze zijn niet gecontroleerd, gebruik deze enkel als je de afzender vertrouwt!
text-burn-hint: Open de URL niet zelf, deze is slechts eenmalig te gebruiken. Geef de URL aan de ontvanger.
text-burn-time: 'Deze vertrouwelijke informatie wordt automatisch gewist indien niet bekeken voor:'
text-hint-burned: <strong>Opgelet:</strong> Je ziet deze informatie alleen nu. Je kan het niet meer opnieuw opvragen als je de pagina verlaat.
text-max-filesize: 'Maximum grootte: {maxSize}'
text-max-filesize-exceeded: 'De bestanden die je toevoegde zijn te groot: {curSize} / {maxSize}'
text-powered-by: Mogelijk gemaakt door
text-pre-reveal-hint: 'Gebruik deze knop om het geheim weer te geven. Let op: Je kan dit slechts eenmaal doen!'
text-pre-url: 'Het geheim kan opgevraagd worden via deze URL:'
text-secret-create-disabled: Het aanmaken van nieuwe geheimen is in deze omgeving uitgeschakeld.
title-explanation: Dit is hoe het werkt&hellip;
title-new-secret: Nieuw geheim aanmaken
title-reading-secret: Geheim wordt gelezen&hellip;
title-secret-create-disabled: Aanmaken geheimen uitgeschakeld...
title-secret-created: Geheim aangemaakt!
nl-BE:
deeplLanguage: nl-BE
translators: []
translations:
alert-secret-not-found: De gegevens die je zocht bestaan niet (meer)&hellip; - Als je hier informatie verwachtte dan is de link mogelijk al door iemand anders bekeken!
alert-something-went-wrong: Er ging iets verkeerd, sorry&hellip;
@ -308,6 +422,7 @@ translations:
translators:
- Icikowski
translations:
alert-insecure-environment: Odwiedzasz tę instancję przez niezabezpieczone połączenie. Nie będziesz mógł tworzyć ani odczytywać sekretów.
alert-secret-not-found: To nie jest sekret, którego szukasz&hellip; - Jeśli spodziewałeś się tu sekretu, to może być on zagrożony, ponieważ ktoś inny mógł już otworzyć ten link.
alert-something-went-wrong: Coś poszło nie tak. Bardzo mi przykro&hellip;
btn-create-secret: Stwórz sekret!
@ -332,21 +447,26 @@ translations:
label-expiry: 'Wygasa po:'
label-secret-data: 'Dane sekretu:'
label-secret-files: 'Dołącz pliki:'
text-attached-files: Nadawca dołączył pliki do tego skeretu. Upewnij się, że ufasz nadawcy, bo pliki nie są sprawdzane!
text-attached-files: Nadawca dołączył pliki do tego sekretu. Upewnij się, że ufasz nadawcy, bo pliki nie są sprawdzane!
text-burn-hint: Pamiętaj, aby nie przechodzić na ten adres URL samemu, ponieważ to zniszczy sekret. Po prostu przekaż go odbiorcy!
text-burn-time: 'Jeżeli nie zostanie wyświetlony, ten sekret zostanie automatycznie usunięty:'
text-hint-burned: <strong>Uwaga:</strong> Zobaczysz to tylko raz. Gdy odświeżysz stronę, to sekret nie będzie już dostępny, więc lepiej skopiuj go teraz&hellip;
text-invalid-files-selected: Co najmniej jeden z załączonych plików nie jest dopuszczalny jako załącznik.
text-max-filesize: 'Maksymalny rozmiar: {maxSize}'
text-max-filesize-exceeded: 'Wybrane załączniki przekraczają makrymalny rozmiar: {curSize} / {maxSize}'
text-max-filesize-exceeded: 'Wybrane załączniki przekraczają maksymalny rozmiar: {curSize} / {maxSize}'
text-powered-by: Obsługiwane przez
text-pre-reveal-hint: Aby odsłonić sekret, naciśnij ten przycisk, jednak wiedz, że to zniszczy sekret. Możesz go zobaczyć tylko raz!
text-pre-url: 'Twój sekret został stworzony i zachowany pod tym adresem URL:'
text-secret-burned: Sekret został pomyślnie zniszczony.
text-secret-create-disabled: Tworzenie nowych sekretów jest wyłączone na tej instancji.
title-explanation: Oto, jak to działa&hellip;
title-new-secret: Stwórz nowy sekret
title-reading-secret: Odczytywanie Twojego sekretu&hellip;
title-secret-create-disabled: Tworzenie sekretów wyłączone&hellip;
title-secret-created: Sekret utworzony!
tooltip-burn-secret: Zniszcz sekret teraz!
tooltip-copy-to-clipboard: Skopiuj do schowka
tooltip-download-as-file: Pobierz jako plik
pt-BR:
deeplLanguage: pt-BR
translators: []
@ -427,11 +547,12 @@ translations:
title-secret-created: Секрет создан!
sv:
deeplLanguage: sv
translators: []
translators:
- artingu
translations:
alert-secret-not-found: Hemlighet hittades inte&hellip; - Om du förväntade dig att hemligheten skulle finnas här kan den vara röjd då någon annan kan ha öppnat denna länk tidigare.
alert-something-went-wrong: Något gick fel. Jag ber om ursäkt för detta!&hellip;
btn-create-secret: Skapa hemliget!
btn-create-secret: Skapa hemlighet!
btn-create-secret-processing: Hemlighet håller på att skapas..
btn-new-secret: Ny hemlighet.
btn-reveal-secret: Visa mig hemligheten!
@ -466,7 +587,7 @@ translations:
title-explanation: Såhär fungerar det&hellip;
title-new-secret: Skapa ny hemlighet
title-reading-secret: Läs din hemlighet&hellip;
title-secret-create-disabled: Hemlig skapelse avaktiverad...
title-secret-create-disabled: Skapande av hemlighet avaktiverat...
title-secret-created: Hemlighet skapad!
tr:
deeplLanguage: tr
@ -577,6 +698,7 @@ translations:
text-burn-hint: 请您记住不要自己访问此链接,因为这会导致机密被销毁。请原封不动的转发给收件人吧!
text-burn-time: 如果机密没有被查看,将会在此时间自动销毁:
text-hint-burned: <strong>注意:</strong>您只能查看一次机密,请尽快拷贝!刷新页面也会导致机密消失!
text-invalid-files-selected: 选中了至少一个不允许被作为附件的文件。
text-max-filesize: 附加文件大小上限:{maxSize}
text-max-filesize-exceeded: 您选择的文件过大,无法附加:{curSize} / {maxSize}
text-powered-by: Powered by

74
main.go
View File

@ -18,6 +18,8 @@ import (
file_helpers "github.com/Luzifer/go_helpers/v2/file"
http_helpers "github.com/Luzifer/go_helpers/v2/http"
"github.com/Luzifer/ots/pkg/customization"
"github.com/Luzifer/ots/pkg/metrics"
"github.com/Luzifer/rconfig/v2"
)
@ -27,14 +29,18 @@ var (
cfg struct {
Customize string `flag:"customize" default:"" description:"Customize-File to load"`
Listen string `flag:"listen" default:":3000" description:"IP/Port to listen on"`
LogRequests bool `flag:"log-requests" default:"true" description:"Enable request logging"`
LogLevel string `flag:"log-level" default:"info" description:"Set log level (debug, info, warning, error)"`
SecretExpiry int64 `flag:"secret-expiry" default:"0" description:"Maximum expiry of the stored secrets in seconds"`
StorageType string `flag:"storage-type" default:"mem" description:"Storage to use for putting secrets to" validate:"nonzero"`
VersionAndExit bool `flag:"version" default:"false" description:"Print version information and exit"`
EnableTLS bool `flag:"enable-tls" default:"false" description:"Enable HTTPS/TLS"`
CertFile string `flag:"cert-file" default:"" description:"Path to the TLS certificate file"`
KeyFile string `flag:"key-file" default:"" description:"Path to the TLS private key file"`
}
assets file_helpers.FSStack
cust customize
cust customization.Customize
indexTpl *template.Template
version = "dev"
@ -70,7 +76,7 @@ func initApp() error {
}
logrus.SetLevel(l)
if cust, err = loadCustomize(cfg.Customize); err != nil {
if cust, err = customization.Load(cfg.Customize); err != nil {
return errors.Wrap(err, "loading customizations")
}
@ -98,6 +104,9 @@ func main() {
os.Exit(0)
}
// Initialize metrics collector
collector := metrics.New()
// Initialize index template in order not to parse it multiple times
source, err := assets.ReadFile("index.html")
if err != nil {
@ -110,29 +119,64 @@ func main() {
if err != nil {
logrus.WithError(err).Fatal("initializing storage")
}
api := newAPI(store)
api := newAPI(store, collector)
// Initialize server
r := mux.NewRouter()
r.Use(http_helpers.GzipHandler)
api.Register(r.PathPrefix("/api").Subrouter())
r.HandleFunc("/", handleIndex)
r.PathPrefix("/").HandlerFunc(assetDelivery)
r.Handle("/metrics", metrics.Handler()).
Methods(http.MethodGet).
MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
return requestInSubnetList(r, cust.MetricsAllowedSubnets)
})
r.HandleFunc("/", handleIndex).
Methods(http.MethodGet)
r.PathPrefix("/").HandlerFunc(assetDelivery).
Methods(http.MethodGet)
var hdl http.Handler = r
hdl = http_helpers.GzipHandler(hdl)
if cfg.LogRequests {
hdl = http_helpers.NewHTTPLogHandlerWithLogger(hdl, logrus.StandardLogger())
}
server := &http.Server{
Addr: cfg.Listen,
Handler: hdl,
ReadHeaderTimeout: time.Second,
}
// Start periodic stored metrics update (required for multi-instance
// OTS hosting as other instances will create / delete secrets and
// we need to keep up with that)
go func() {
for t := time.NewTicker(time.Minute); ; <-t.C {
updateStoredSecretsCount(store, collector)
}
}()
// Start server
logrus.WithFields(logrus.Fields{
"secret_expiry": time.Duration(cfg.SecretExpiry) * time.Second,
"version": version,
}).Info("ots started")
server := &http.Server{
Addr: cfg.Listen,
Handler: http_helpers.NewHTTPLogHandlerWithLogger(r, logrus.StandardLogger()),
ReadHeaderTimeout: time.Second,
}
if err = server.ListenAndServe(); err != nil {
logrus.WithError(err).Fatal("HTTP server quit unexpectedly")
if cfg.EnableTLS {
if cfg.CertFile == "" || cfg.KeyFile == "" {
logrus.Fatal("TLS is enabled but cert-file or key-file is not provided")
}
logrus.Infof("Starting HTTPS server on %s", cfg.Listen)
if err := server.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile); err != nil {
logrus.WithError(err).Fatal("HTTPS server quit unexpectedly")
}
} else {
logrus.Infof("Starting HTTP server on %s", cfg.Listen)
if err := server.ListenAndServe(); err != nil {
logrus.WithError(err).Fatal("HTTP server quit unexpectedly")
}
}
}
@ -182,7 +226,7 @@ func handleIndex(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
if err := indexTpl.Execute(w, struct {
Customize customize
Customize customization.Customize
InlineContentNonce string
MaxSecretExpiry int64
Version string

2743
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,20 @@
{
"devDependencies": {
"@babel/eslint-parser": "^7.22.5",
"esbuild": "^0.17.17",
"esbuild-sass-plugin": "^2.9.0",
"@babel/eslint-parser": "^7.26.8",
"esbuild": "^0.25.0",
"esbuild-sass-plugin": "^3.3.1",
"esbuild-vue": "^1.2.2",
"eslint": "^8.42.0",
"eslint-plugin-vue": "^9.14.1",
"vue-template-compiler": "^2.7.14"
"eslint": "^9.20.1",
"eslint-plugin-vue": "^9.32.0",
"vue-template-compiler": "^2.7.16"
},
"name": "ots",
"private": true,
"dependencies": {
"base64-js": "^1.5.1",
"bootstrap": "^5.3.2",
"qrcode": "^1.5.3",
"vue": "^2.7.14",
"bootstrap": "^5.3.3",
"qrcode": "^1.5.4",
"vue": "^2.7.16",
"vue-i18n": "^8.28.2",
"vue-router": "^3.6.5"
}

View File

@ -17,11 +17,21 @@ import (
"time"
"github.com/Luzifer/go-openssl/v4"
"github.com/sirupsen/logrus"
)
type (
// HTTPClientIntf describes a minimal interface to be fulfilled
// by the given HTTP client. This can be used for mocking and to
// pass in authenticated clients
HTTPClientIntf interface {
Do(*http.Request) (*http.Response, error)
}
)
// HTTPClient defines the client to use for create and fetch requests
// and can be overwritten to provide authentication
var HTTPClient = http.DefaultClient
var HTTPClient HTTPClientIntf = http.DefaultClient
// KeyDerivationFunc defines the key derivation algorithm used in OTS
// to derive the key / iv from the password for encryption. You only
@ -30,7 +40,11 @@ var HTTPClient = http.DefaultClient
//
// The corresponding settings are found in `/src/crypto.js` in the OTS
// source code.
var KeyDerivationFunc = openssl.NewPBKDF2Generator(sha512.New, 300000) //nolint:gomnd // that's the definition
var KeyDerivationFunc = openssl.NewPBKDF2Generator(sha512.New, 300000) //nolint:mnd // that's the definition
// Logger can be set to enable logging from the library. By default
// all log-messages will be discarded.
var Logger *logrus.Entry
// PasswordLength defines the length of the generated encryption password
var PasswordLength = 20
@ -45,6 +59,12 @@ var RequestTimeout = 5 * time.Second
// provide an URL to useful information about your tool.
var UserAgent = "ots-client/1.x +https://github.com/Luzifer/ots"
func init() {
l := logrus.New()
l.SetOutput(io.Discard)
Logger = logrus.NewEntry(l)
}
// Create serializes the secret and creates a new secret on the
// instance given by its URL.
//
@ -86,7 +106,7 @@ func Create(instanceURL string, secret Secret, expireIn time.Duration) (string,
}.Encode()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, createURL.String(), body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, createURL.String(), body)
if err != nil {
return "", time.Time{}, fmt.Errorf("creating request: %w", err)
}
@ -138,7 +158,7 @@ func Fetch(secretURL string) (s Secret, err error) {
if err != nil {
return s, fmt.Errorf("unescaping fragment: %w", err)
}
fragmentParts := strings.SplitN(fragment, "|", 2) //nolint:gomnd
fragmentParts := strings.SplitN(fragment, "|", 2) //nolint:mnd
fetchURL := u.JoinPath(strings.Join([]string{".", "api", "get", fragmentParts[0]}, "/")).String()
ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)

View File

@ -1,15 +1,23 @@
module github.com/Luzifer/ots/pkg/client
go 1.21.1
go 1.23
replace github.com/Luzifer/ots/pkg/customization => ../customization
require (
github.com/Luzifer/go-openssl/v4 v4.2.1
github.com/Luzifer/go-openssl/v4 v4.2.4
github.com/Luzifer/ots/pkg/customization v0.0.0-20241212093302-8fadf7205f51
github.com/ryanuber/go-glob v1.0.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,14 +1,29 @@
github.com/Luzifer/go-openssl/v4 v4.2.1 h1:0+/gaQ5TcBhGmVqGrfyA21eujlbbaNwj0VlOA3nh4ts=
github.com/Luzifer/go-openssl/v4 v4.2.1/go.mod h1:CZZZWY0buCtkxrkqDPQYigC4Kn55UuO97TEoV+hwz2s=
github.com/Luzifer/go-openssl/v4 v4.2.4 h1:3Eu3gSeZpr8Ha+IofVnSWttCL1xejRr/lda4l4TZRWk=
github.com/Luzifer/go-openssl/v4 v4.2.4/go.mod h1:ykquxaR0R1Vor83/FAtGBJZZO5zswuSQTVx1FQc1bJY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

142
pkg/client/sanity.go Normal file
View File

@ -0,0 +1,142 @@
package client
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/Luzifer/ots/pkg/customization"
"github.com/ryanuber/go-glob"
)
var (
// ErrAttachmentsDisabled signalizes the instance has attachments
// disabled but the checked secret contains attachments
ErrAttachmentsDisabled = errors.New("attachments are disabled on this instance")
// ErrAttachmentsTooLarge signalizes the size of the attached files
// exceeds the configured maximum size of the given instance
ErrAttachmentsTooLarge = errors.New("attachment size exceeds allowed size")
// ErrAttachmentTypeNotAllowed signalizes any file does not match
// the allowed extensions / mime types
ErrAttachmentTypeNotAllowed = errors.New("attachment type is not allowed")
errSettingsNotFound = errors.New("settings not found")
mimeRegex = regexp.MustCompile(`^(?:[a-z]+|\*)\/(?:[a-zA-Z0-9.+_-]+|\*)$`)
)
// SanityCheck fetches the instance settings and validates the secret
// against those settings (matching file size, disabled attachments,
// allowed file types, ...)
func SanityCheck(instanceURL string, secret Secret) error {
cust, err := loadSettings(instanceURL)
if err != nil {
if errors.Is(err, errSettingsNotFound) {
// Sanity check is not possible when the API endpoint is not
// supported, therefore we ignore this.
return nil
}
return fmt.Errorf("fetching settings: %w", err)
}
// Check whether attachments are allowed at all
if cust.DisableFileAttachment && len(secret.Attachments) > 0 {
return ErrAttachmentsDisabled
}
// Check whether attachments are too large
var totalAttachmentSize int64
for _, a := range secret.Attachments {
totalAttachmentSize += int64(len(a.Content))
}
if cust.MaxAttachmentSizeTotal > 0 && totalAttachmentSize > cust.MaxAttachmentSizeTotal {
return ErrAttachmentsTooLarge
}
// Check for allowed types
if cust.AcceptedFileTypes != "" {
allowed := strings.Split(cust.AcceptedFileTypes, ",")
for _, a := range secret.Attachments {
if !attachmentAllowed(a, allowed) {
return ErrAttachmentTypeNotAllowed
}
}
}
return nil
}
func attachmentAllowed(file SecretAttachment, allowed []string) bool {
mimeType, _, _ := strings.Cut(file.Type, ";")
logger := Logger.WithField("content-type", mimeType)
for _, a := range allowed {
switch {
case mimeRegex.MatchString(a):
// That's a mime type
if glob.Glob(a, mimeType) {
// The mime "glob" matches the file type
logger.WithField("allowed_by", a).Debug("attachment allowed")
return true
}
case a[0] == '.':
// That's a file extension
if strings.HasSuffix(file.Name, a) {
// The filename has the right extension
logger.WithField("allowed_by", a).Debug("attachment allowed")
return true
}
}
}
logger.Debug("attachment type not allowed")
return false
}
func loadSettings(instanceURL string) (c customization.Customize, err error) {
u, err := url.Parse(instanceURL)
if err != nil {
return c, fmt.Errorf("parsing instance URL: %w", err)
}
createURL := u.JoinPath(strings.Join([]string{".", "api", "settings"}, "/"))
ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, createURL.String(), nil)
if err != nil {
return c, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", UserAgent)
resp, err := HTTPClient.Do(req)
if err != nil {
return c, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close() //nolint:errcheck // possible leaked-fd, lib should not log, potential short-lived leak
if resp.StatusCode == http.StatusNotFound {
return c, errSettingsNotFound
}
if resp.StatusCode != http.StatusOK {
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return c, fmt.Errorf("unexpected HTTP status %d", resp.StatusCode)
}
return c, fmt.Errorf("unexpected HTTP status %d (%s)", resp.StatusCode, respBody)
}
if err = json.NewDecoder(resp.Body).Decode(&c); err != nil {
return c, fmt.Errorf("decoding response: %w", err)
}
return c, nil
}

100
pkg/client/sanity_test.go Normal file
View File

@ -0,0 +1,100 @@
package client
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/Luzifer/ots/pkg/customization"
"github.com/stretchr/testify/assert"
)
type custMockClient struct {
Response *customization.Customize
}
func (c custMockClient) Do(r *http.Request) (*http.Response, error) {
m := http.NewServeMux()
m.HandleFunc(r.URL.Path, func(w http.ResponseWriter, _ *http.Request) {
if c.Response == nil {
w.WriteHeader(http.StatusNotFound)
return
}
d, _ := c.Response.ToJSON()
_, _ = w.Write([]byte(d))
})
w := httptest.NewRecorder()
m.ServeHTTP(w, r)
return w.Result(), nil
}
func TestSanityCheck(t *testing.T) {
var (
err error
m = custMockClient{&customization.Customize{
AcceptedFileTypes: "text/*,image/png,.gif",
DisableFileAttachment: true,
MaxAttachmentSizeTotal: 64,
}}
u = "http://localhost/"
)
HTTPClient = &m
defer func() { HTTPClient = http.DefaultClient }()
s := Secret{Secret: "ohai"}
// no attachments & attachments disabled
err = SanityCheck(u, s)
assert.NoError(t, err)
// attachments & attachmetns disabled
s.Attachments = []SecretAttachment{
{Name: "myfile.webm", Type: "video/webm", Content: []byte{0x0}},
}
err = SanityCheck(u, s)
assert.ErrorIs(t, err, ErrAttachmentsDisabled)
// disallowed attachment
m.Response.DisableFileAttachment = false
err = SanityCheck(u, s)
assert.ErrorIs(t, err, ErrAttachmentTypeNotAllowed)
// attachment allowed by extension
s.Attachments = []SecretAttachment{
{Name: "doesthiswork.gif", Type: "image/gif", Content: []byte{0x0}},
}
err = SanityCheck(u, s)
assert.NoError(t, err)
// attachment allowed by mime type
s.Attachments = []SecretAttachment{
{Name: "doesthiswork.png", Type: "image/png", Content: []byte{0x0}},
}
err = SanityCheck(u, s)
assert.NoError(t, err)
// attachment allowed by mime type wildcard
s.Attachments = []SecretAttachment{
{Name: "doesthiswork.md", Type: "text/markdown", Content: []byte{0x0}},
}
err = SanityCheck(u, s)
assert.NoError(t, err)
// attachment too large
s.Attachments = []SecretAttachment{
{Name: "doesthiswork.md", Type: "text/markdown", Content: bytes.Repeat([]byte{0x0}, 128)},
}
err = SanityCheck(u, s)
assert.ErrorIs(t, err, ErrAttachmentsTooLarge)
// check without settings API on instance
m.Response = nil
err = SanityCheck(u, s)
assert.NoError(t, err)
}

View File

@ -1,4 +1,6 @@
package main
// Package customization contains the structure for the customization
// file to configure the OTS web- and command-line interface
package customization
import (
"encoding/json"
@ -10,9 +12,18 @@ import (
"gopkg.in/yaml.v2"
)
// Frontend has a max attachment size of 64MiB as the base64 encoding
// will break afterwards. Therefore we use a maximum secret size of
// 65MiB and increase it by double base64 encoding:
//
// 65 MiB * 16/9 (twice 4/3 base64 size increase)
const defaultMaxSecretSize = 65 * 1024 * 1024 * (16 / 9) // = 115.6MiB
type (
customize struct {
// Customize holds the structure of the customization file
Customize struct {
AppIcon string `json:"appIcon,omitempty" yaml:"appIcon"`
AppIconDark string `json:"appIconDark,omitempty" yaml:"appIconDark"`
AppTitle string `json:"appTitle,omitempty" yaml:"appTitle"`
DisableAppTitle bool `json:"disableAppTitle,omitempty" yaml:"disableAppTitle"`
DisablePoweredBy bool `json:"disablePoweredBy,omitempty" yaml:"disablePoweredBy"`
@ -26,12 +37,22 @@ type (
DisableFileAttachment bool `json:"disableFileAttachment" yaml:"disableFileAttachment"`
MaxAttachmentSizeTotal int64 `json:"maxAttachmentSizeTotal" yaml:"maxAttachmentSizeTotal"`
OverlayFSPath string `json:"-" yaml:"overlayFSPath"`
UseFormalLanguage bool `json:"-" yaml:"useFormalLanguage"`
MaxSecretSize int64 `json:"-" yaml:"maxSecretSize"`
MetricsAllowedSubnets []string `json:"-" yaml:"metricsAllowedSubnets"`
OverlayFSPath string `json:"-" yaml:"overlayFSPath"`
UseFormalLanguage bool `json:"-" yaml:"useFormalLanguage"`
FooterLinks []FooterLink `json:"footerLinks,omitempty" yaml:"footerLinks"`
}
FooterLink struct {
Name string `json:"name" yaml:"name"`
URL string `json:"url" yaml:"url"`
}
)
func loadCustomize(filename string) (cust customize, err error) {
// Load retrieves the Customization file from filesystem
func Load(filename string) (cust Customize, err error) {
if filename == "" {
// None given, take a shortcut
cust.applyFixes()
@ -61,13 +82,19 @@ func loadCustomize(filename string) (cust customize, err error) {
return cust, nil
}
func (c customize) ToJSON() (string, error) {
// ToJSON is a templating helper which returns the customization
// serialized as JSON in a string
func (c Customize) ToJSON() (string, error) {
j, err := json.Marshal(c)
return string(j), errors.Wrap(err, "marshalling JSON")
}
func (c *customize) applyFixes() {
func (c *Customize) applyFixes() {
if len(c.AppTitle) == 0 {
c.AppTitle = "OTS - One Time Secrets"
}
if c.MaxSecretSize == 0 {
c.MaxSecretSize = defaultMaxSecretSize
}
}

11
pkg/customization/go.mod Normal file
View File

@ -0,0 +1,11 @@
module github.com/Luzifer/ots/pkg/customization
go 1.23
require (
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v2 v2.4.0
)
require golang.org/x/sys v0.30.0 // indirect

21
pkg/customization/go.sum Normal file
View File

@ -0,0 +1,21 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

99
pkg/metrics/metrics.go Normal file
View File

@ -0,0 +1,99 @@
// Package metrics provides an abstraction around metrics collection
// in order to bundle all metrics related calls in one location
package metrics
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
metricSecretsCreated = "secrets_created"
metricSecretsRead = "secrets_read"
metricSecretsCreateErrors = "secrets_create_errors"
meticsSecretsReadErrors = "secrets_read_errors"
metricsSecretsStored = "secrets_stored"
labelReason = "reason"
namespace = "ots"
)
type (
// Collector contains all required methods to collect metrics
// and to populate them into the Handler
Collector struct {
secretsCreated prometheus.Counter
secretsRead prometheus.Counter
secretsCreateErrors *prometheus.CounterVec
secretsReadErrors *prometheus.CounterVec
secretsStored prometheus.Gauge
}
)
// Handler returns the handler to be registered at /metrics
func Handler() http.Handler { return promhttp.Handler() }
// New creates a new Collector and registers the metrics
func New() *Collector {
return &Collector{
secretsCreated: promauto.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Name: metricSecretsCreated,
Help: "number of successfully created secrets",
}),
secretsRead: promauto.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Name: metricSecretsRead,
Help: "number of fetched (and destroyed) secrets",
}),
secretsCreateErrors: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Name: metricSecretsCreateErrors,
Help: "number of errors on secret creation for each reason",
}, []string{labelReason}),
secretsReadErrors: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Name: meticsSecretsReadErrors,
Help: "number of read-errors for each reason",
}, []string{labelReason}),
secretsStored: promauto.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: metricsSecretsStored,
Help: "number of secrets currently held in the backend store",
}),
}
}
// CountSecretCreated signalizes a secret has successfully been created
func (c Collector) CountSecretCreated() { c.secretsCreated.Inc() }
// CountSecretRead signalizes a secret has successfully been read and destroyed
func (c Collector) CountSecretRead() { c.secretsRead.Inc() }
// CountSecretCreateError signalizes an error occurred during secret
// creation. The reason must not be the error.Error() but a simple
// static string describing the error.
func (c Collector) CountSecretCreateError(reason string) {
c.secretsCreateErrors.WithLabelValues(reason).Inc()
}
// CountSecretReadError signalizes an error occurred during secret
// read. The reason must not be the error.Error() but a simple
// static string describing the error.
func (c Collector) CountSecretReadError(reason string) {
c.secretsReadErrors.WithLabelValues(reason).Inc()
}
// UpdateSecretsCount sets the current amount of secrets stored in the
// backend storage
func (c Collector) UpdateSecretsCount(count int64) {
c.secretsStored.Set(float64(count))
}

View File

@ -0,0 +1,106 @@
// Package memory implements a pure in-memory store for secrets which
// is suitable for testing and should not be used for productive use
package memory
import (
"sync"
"time"
"github.com/Luzifer/ots/pkg/storage"
"github.com/gofrs/uuid"
)
type (
memStorageSecret struct {
Expiry time.Time
Secret string
}
storageMem struct {
sync.RWMutex
store map[string]memStorageSecret
storePruneTimer *time.Ticker
}
)
// New creates a new In-Mem storage
func New() storage.Storage {
store := &storageMem{
store: make(map[string]memStorageSecret),
storePruneTimer: time.NewTicker(time.Minute),
}
go store.storePruner()
return store
}
func (s *storageMem) storePruner() {
for range s.storePruneTimer.C {
s.pruneStore()
}
}
func (s *storageMem) pruneStore() {
s.Lock()
defer s.Unlock()
for k, v := range s.store {
if v.hasExpired() {
delete(s.store, k)
}
}
}
func (s *storageMem) Count() (int64, error) {
s.RLock()
defer s.RUnlock()
return int64(len(s.store)), nil
}
func (s *storageMem) Create(secret string, expireIn time.Duration) (string, error) {
s.Lock()
defer s.Unlock()
var (
expire time.Time
id = uuid.Must(uuid.NewV4()).String()
)
if expireIn > 0 {
expire = time.Now().Add(expireIn)
}
s.store[id] = memStorageSecret{
Expiry: expire,
Secret: secret,
}
return id, nil
}
func (s *storageMem) ReadAndDestroy(id string) (string, error) {
s.Lock()
defer s.Unlock()
secret, ok := s.store[id]
if !ok {
return "", storage.ErrSecretNotFound
}
defer delete(s.store, id)
// Still check to see if the secret has expired in order to prevent a
// race condition where a secret has expired but the the store pruner has
// not yet been invoked.
if secret.hasExpired() {
return "", storage.ErrSecretNotFound
}
return secret.Secret, nil
}
func (m *memStorageSecret) hasExpired() bool {
return !m.Expiry.IsZero() && m.Expiry.Before(time.Now())
}

View File

@ -1,24 +1,30 @@
package main
// Package redis implements a Redis backed storage for secrets
package redis
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/Luzifer/ots/pkg/storage"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
redis "github.com/redis/go-redis/v9"
)
const redisDefaultPrefix = "io.luzifer.ots"
const (
redisDefaultPrefix = "io.luzifer.ots"
redisScanCount = 10
)
type storageRedis struct {
conn *redis.Client
}
func newStorageRedis() (storage, error) {
// New returns a new Redis backed storage
func New() (storage.Storage, error) {
if os.Getenv("REDIS_URL") == "" {
return nil, fmt.Errorf("REDIS_URL environment variable not set")
}
@ -30,7 +36,7 @@ func newStorageRedis() (storage, error) {
// in order to maintain backwards compatibility
opt, err := redis.ParseURL(strings.Replace(os.Getenv("REDIS_URL"), "tcp://", "redis://", 1))
if err != nil {
return nil, errors.Wrap(err, "parsing REDIS_URL")
return nil, fmt.Errorf("parsing REDIS_URL: %w", err)
}
s := &storageRedis{
@ -40,24 +46,50 @@ func newStorageRedis() (storage, error) {
return s, nil
}
func (s storageRedis) Count() (n int64, err error) {
var cursor uint64
for {
var keys []string
keys, cursor, err = s.conn.Scan(context.Background(), cursor, s.redisKey("*"), redisScanCount).Result()
if err != nil {
return n, fmt.Errorf("scanning stored keys: %w", err)
}
n += int64(len(keys))
if cursor == 0 {
break
}
}
return n, nil
}
func (s storageRedis) Create(secret string, expireIn time.Duration) (string, error) {
id := uuid.Must(uuid.NewV4()).String()
err := s.conn.Set(context.Background(), s.redisKey(id), secret, expireIn).Err()
if err != nil {
return "", fmt.Errorf("writing redis key: %w", err)
}
return id, errors.Wrap(err, "writing redis key")
return id, nil
}
func (s storageRedis) ReadAndDestroy(id string) (string, error) {
secret, err := s.conn.Get(context.Background(), s.redisKey(id)).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return "", errSecretNotFound
return "", storage.ErrSecretNotFound
}
return "", errors.Wrap(err, "getting key")
return "", fmt.Errorf("getting key: %w", err)
}
err = s.conn.Del(context.Background(), s.redisKey(id)).Err()
return secret, errors.Wrap(err, "deleting key")
if err != nil {
return secret, fmt.Errorf("deleting key: %w", err)
}
return secret, nil
}
func (storageRedis) redisKey(id string) string {

21
pkg/storage/storage.go Normal file
View File

@ -0,0 +1,21 @@
// Package storage describes the requirements a storage provider
// has to fulfill ot be usable in OTS
package storage
import (
"errors"
"time"
)
type (
// Storage is the interface to implement in each storage provider
Storage interface {
Count() (int64, error)
Create(secret string, expireIn time.Duration) (string, error)
ReadAndDestroy(id string) (string, error)
}
)
// ErrSecretNotFound is a generic error to be returned when a secret
// does not exist in the backend. It will then be handled by API.
var ErrSecretNotFound = errors.New("secret not found")

View File

@ -8,7 +8,7 @@
v-if="error"
class="row justify-content-center"
>
<div class="col-8">
<div class="col-12 col-md-8">
<div
class="alert alert-danger"
role="alert"
@ -24,13 +24,24 @@
</div>
<div
v-if="!$root.customize.disablePoweredBy"
class="row mt-4"
>
<div class="col form-text text-center">
{{ $t('text-powered-by') }}
<a href="https://github.com/Luzifer/ots"><i class="fab fa-github" /> OTS</a>
{{ $root.version }}
<span
v-if="!$root.customize.disablePoweredBy"
class="mx-2"
>
{{ $t('text-powered-by') }}
<a href="https://github.com/Luzifer/ots"><i class="fab fa-github" /> OTS</a>
{{ $root.version }}
</span>
<span
v-for="link in $root.customize.footerLinks"
:key="link.url"
class="mx-2"
>
<a :href="link.url">{{ link.name }}</a>
</span>
</div>
</div>
</div>
@ -89,6 +100,10 @@ export default {
mounted() {
window.onhashchange = this.hashLoad
this.hashLoad()
if (!this.$root.isSecureEnvironment) {
this.error = this.$t('alert-insecure-environment')
}
},
name: 'App',

View File

@ -5,7 +5,7 @@
:disabled="!content"
@click="copy"
>
<i class="fas fa-clipboard" />
<i :class="{'fas fa-fw fa-clipboard': !copyToClipboardSuccess, 'fas fa-fw fa-circle-check': copyToClipboardSuccess}" />
</button>
</template>
<script>
@ -29,7 +29,7 @@ export default {
this.copyToClipboardSuccess = true
window.setTimeout(() => {
this.copyToClipboardSuccess = false
}, 500)
}, 1500)
})
},
},

View File

@ -2,7 +2,7 @@
<template>
<!-- Creation disabled -->
<div
v-if="!canWrite"
v-if="!showCreateForm"
class="card border-info-subtle mb-3"
>
<div
@ -31,11 +31,12 @@
>
<div class="col-12 mb-3">
<label for="createSecretData">{{ $t('label-secret-data') }}</label>
<textarea
<grow-area
id="createSecretData"
v-model="secret"
class="form-control"
rows="5"
:rows="2"
@pasteFile="handlePasteFile"
/>
</div>
<div
@ -50,23 +51,37 @@
type="file"
multiple
:accept="$root.customize.acceptedFileTypes"
@change="updateFileSize"
@change="handleSelectFiles"
>
<div class="form-text">
{{ $t('text-max-filesize', { maxSize: bytesToHuman(maxFileSize) }) }}
</div>
<div
v-if="maxFileSizeExceeded"
v-if="invalidFilesSelected"
class="alert alert-danger"
>
{{ $t('text-invalid-files-selected') }}
</div>
<div
v-else-if="maxFileSizeExceeded"
class="alert alert-danger"
>
{{ $t('text-max-filesize-exceeded', { curSize: bytesToHuman(fileSize), maxSize: bytesToHuman(maxFileSize) }) }}
</div>
<FilesDisplay
v-if="attachedFiles.length > 0"
class="mt-3"
:can-delete="true"
:track-download="false"
:files="attachedFiles"
@fileClicked="deleteFile"
/>
</div>
<div class="col-md-6 col-12 order-2 order-md-1">
<button
type="submit"
class="btn btn-success"
:disabled="secret.trim().length < 1 || maxFileSizeExceeded || createRunning"
:disabled="!canCreate"
>
<template v-if="!createRunning">
{{ $t('btn-create-secret') }}
@ -111,6 +126,8 @@
import appCrypto from '../crypto.js'
import { bytesToHuman } from '../helpers'
import FilesDisplay from './fileDisplay.vue'
import GrowArea from './growarea.vue'
import OTSMeta from '../ots-meta'
const defaultExpiryChoices = [
@ -141,7 +158,13 @@ const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS
const passwordLength = 20
export default {
components: { FilesDisplay, GrowArea },
computed: {
canCreate() {
return (this.secret.trim().length > 0 || this.selectedFileMeta.length > 0) && !this.maxFileSizeExceeded && !this.invalidFilesSelected
},
expiryChoices() {
const choices = [{ text: this.$t('expire-default'), value: null }]
for (const choice of this.$root.customize.expiryChoices || defaultExpiryChoices) {
@ -166,6 +189,30 @@ export default {
return choices
},
invalidFilesSelected() {
if (this.$root.customize.acceptedFileTypes === '') {
// No limitation configured, no need to check
return false
}
const accepted = this.$root.customize.acceptedFileTypes.split(',')
for (const fm of this.selectedFileMeta) {
let isAccepted = false
for (const a of accepted) {
isAccepted ||= this.isAcceptedBy(fm, a)
}
if (!isAccepted) {
// Well we only needed one rejected
return true
}
}
// We found no reason to reject: This is fine!
return false
},
maxFileSize() {
return this.$root.customize.maxAttachmentSizeTotal === 0 ? internalMaxFileSize : Math.min(internalMaxFileSize, this.$root.customize.maxAttachmentSizeTotal)
},
@ -173,6 +220,10 @@ export default {
maxFileSizeExceeded() {
return this.fileSize > this.maxFileSize
},
showCreateForm() {
return this.canWrite && this.$root.isSecureEnvironment
},
},
created() {
@ -181,12 +232,14 @@ export default {
data() {
return {
attachedFiles: [],
canWrite: null,
createRunning: false,
fileSize: 0,
secret: '',
securePassword: null,
selectedExpiry: null,
selectedFileMeta: [],
}
},
@ -212,7 +265,7 @@ export default {
// createSecret executes the secret creation after encrypting the secret
createSecret() {
if (this.secret.trim().length < 1 || this.maxFileSizeExceeded) {
if (!this.canCreate) {
return false
}
@ -226,9 +279,9 @@ export default {
const meta = new OTSMeta()
meta.secret = this.secret
if (this.$refs.createSecretFiles) {
for (const f of [...this.$refs.createSecretFiles.files]) {
meta.files.push(f)
if (this.attachedFiles.length > 0) {
for (const f of this.attachedFiles) {
meta.files.push(f.fileObj)
}
}
@ -275,13 +328,61 @@ export default {
return false
},
updateFileSize() {
deleteFile(fileId) {
this.attachedFiles = [...this.attachedFiles].filter(file => file.id !== fileId)
this.updateFileMeta()
},
handlePasteFile(file) {
this.attachedFiles.push({
fileObj: file,
id: window.crypto.randomUUID(),
name: file.name,
size: file.size,
type: file.type,
})
this.updateFileMeta()
},
handleSelectFiles() {
for (const file of this.$refs.createSecretFiles.files) {
this.attachedFiles.push({
fileObj: file,
id: window.crypto.randomUUID(),
name: file.name,
size: file.size,
type: file.type,
})
}
this.updateFileMeta()
this.$refs.createSecretFiles.value = ''
},
isAcceptedBy(fileMeta, accept) {
if (/^(?:[a-z]+|\*)\/(?:[a-zA-Z0-9.+_-]+|\*)$/.test(accept)) {
// That's likely supposed to be a mime-type
return RegExp(`^${accept.replaceAll('*', '.*')}$`).test(fileMeta.type)
} else if (/^\.[a-z.]+$/.test(accept)) {
// That should be a file extension
return fileMeta.name.endsWith(accept)
}
// What exactly is it then? At least it can't accept anything.
return false
},
updateFileMeta() {
let cumSize = 0
for (const f of [...this.$refs.createSecretFiles.files]) {
for (const f of this.attachedFiles) {
cumSize += f.size
}
this.fileSize = cumSize
this.selectedFileMeta = this.attachedFiles.map(file => ({
name: file.name,
type: file.type,
}))
},
},

View File

@ -5,7 +5,10 @@
class="card-header bg-success-subtle"
v-html="$t('title-secret-created')"
/>
<div class="card-body">
<div
v-if="!burned"
class="card-body"
>
<p v-html="$t('text-pre-url')" />
<div class="input-group mb-3">
<input
@ -16,8 +19,18 @@
:value="secretUrl"
@focus="$refs.secretUrl.select()"
>
<app-clipboard-button :content="secretUrl" />
<app-clipboard-button
:content="secretUrl"
:title="$t('tooltip-copy-to-clipboard')"
/>
<app-qr-button :qr-content="secretUrl" />
<button
class="btn btn-danger"
:title="$t('tooltip-burn-secret')"
@click="burnSecret"
>
<i class="fas fa-fire fa-fw" />
</button>
</div>
<p v-html="$t('text-burn-hint')" />
<p v-if="expiresAt">
@ -25,6 +38,12 @@
<strong>{{ expiresAt.toLocaleString() }}</strong>
</p>
</div>
<div
v-else
class="card-body"
>
{{ $t('text-secret-burned') }}
</div>
</div>
</template>
<script>
@ -47,10 +66,20 @@ export default {
data() {
return {
burned: false,
popover: null,
}
},
methods: {
burnSecret() {
return fetch(`api/get/${this.secretId}`)
.then(() => {
this.burned = true
})
},
},
mounted() {
// Give the interface a moment to transistion and focus
window.setTimeout(() => this.$refs.secretUrl.focus(), 100)

View File

@ -0,0 +1,90 @@
<template>
<div class="list-group mb-3">
<a
v-for="file in files"
:key="file.id"
class="cursor-pointer list-group-item list-group-item-action font-monospace d-flex align-items-center"
:href="file.url"
:download="file.name"
@click="handleClick(file)"
>
<i :class="fasFileType(file.type)" />
<span>{{ file.name }}</span>
<span class="ms-auto">{{ bytesToHuman(file.size) }}</span>
<template v-if="trackDownload">
<i
v-if="!hasDownloaded[file.id]"
class="fas fa-fw fa-download ms-2 text-warning"
/>
<i
v-else
class="fas fa-fw fa-circle-check ms-2 text-success"
/>
</template>
<template v-if="canDelete">
<i
class="fas fa-fw fa-trash ms-2 text-danger"
/>
</template>
</a>
</div>
</template>
<script>
import { bytesToHuman } from '../helpers'
export default {
data() {
return {
hasDownloaded: {},
}
},
methods: {
bytesToHuman,
fasFileType(type) {
return [
'fas',
'fa-fw',
'me-2',
...[
{ icon: ['fa-file-pdf'], match: /application\/pdf/ },
{ icon: ['fa-file-audio'], match: /^audio\// },
{ icon: ['fa-file-image'], match: /^image\// },
{ icon: ['fa-file-lines'], match: /^text\// },
{ icon: ['fa-file-video'], match: /^video\// },
{ icon: ['fa-file-zipper'], match: /^application\/(gzip|x-tar|zip)$/ },
{ icon: ['fa-file-circle-question'], match: /.*/ },
].filter(el => el.match.test(type))[0].icon,
].join(' ')
},
handleClick(file) {
this.$set(this.hasDownloaded, file.id, true)
this.$emit('fileClicked', file.id)
},
},
name: 'AppFileDisplay',
props: {
canDelete: {
default: false,
required: false,
type: Boolean,
},
files: {
required: true,
type: Array,
},
trackDownload: {
default: true,
required: false,
type: Boolean,
},
},
}
</script>

View File

@ -0,0 +1,96 @@
<template>
<textarea
ref="area"
v-model="data"
style="resize: none;"
@paste="handlePaste"
/>
</template>
<script>
export default {
created() {
this.data = this.value
},
data() {
return {
data: '',
}
},
methods: {
changeSize() {
const verticalBorderSize = this.getStyle('borderTopWidth') + this.getStyle('borderBottomWidth') || 0
const verticalPaddingSize = this.getStyle('paddingTop') + this.getStyle('paddingBottom') || 0
const smallestHeight = this.getStyle('lineHeight') * this.rows + verticalBorderSize + verticalPaddingSize
this.$refs.area.style.height = `${smallestHeight}px`
const newHeight = this.$refs.area.scrollHeight + verticalBorderSize
this.$refs.area.style.height = `${newHeight}px`
},
getStyle(name) {
return parseInt(getComputedStyle(this.$refs.area, null)[name])
},
handlePaste(evt) {
if ([...evt.clipboardData.items]
.filter(item => item.kind !== 'string')
.length === 0) {
return
}
/*
* We have something else than text, prevent using clipboard and
* pasting and emit an event containing the file data
*/
evt.stopPropagation()
evt.preventDefault()
for (const item of evt.clipboardData.items) {
if (item.kind === 'string') {
continue
}
this.$emit('pasteFile', item.getAsFile())
}
},
},
mounted() {
this.changeSize()
},
name: 'GrowArea',
props: {
rows: {
default: 4,
type: Number,
},
value: {
default: '',
type: String,
},
},
watch: {
data(to, from) {
this.changeSize()
if (to !== from) {
this.$emit('input', to)
}
},
value(to) {
if (to !== this.data) {
this.data = to
}
},
},
}
</script>

View File

@ -7,13 +7,13 @@
@click.prevent="$root.navigate('/')"
>
<i
v-if="!$root.customize.appIcon"
v-if="!appIcon"
class="fas fa-user-secret mr-1"
/>
<img
v-else
class="mr-1"
:src="$root.customize.appIcon"
:src="appIcon"
>
<span v-if="!$root.customize.disableAppTitle">{{ $root.customize.appTitle }}</span>
</a>
@ -77,6 +77,17 @@
<script>
export default {
computed: {
appIcon() {
// Use specified icon or fall back to null
const appIcon = this.$root.customize.appIcon || null
// Use specified icon or fall back to light-mode appIcon (which might be null)
const darkIcon = this.$root.customize.appIconDark || appIcon
return this.$root.darkTheme ? darkIcon : appIcon
},
},
name: 'AppNavbar',
}
</script>

View File

@ -6,7 +6,7 @@
v-html="$t('title-reading-secret')"
/>
<div class="card-body">
<template v-if="!secret">
<template v-if="!secret && files.length === 0">
<p v-html="$t('text-pre-reveal-hint')" />
<button
class="btn btn-success"
@ -23,23 +23,30 @@
</button>
</template>
<template v-else>
<div class="input-group mb-3">
<textarea
<div
v-if="secret"
class="input-group mb-3"
>
<grow-area
class="form-control"
readonly
:value="secret"
rows="4"
:rows="4"
/>
<div class="d-flex align-items-start p-0">
<div
class="btn-group-vertical"
role="group"
>
<app-clipboard-button :content="secret" />
<app-clipboard-button
:content="secret"
:title="$t('tooltip-copy-to-clipboard')"
/>
<a
class="btn btn-secondary"
:href="secretContentBlobURL"
download
:title="$t('tooltip-download-as-file')"
>
<i class="fas fa-fw fa-download" />
</a>
@ -47,23 +54,11 @@
</div>
</div>
</div>
<p v-html="$t('text-hint-burned')" />
<template v-if="files.length > 0">
<p v-html="$t('text-attached-files')" />
<ul>
<li
v-for="file in files"
:key="file.name"
class="font-monospace"
>
<a
:href="file.url"
:download="file.name"
>{{ file.name }}</a>
({{ bytesToHuman(file.size) }})
</li>
</ul>
<FilesDisplay :files="files" />
</template>
<p v-html="$t('text-hint-burned')" />
</template>
</div>
</div>
@ -72,11 +67,12 @@
import appClipboardButton from './clipboard-button.vue'
import appCrypto from '../crypto.js'
import appQrButton from './qr-button.vue'
import { bytesToHuman } from '../helpers'
import FilesDisplay from './fileDisplay.vue'
import GrowArea from './growarea.vue'
import OTSMeta from '../ots-meta'
export default {
components: { appClipboardButton, appQrButton },
components: { FilesDisplay, GrowArea, appClipboardButton, appQrButton },
data() {
return {
@ -89,8 +85,6 @@ export default {
},
methods: {
bytesToHuman,
// requestSecret requests the encrypted secret from the backend
requestSecret() {
this.secretLoading = true
@ -126,7 +120,13 @@ export default {
file.arrayBuffer()
.then(ab => {
const blobURL = window.URL.createObjectURL(new Blob([ab], { type: file.type }))
this.files.push({ name: file.name, size: ab.byteLength, url: blobURL })
this.files.push({
id: window.crypto.randomUUID(),
name: file.name,
size: ab.byteLength,
type: file.type,
url: blobURL,
})
})
})
this.secretLoading = false

View File

@ -6,33 +6,37 @@ export default {
'ca': JSON.parse('{"alert-secret-not-found":"Aquest no és el secret que busques\u0026hellip; - Si esperaves que el secret estiguera ací, és possible que s\'haja vist compromés, ja que una altra persona podria haver obert l\'enllaç en comptes de tu.","alert-something-went-wrong":"Alguna cosa ha eixit malament. Ens sap molt greu\u0026hellip;","btn-create-secret":"Crea el secret!","btn-create-secret-processing":"El secret s\'està creant...","btn-new-secret":"Nou secret","btn-reveal-secret":"Mostra\'m el secret!","btn-reveal-secret-processing":"El secret s\'està desxifrant...","btn-show-explanation":"Com funciona?","expire-default":"Caducitat predeterminada","expire-n-days":"{n} dia | {n} dies","expire-n-hours":"{n} hora | {n} hores","expire-n-minutes":"{n} minut | {n} minuts","expire-n-seconds":"{n} segon | {n} segons","items-explanation":["Introduïx un secret en el formulari que hi ha en aquesta pàgina","El teu navegador xifra el secret utilitzant una contrasenya generada","Únicament s\'envia al servidor el secret xifrat (mai s\'envien ni el secret sense xifrar ni la contrasenya!)","El servidor emmagatzema el secret xifrat durant un temps limitat","Envia al destinatari l\'enllaç mostrat, que conté l\'identificador del secret i la contrasenya de desxifrat","El destinatari pot veure el secret una sola vegada: si no pot, el secret podria haver sigut vist per una altra persona!","Quan s\'ha obtingut per primera i única vegada el secret xifrat, s\'elimina del servidor"],"label-expiry":"Caduca en:","label-secret-data":"Informació secreta:","label-secret-files":"Adjuntar arxius:","text-attached-files":"El remitent ha adjuntat arxius al secret. Assegura\'t que confies en aquesta persona, ja que els arxius no han sigut revisats.","text-burn-hint":"Per favor, recorda no accedir a aquest enllaç tu mateix, ja que això destruiria el secret. Només has de passar-li\'l a una altra persona!","text-burn-time":"Si no es mostra abans, aquest secret s\'eliminarà automàticament:","text-hint-burned":"\u003cstrong\u003eAtenció:\u003c/strong\u003e Només veuràs això una vegada. Quan recarregues la pàgina, el secret desapareixerà, així que copia\'l ja\u0026hellip;","text-max-filesize":"Mida màxima: {maxSize}","text-max-filesize-exceeded":"Els arxius seleccionats són massa grans per adjuntar-los: {curSize} / {maxSize}","text-powered-by":"Funciona amb","text-pre-reveal-hint":"Per a mostrar el secret prem aquest botó, però tingues en compte que en fer-ho es destruirà. Només pots veure\'l una vegada!","text-pre-url":"El teu secret ha sigut creat i emmagatzemat en el següent enllaç:","text-secret-create-disabled":"La creació de nous secrets està desactivada en aquesta instància.","title-explanation":"Així és com funciona\u0026hellip;","title-new-secret":"Crea un nou secret","title-reading-secret":"Obtenint el teu secret\u0026hellip;","title-secret-create-disabled":"S\'ha desactivat la creació de secrets...","title-secret-created":"Secret creat!"}'),
'de': switchFormal(
JSON.parse('{"alert-secret-not-found":"Dieses Secret existiert nicht. - Falls Sie diesen Link noch nicht selbst geöffnet haben, könnte der Inhalt kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.","alert-something-went-wrong":"Irgendwas ging schief. Entschuldigung\u0026hellip;","btn-create-secret":"Secret erstellen!","btn-create-secret-processing":"Secret wird erstellt…","btn-new-secret":"Neues Secret","btn-reveal-secret":"Secret anzeigen","btn-reveal-secret-processing":"Secret wird entschlüsselt…","btn-show-explanation":"Wie funktioniert das?","expire-default":"Server-Standard","expire-n-days":"{n} Tag | {n} Tage","expire-n-hours":"{n} Stunde | {n} Stunden","expire-n-minutes":"{n} Minute | {n} Minuten","expire-n-seconds":"{n} Sekunde | {n} Sekunden","items-explanation":["Sie geben ein Secret auf dieser Seite ein","Ihr Browser verschlüsselt das Secret mit einem generierten Passwort","Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!)","Der Server speichert das verschlüsselte Secret für eine Weile","Sie geben die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger","Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!","Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht"],"label-expiry":"Ablauf in:","label-secret-data":"Inhalt des Secrets:","label-secret-files":"Dateien Anhängen:","text-attached-files":"Der Absender hat Dateien an das Secret angehängt. Stellen Sie sicher, dass Sie dem Absender vertrauen, da die Dateien nicht geprüft wurden!","text-burn-hint":"Bitte rufen Sie die URL nicht selbst auf, da das Secret dadurch zerstört würde.","text-burn-time":"Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:","text-hint-burned":"\u003cstrong\u003eAchtung:\u003c/strong\u003e Sie können das Secret nur einmal ansehen! Sobald Sie die Seite neu laden, kann das Secret nicht erneut abgerufen werden, also besser direkt kopieren und sicher abspeichern\u0026hellip;","text-max-filesize":"Maximale Größe: {maxSize}","text-max-filesize-exceeded":"Die ausgewählten Dateien übersteigen die maximale Größe: {curSize} / {maxSize}","text-powered-by":"Läuft mit","text-pre-reveal-hint":"Klicken Sie auf diesen Button um das Secret anzuzeigen, bedenken Sie aber, dass das Secret nur einmal angezeigt und dabei gelöscht wird.","text-pre-url":"Das Secret wurde angelegt und unter folgender URL gespeichert:","text-secret-create-disabled":"Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.","title-explanation":"So funktioniert es\u0026hellip;","title-new-secret":"Ein neues Secret erstellen","title-reading-secret":"Secret auslesen\u0026hellip;","title-secret-create-disabled":"Erstellen von Secrets deaktiviert…","title-secret-created":"Secret erstellt!"}'),
JSON.parse('{"alert-secret-not-found":"Das ist nicht das Secret, was du suchst\u0026hellip; - Falls du diesen Link noch nicht selbst geöffnet hast, könnte das Secret kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.","alert-something-went-wrong":"Irgendwas ging schief. Entschuldigung\u0026hellip;","btn-create-secret":"Secret erstellen!","btn-create-secret-processing":"Secret wird erstellt…","btn-new-secret":"Neues Secret","btn-reveal-secret":"Zeig mir das Secret!","btn-reveal-secret-processing":"Secret wird entschlüsselt…","btn-show-explanation":"Wie funktioniert das?","expire-default":"Server-Standard","expire-n-days":"{n} Tag | {n} Tage","expire-n-hours":"{n} Stunde | {n} Stunden","expire-n-minutes":"{n} Minute | {n} Minuten","expire-n-seconds":"{n} Sekunde | {n} Sekunden","items-explanation":["Du gibst ein Secret auf dieser Seite ein","Dein Browser verschlüsselt das Secret mit einem generierten Passwort","Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!)","Der Server speichert das verschlüsselte Secret für eine Weile","Du gibst die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger","Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!","Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht"],"label-expiry":"Ablauf in:","label-secret-data":"Inhalt des Secrets:","label-secret-files":"Dateien Anhängen:","text-attached-files":"Der Absender hat Dateien an das Secret angehängt. Stell sicher, dass du dem Absender vertraust, da die Dateien nicht geprüft wurden!","text-burn-hint":"Bitte rufe die URL nicht selbst auf, da das Secret dadurch zerstört würde. Gib sie einfach weiter!","text-burn-time":"Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:","text-hint-burned":"\u003cstrong\u003eAchtung:\u003c/strong\u003e Du kannst das nur einmal ansehen! Sobald du die Seite neu lädst, ist das Secret verschwunden, also besser direkt kopieren und sicher abspeichern\u0026hellip;","text-max-filesize":"Maximale Größe: {maxSize}","text-max-filesize-exceeded":"Die ausgewählten Dateien übersteigen die maximale Größe: {curSize} / {maxSize}","text-powered-by":"Läuft mit","text-pre-reveal-hint":"Um das Secret anzuzeigen klicke diesen Button aber denk dran, dass das Secret nur einmal angezeigt und dabei gelöscht wird.","text-pre-url":"Dein Secret wurde angelegt und unter folgender URL gespeichert:","text-secret-create-disabled":"Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.","title-explanation":"So funktioniert es\u0026hellip;","title-new-secret":"Erstelle ein neues Secret","title-reading-secret":"Secret auslesen\u0026hellip;","title-secret-create-disabled":"Erstellen von Secrets deaktiviert…","title-secret-created":"Secret erstellt!"}'),
JSON.parse('{"alert-insecure-environment":"Sie besuchen diese Instanz über eine unsichere Verbindung. Sie können deswegen keine Secrets erstellen oder lesen.","alert-secret-not-found":"Dieses Secret existiert nicht. - Falls Sie diesen Link noch nicht selbst geöffnet haben, könnte der Inhalt kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.","alert-something-went-wrong":"Irgendwas ging schief. Entschuldigung\u0026hellip;","btn-create-secret":"Secret erstellen!","btn-create-secret-processing":"Secret wird erstellt…","btn-new-secret":"Neues Secret","btn-reveal-secret":"Secret anzeigen","btn-reveal-secret-processing":"Secret wird entschlüsselt…","btn-show-explanation":"Wie funktioniert das?","expire-default":"Server-Standard","expire-n-days":"{n} Tag | {n} Tage","expire-n-hours":"{n} Stunde | {n} Stunden","expire-n-minutes":"{n} Minute | {n} Minuten","expire-n-seconds":"{n} Sekunde | {n} Sekunden","items-explanation":["Sie geben ein Secret auf dieser Seite ein","Ihr Browser verschlüsselt das Secret mit einem generierten Passwort","Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!)","Der Server speichert das verschlüsselte Secret für eine Weile","Sie geben die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger","Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!","Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht"],"label-expiry":"Ablauf in:","label-secret-data":"Inhalt des Secrets:","label-secret-files":"Dateien Anhängen:","text-attached-files":"Der Absender hat Dateien an das Secret angehängt. Stellen Sie sicher, dass Sie dem Absender vertrauen, da die Dateien nicht geprüft wurden!","text-burn-hint":"Bitte rufen Sie die URL nicht selbst auf, da das Secret dadurch zerstört würde.","text-burn-time":"Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:","text-hint-burned":"\u003cstrong\u003eAchtung:\u003c/strong\u003e Sie können das Secret nur einmal ansehen! Sobald Sie die Seite neu laden, kann das Secret nicht erneut abgerufen werden, also besser direkt kopieren und sicher abspeichern\u0026hellip;","text-invalid-files-selected":"Mindestens eine der ausgewählten Dateien ist nicht als Anhang erlaubt.","text-max-filesize":"Maximale Größe: {maxSize}","text-max-filesize-exceeded":"Die ausgewählten Dateien übersteigen die maximale Größe: {curSize} / {maxSize}","text-powered-by":"Läuft mit","text-pre-reveal-hint":"Klicken Sie auf diesen Button um das Secret anzuzeigen, bedenken Sie aber, dass das Secret nur einmal angezeigt und dabei gelöscht wird.","text-pre-url":"Das Secret wurde angelegt und unter folgender URL gespeichert:","text-secret-burned":"Das Secret wurde zerstört.","text-secret-create-disabled":"Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.","title-explanation":"So funktioniert es\u0026hellip;","title-new-secret":"Ein neues Secret erstellen","title-reading-secret":"Secret auslesen\u0026hellip;","title-secret-create-disabled":"Erstellen von Secrets deaktiviert…","title-secret-created":"Secret erstellt!","tooltip-burn-secret":"Secret jetzt zerstören!","tooltip-copy-to-clipboard":"In die Zwischenablage kopieren","tooltip-download-as-file":"Als Datei herunterladen"}'),
JSON.parse('{"alert-insecure-environment":"Du besuchst diese Instanz über eine unsichere Verbindung. Du kannst deswegen keine Secrets erstellen oder lesen.","alert-secret-not-found":"Das ist nicht das Secret, was du suchst\u0026hellip; - Falls du diesen Link noch nicht selbst geöffnet hast, könnte das Secret kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.","alert-something-went-wrong":"Irgendwas ging schief. Entschuldigung\u0026hellip;","btn-create-secret":"Secret erstellen!","btn-create-secret-processing":"Secret wird erstellt…","btn-new-secret":"Neues Secret","btn-reveal-secret":"Zeig mir das Secret!","btn-reveal-secret-processing":"Secret wird entschlüsselt…","btn-show-explanation":"Wie funktioniert das?","expire-default":"Server-Standard","expire-n-days":"{n} Tag | {n} Tage","expire-n-hours":"{n} Stunde | {n} Stunden","expire-n-minutes":"{n} Minute | {n} Minuten","expire-n-seconds":"{n} Sekunde | {n} Sekunden","items-explanation":["Du gibst ein Secret auf dieser Seite ein","Dein Browser verschlüsselt das Secret mit einem generierten Passwort","Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!)","Der Server speichert das verschlüsselte Secret für eine Weile","Du gibst die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger","Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!","Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht"],"label-expiry":"Ablauf in:","label-secret-data":"Inhalt des Secrets:","label-secret-files":"Dateien Anhängen:","text-attached-files":"Der Absender hat Dateien an das Secret angehängt. Stell sicher, dass du dem Absender vertraust, da die Dateien nicht geprüft wurden!","text-burn-hint":"Bitte rufe die URL nicht selbst auf, da das Secret dadurch zerstört würde. Gib sie einfach weiter!","text-burn-time":"Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:","text-hint-burned":"\u003cstrong\u003eAchtung:\u003c/strong\u003e Du kannst das nur einmal ansehen! Sobald du die Seite neu lädst, ist das Secret verschwunden, also besser direkt kopieren und sicher abspeichern\u0026hellip;","text-invalid-files-selected":"Mindestens eine der ausgewählten Dateien ist nicht als Anhang erlaubt.","text-max-filesize":"Maximale Größe: {maxSize}","text-max-filesize-exceeded":"Die ausgewählten Dateien übersteigen die maximale Größe: {curSize} / {maxSize}","text-powered-by":"Läuft mit","text-pre-reveal-hint":"Um das Secret anzuzeigen klicke diesen Button aber denk dran, dass das Secret nur einmal angezeigt und dabei gelöscht wird.","text-pre-url":"Dein Secret wurde angelegt und unter folgender URL gespeichert:","text-secret-burned":"Das Secret wurde zerstört.","text-secret-create-disabled":"Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.","title-explanation":"So funktioniert es\u0026hellip;","title-new-secret":"Erstelle ein neues Secret","title-reading-secret":"Secret auslesen\u0026hellip;","title-secret-create-disabled":"Erstellen von Secrets deaktiviert…","title-secret-created":"Secret erstellt!","tooltip-burn-secret":"Secret jetzt zerstören!","tooltip-copy-to-clipboard":"In die Zwischenablage kopieren","tooltip-download-as-file":"Als Datei herunterladen"}'),
),
'en': JSON.parse('{"alert-secret-not-found":"This is not the secret you are looking for\u0026hellip; - If you expected the secret to be here it might be compromised as someone else might have opened the link already.","alert-something-went-wrong":"Something went wrong. I\'m very sorry about this\u0026hellip;","btn-create-secret":"Create the secret!","btn-create-secret-processing":"Secret is being created…","btn-new-secret":"New Secret","btn-reveal-secret":"Show me the secret!","btn-reveal-secret-processing":"Secret is being decrypted…","btn-show-explanation":"How does this work?","expire-default":"Default Expiry","expire-n-days":"{n} day | {n} days","expire-n-hours":"{n} hour | {n} hours","expire-n-minutes":"{n} minute | {n} minutes","expire-n-seconds":"{n} second | {n} seconds","items-explanation":["You enter a secret into the field on this page","Your browser encrypts the secret using a generated password","Only the encrypted secret is sent to the server (neither the plain secret nor the password are ever sent!)","The server stores the encrypted secret for a certain time","You pass the displayed URL containing the ID and the decryption password to the recipient","The recipient can view the secret exactly once: If they can\'t, the secret might have been viewed by someone else!","After the encrypted secret has been retrieved once, it is deleted from the server"],"label-expiry":"Expire in:","label-secret-data":"Secret data:","label-secret-files":"Attach Files:","text-attached-files":"The sender attached files to the secret. Make sure you trust the sender as the files were not checked!","text-burn-hint":"Please remember not to go to this URL yourself as that would destroy the secret. Just pass it to someone else!","text-burn-time":"If not viewed before, this secret will automatically be deleted:","text-hint-burned":"\u003cstrong\u003eAttention:\u003c/strong\u003e You\'re only seeing this once. As soon as you reload the page the secret will be gone so maybe copy it now\u0026hellip;","text-max-filesize":"Maximum size: {maxSize}","text-max-filesize-exceeded":"The file(s) you chose are too big to attach: {curSize} / {maxSize}","text-powered-by":"Powered by","text-pre-reveal-hint":"To reveal the secret click this button but be aware doing so will destroy the secret. You can only view it once!","text-pre-url":"Your secret was created and stored using this URL:","text-secret-create-disabled":"The creation of new secrets is disabled in this instance.","title-explanation":"This is how it works\u0026hellip;","title-new-secret":"Create a new secret","title-reading-secret":"Reading your secret\u0026hellip;","title-secret-create-disabled":"Secret creation disabled…","title-secret-created":"Secret created!"}'),
'en': JSON.parse('{"alert-insecure-environment":"You are accessing this instance using an insecure connection. You will not be able to create or read secrets.","alert-secret-not-found":"This is not the secret you are looking for\u0026hellip; - If you expected the secret to be here it might be compromised as someone else might have opened the link already.","alert-something-went-wrong":"Something went wrong. I\'m very sorry about this\u0026hellip;","btn-create-secret":"Create the secret!","btn-create-secret-processing":"Secret is being created…","btn-new-secret":"New Secret","btn-reveal-secret":"Show me the secret!","btn-reveal-secret-processing":"Secret is being decrypted…","btn-show-explanation":"How does this work?","expire-default":"Default Expiry","expire-n-days":"{n} day | {n} days","expire-n-hours":"{n} hour | {n} hours","expire-n-minutes":"{n} minute | {n} minutes","expire-n-seconds":"{n} second | {n} seconds","items-explanation":["You enter a secret into the field on this page","Your browser encrypts the secret using a generated password","Only the encrypted secret is sent to the server (neither the plain secret nor the password are ever sent!)","The server stores the encrypted secret for a certain time","You pass the displayed URL containing the ID and the decryption password to the recipient","The recipient can view the secret exactly once: If they can\'t, the secret might have been viewed by someone else!","After the encrypted secret has been retrieved once, it is deleted from the server"],"label-expiry":"Expire in:","label-secret-data":"Secret data:","label-secret-files":"Attach Files:","text-attached-files":"The sender attached files to the secret. Make sure you trust the sender as the files were not checked!","text-burn-hint":"Please remember not to go to this URL yourself as that would destroy the secret. Just pass it to someone else!","text-burn-time":"If not viewed before, this secret will automatically be deleted:","text-hint-burned":"\u003cstrong\u003eAttention:\u003c/strong\u003e You\'re only seeing this once. As soon as you reload the page the secret will be gone so maybe copy it now\u0026hellip;","text-invalid-files-selected":"At least one of the selected files is not allowed as an attachment.","text-max-filesize":"Maximum size: {maxSize}","text-max-filesize-exceeded":"The file(s) you chose are too big to attach: {curSize} / {maxSize}","text-powered-by":"Powered by","text-pre-reveal-hint":"To reveal the secret click this button but be aware doing so will destroy the secret. You can only view it once!","text-pre-url":"Your secret was created and stored using this URL:","text-secret-burned":"The secret was successfully destroyed.","text-secret-create-disabled":"The creation of new secrets is disabled in this instance.","title-explanation":"This is how it works\u0026hellip;","title-new-secret":"Create a new secret","title-reading-secret":"Reading your secret\u0026hellip;","title-secret-create-disabled":"Secret creation disabled…","title-secret-created":"Secret created!","tooltip-burn-secret":"Burn Secret now!","tooltip-copy-to-clipboard":"Copy to Clipboard","tooltip-download-as-file":"Download as File"}'),
'es': JSON.parse('{"alert-secret-not-found":"Este no es el secreto que buscas\u0026hellip; - Si esperabas que el secreto estuviera aquí, es posible que se haya visto comprometido, ya que otra persona podría haber abierto el enlace en tu lugar.","alert-something-went-wrong":"Algo ha salido mal. Lo sentimos mucho\u0026hellip;","btn-create-secret":"¡Crea el secreto!","btn-create-secret-processing":"El secreto se está creando...","btn-new-secret":"Nuevo secreto","btn-reveal-secret":"¡Muéstrame el secreto!","btn-reveal-secret-processing":"El secreto se está descifrando...","btn-show-explanation":"¿Cómo funciona?","expire-default":"Caducidad predeterminada","expire-n-days":"{n} día | {n} días","expire-n-hours":"{n} hora | {n} horas","expire-n-minutes":"{n} minuto | {n} minutos","expire-n-seconds":"{n} segundo | {n} segundos","items-explanation":["Introduce un secreto en el formulario que hay en esta página","Tu navegador cifra el secreto utilizando una contraseña generada","Únicamente se envía al servidor el secreto cifrado (¡nunca se envían ni el secreto sin cifrar ni la contraseña!)","El servidor almacena el secreto cifrado durante un tiempo limitado","Envía al destinatario el enlace mostrado, que contiene el identificador del secreto y la contraseña de descifrado","El destinatario puede ver el secreto una sola vez: si no puede, ¡el secreto podría haber sido visto por otra persona!","Cuando se ha obtenido por primera y única vez el secreto cifrado, se elimina del servidor"],"label-expiry":"Caduca en:","label-secret-data":"Información secreta:","label-secret-files":"Adjuntar archivos:","text-attached-files":"El remitente ha adjuntado archivos al secreto. Asegúrate de que confías en esta persona, ya que los archivos no han sido revisados.","text-burn-hint":"Por favor, recuerda no acceder a este enlace tú mismo, ya que esto destruiría el secreto. ¡Solo tienes que pasárselo a otra persona!","text-burn-time":"Si no se muestra antes, este secreto se eliminará automáticamente:","text-hint-burned":"\u003cstrong\u003eAtención:\u003c/strong\u003e Solo verás esto una vez. En cuanto recargues la página, el secreto desaparecerá, así que cópialo ya\u0026hellip;","text-max-filesize":"Tamaño máximo: {maxSize}","text-max-filesize-exceeded":"Los archivos seleccionados son demasiado grandes para adjuntarlos: {curSize} / {maxSize}","text-powered-by":"Funciona con","text-pre-reveal-hint":"Para mostrar el secreto pulsa este botón, pero ten en cuenta que al hacerlo se destruirá. ¡Solo puedes verlo una vez!","text-pre-url":"Tu secreto ha sido creado y almacenado en el siguiente enlace:","text-secret-create-disabled":"La creación de nuevos secretos está desactivada en esta instancia.","title-explanation":"Así es como funciona\u0026hellip;","title-new-secret":"Crea un nuevo secreto","title-reading-secret":"Obteniendo tu secreto\u0026hellip;","title-secret-create-disabled":"Creación de secretos desactivada...","title-secret-created":"¡Secreto creado!"}'),
'fr': JSON.parse('{"alert-secret-not-found":"Ce secret n\'est pas celui que vous cherchez\u0026hellip; - Si vous comptiez trouvez ce secret ici, il a pu être compromis car quelqu\'un a probablement déjà ouvert le lien.","alert-something-went-wrong":"Un problème est survenu. Nous en sommes désolés\u0026hellip;","btn-create-secret":"Créer le secret!","btn-new-secret":"Nouveau secret","btn-reveal-secret":"Voir le secret!","btn-show-explanation":"Comment ça fonctionne?","expire-n-days":"{n} jour | {n} jours","expire-n-hours":"{n} heure | {n} heures","expire-n-minutes":"{n} minute | {n} minutes","expire-n-seconds":"{n} seconde | {n} secondes","items-explanation":["Vous saisissez le secret dans un champ sur cette page","Votre navigateur chiffre le secret en utilisant un mot de passe généré","Seul le secret chiffré est envoyé au serveur (ni le secret en clair, ni le mot de passe ne sont envoyés!)","Le serveur stocke le secret chiffré pendant un certain temps","Vous fournissez l\'URL affichée contenant l\'identifiant et le mot de passe de déchiffrage au destinataire","Le destintaire ne peut voir le secret qu\'une fois: si cela ne fonctionne pas, c\'est que le secret a été consulté par quelqu\'un d\'autre!","Dès que le secret chiffré a été récupéré, il est supprimé du serveur"],"label-secret-data":"Données secrètes:","text-burn-hint":"Attention de ne pas ouvrir cette URL vous-même, cela détruirait le secret. Fournissez-la à quelqu\'un d\'autre!","text-hint-burned":"\u003cstrong\u003eAttention:\u003c/strong\u003e Vous ne pouvez consulter ce contenu qu\'une fois. Le secret sera détruit dès que vous rechargez la page, donc copiez le maintenant\u0026hellip;","text-powered-by":"Propulsé par","text-pre-reveal-hint":"Pour afficher le secret, cliquez sur ce bouton, mais soyez conscient que cela le détruira. Vous ne pouvez l\'afficher qu\'une fois!","text-pre-url":"Votre secret a été créé et stocké à cette URL:","text-secret-create-disabled":"La création de nouveaux secrets est désactivée dans ce cas.","title-explanation":"Voici comment ça fonctionne\u0026hellip;","title-new-secret":"Créer un nouveau secret","title-reading-secret":"Lecture du secret\u0026hellip;","title-secret-create-disabled":"Création secrète désactivée...","title-secret-created":"Secret créé!"}'),
'fr': JSON.parse('{"alert-secret-not-found":"Ce secret n\'est pas celui que vous cherchez\u0026hellip; - Si vous comptiez trouvez ce secret ici, il a pu être compromis car quelqu\'un a probablement déjà ouvert le lien.","alert-something-went-wrong":"Un problème est survenu. Nous en sommes désolés\u0026hellip;","btn-create-secret":"Créer le secret!","btn-create-secret-processing":"Secret en cours de création ...","btn-new-secret":"Nouveau secret","btn-reveal-secret":"Voir le secret!","btn-reveal-secret-processing":"Secret en cours de déchiffrement ...","btn-show-explanation":"Comment ça fonctionne?","expire-default":"Expiration par défaut","expire-n-days":"{n} jour | {n} jours","expire-n-hours":"{n} heure | {n} heures","expire-n-minutes":"{n} minute | {n} minutes","expire-n-seconds":"{n} seconde | {n} secondes","items-explanation":["Vous saisissez le secret dans un champ sur cette page","Votre navigateur chiffre le secret en utilisant un mot de passe généré","Seul le secret chiffré est envoyé au serveur (ni le secret en clair, ni le mot de passe ne sont envoyés!)","Le serveur stocke le secret chiffré pendant un certain temps","Vous fournissez l\'URL affichée contenant l\'identifiant et le mot de passe de déchiffrage au destinataire","Le destinataire ne peut voir le secret qu\'une fois: si cela ne fonctionne pas, c\'est que le secret a été consulté par quelqu\'un d\'autre!","Dès que le secret chiffré a été récupéré, il est supprimé du serveur"],"label-expiry":"Expiration dans:","label-secret-data":"Données secrètes:","label-secret-files":"Attacher des fichiers:","text-attached-files":"L\'émetteur a attaché des fichiers au secret. Assurez-vous d\'avoir confiance en l\'émetteur, les fichiers n\'ont pas été vérifiés !","text-burn-hint":"Attention de ne pas ouvrir cette URL vous-même, cela détruirait le secret. Fournissez-la à quelqu\'un d\'autre!","text-burn-time":"S\'il n\'a pas été vu auparavant, ce secret sera automatiquement supprimé :","text-hint-burned":"\u003cstrong\u003eAttention:\u003c/strong\u003e Vous ne pouvez consulter ce contenu qu\'une fois. Le secret sera détruit dès que vous rechargez la page, donc copiez le maintenant\u0026hellip;","text-invalid-files-selected":"Au moins l\'un des fichiers sélectionnés n\'est pas autorisé comme pièce-jointe.","text-max-filesize":"Taille maximum: {maxSize}","text-max-filesize-exceeded":"Le(s) fichier(s) que vous avez choisis sont trop volumineux pour être attachés : {curSize} / {maxSize}","text-powered-by":"Propulsé par","text-pre-reveal-hint":"Pour afficher le secret, cliquez sur ce bouton, mais soyez conscient que cela le détruira. Vous ne pouvez l\'afficher qu\'une fois!","text-pre-url":"Votre secret a été créé et stocké à cette URL:","text-secret-create-disabled":"La création de nouveaux secrets est désactivée dans ce cas.","title-explanation":"Voici comment ça fonctionne\u0026hellip;","title-new-secret":"Créer un nouveau secret","title-reading-secret":"Lecture du secret\u0026hellip;","title-secret-create-disabled":"Création secrète désactivée...","title-secret-created":"Secret créé!","tooltip-copy-to-clipboard":"Copier dans le presse-papiers","tooltip-download-as-file":"Télécharger en tant que fichier"}'),
'it': JSON.parse('{"alert-secret-not-found":"Questo non è il secret che stai cercando\u0026hellip; - Se ti aspettavi di vedere il secret allora potrebbe essere stato compromesso poichè qualcun altro potrebbe aver già aperto il link.","alert-something-went-wrong":"Qualcosa non ha funzionato. Mi dispiace davvero\u0026hellip;","btn-create-secret":"Crea il secret!","btn-create-secret-processing":"Creazione del secret in corso…","btn-new-secret":"Nuovo secret","btn-reveal-secret":"Mostrami il secret!","btn-reveal-secret-processing":"Decrittazione del secret in corso…","btn-show-explanation":"Come funziona?","expire-default":"Scadenza predefinita","expire-n-days":"{n} giorno | {n} giorni","expire-n-hours":"{n} ora | {n} ore","expire-n-minutes":"{n} minuto | {n} minuti","expire-n-seconds":"{n} secondo | {n} secondi","items-explanation":["Inserisci un secret nel campo di testo di questa pagina","Il tuo browser critta il secret usando una password generata in modo casuale","Solo il secret crittato viene inviato al server (nè il secret decrittato nè la password vengono mai inviati!)","Il server conserva il secret crittato per un certo periodo di tempo","Passi l\'URL visualizzato contenente l\'ID e la password di decrittazione al destinatario","Il destinatario può vedere il secret esattamente una sola volta: se non può, il secret potrebbe essere stato visto da qualcun altro!","Dopo che il secret crittato è stato visualizzato la prima volta, viene cancellato dal server"],"label-expiry":"Scade in:","label-secret-data":"Dati del secret:","label-secret-files":"Allega files:","text-attached-files":"Il mittente ha allegato alcuni files al secret. Assicurati di fidarti del mittente perchè i files non sono stati controllati!","text-burn-hint":"Per favore ricorda di non visitare questo URL perchè il secret verrebbe cancellato. Passalo semplicemente a qualcun altro!","text-burn-time":"Se non viene visualizzato prima, questo secret verrà cancellato automaticamente:","text-hint-burned":"\u003cstrong\u003eAttenzione:\u003c/strong\u003e Vedrai il secret solo questa volta. Non appena ricaricherai la pagina verrà cancellato, quindi magari copialo ora\u0026hellip;","text-invalid-files-selected":"Almeno uno dei files selezionati non è consentito come allegato.","text-max-filesize":"Dimensione massima: {maxSize}","text-max-filesize-exceeded":"Il/I file(s) che hai scelto ha/hanno una dimensione troppo grande per essere allegato/allegati: {curSize} / {maxSize}","text-powered-by":"Realizzato con","text-pre-reveal-hint":"Per rivelare il secret clicca su questo pulsante, ma attenzione perchè farlo lo cancellerà. Lo puoi vedere solo una volta!","text-pre-url":"Il tuo secret è stato creato e salvato usando questo URL:","text-secret-create-disabled":"La creazione di nuovi secrets è disabilitata su questa istanza.","title-explanation":"Questo è come funziona\u0026hellip;","title-new-secret":"Crea un nuovo secret","title-reading-secret":"Leggi il tuo secret\u0026hellip;","title-secret-create-disabled":"Creazione secret disabilitata…","title-secret-created":"Secret creato!","tooltip-copy-to-clipboard":"Copia nella clipboard","tooltip-download-as-file":"Scarica come file"}'),
'lv': JSON.parse('{"alert-secret-not-found":"\u003cstrong\u003eZiņa nav atrasta!\u003c/strong\u003e\u0026hellip; - Ja ievadītā saite ir pareiza, tad ir beidzies ziņas glabāšanas laiks, vai arī tā jau vienreiz ir atvērta.","alert-something-went-wrong":"Neparedzēta sistēmas kļūda. Atvainojiet par sagādātajām neērtībām\u0026hellip;","btn-create-secret":"Šifrēt ziņu!","btn-new-secret":"Jauna ziņa","btn-reveal-secret":"Atvērt ziņu!","btn-show-explanation":"Kā tas strādā?","expire-n-days":"{n} diena | {n} dienas","expire-n-hours":"{n} stunda | {n} stundas","expire-n-minutes":"{n} minūte | {n} minūtes","expire-n-seconds":"{n} sekundes | {n} sekundes","items-explanation":["Tu ievadi ziņu ievades laukā","Pārlūks nošifrē ziņu ar uzģenerētu paroli","Tikai šifrētā ziņa tiek nosūtīta serverim (nešifrētā ziņa un parole sūtīta netiek!)","Serveris noteiktu laiku glabā šifrēto ziņu","Tu nodod URL ar ziņas ID un atšifrēšanas paroli saņēmējam","Saņēmējs var atvērt ziņu tikai vienreiz: ja tas neizdodas, iespējams, ziņu jau atvēris kāds cits!","Kad ziņa tiek atvērta pirmo reizi, tā no servera tiek dzēsta"],"label-secret-data":"Ziņa:","text-burn-hint":"Lūdzu atceries neatvērt saiti pats, jo tad ziņa tiks dzēsta. Nodod saiti ziņas saņēmējam!","text-hint-burned":"\u003cstrong\u003eUzmanību:\u003c/strong\u003e Ziņa tiek parādīta tikai vienu reizi. Līdzko lapa tiks pārlādēta, ziņa būs neatgriezeniski zaudēta, tāpēc nepieciešamības gadījumā nokopē to tagad\u0026hellip;","text-powered-by":"Darbina","text-pre-reveal-hint":"Lai parādītu ziņu nospied šo pogu, bet rēķinies ar to, ka pēc apskates ziņa vairs nebūs pieejama. To var atvērt tikai vienreiz!","text-pre-url":"Ziņa ir nošifrēta un ir atverama šajā adresē:","text-secret-create-disabled":"Šajā gadījumā jaunu noslēpumu izveide ir atspējota.","title-explanation":"Tā tas strādā\u0026hellip;","title-new-secret":"Šifrēt ziņu","title-reading-secret":"Atver ziņu\u0026hellip;","title-secret-create-disabled":"Slepena izveide atspējota...","title-secret-created":"Ziņa nošifrēta!"}'),
'nl': JSON.parse('{"alert-secret-not-found":"De gegevens die je zocht bestaan niet (meer)\u0026hellip; - Als je hier informatie verwachtte dan is de link mogelijk al door iemand anders bekeken!","alert-something-went-wrong":"Er ging iets verkeerd, sorry\u0026hellip;","btn-create-secret":"Nieuwe vertrouwelijke info aanmaken!","btn-create-secret-processing":"Vertrouwelijke info wordt versleuteld...","btn-new-secret":"Nieuw","btn-reveal-secret":"Toon mij de vertrouwelijke info!","btn-reveal-secret-processing":"Vertrouwelijke info wordt ontsleuteld...","btn-show-explanation":"Hoe werkt dit?","expire-default":"Standaard termijn","expire-n-days":"{n} dag | {n} dagen","expire-n-hours":"{n} uur | {n} uur","expire-n-minutes":"{n} minuut | {n} minuten","expire-n-seconds":"{n} seconde | {n} seconden","items-explanation":["Je vult vertrouwelijke informatie in op deze pagina.","Je browser versleutelt de ingevulde tekst via een automatisch gegenereerd wachtwoord.","Alleen de versleutelde data wordt naar de server gestuurd. (De leesbare versie of het wachtwoord worden nooit verstuurd!)","De server slaat de versleutelde data gedurende een beperkte periode op.","Je geeft de URL met identificatie en het gegenereerde wachtwoord aan de ontvanger.","De ontvanger kan de vertrouwelijke informatie exact eenmaal bekijken: indien het niet lukt heeft mogelijk iemand anders de info gezien!","De versleutelde data wordt van de server gewist van zodra de ontvanger het bekeken heeft."],"label-expiry":"Verwijder na:","label-secret-data":"Vertrouwelijke info:","label-secret-files":"Bestanden toevoegen:","text-attached-files":"De afzender heeft bestanden toegevoegd. Deze werden niet gecontroleerd, gebruik deze enkel als je de afzender vertrouwt!","text-burn-hint":"Bezoek de URL niet zelf: je kan deze slechts eenmaal gebruiken. Geef de URL aan de ontvanger.","text-burn-time":"Deze vertrouwelijke informatie wordt automatisch gewist indien niet bekeken voor:","text-hint-burned":"\u003cstrong\u003eOpgelet:\u003c/strong\u003e Je ziet deze informatie alleen nu. Je kan het niet meer opnieuw opvragen als je de pagina verlaat.","text-max-filesize":"Maximum grootte: {maxSize}","text-max-filesize-exceeded":"De bestanden die je toevoegde zijn te groot: {curSize} / {maxSize}","text-powered-by":"Mogelijk gemaakt door","text-pre-reveal-hint":"Gebruik deze knop om de vertrouwelijke info op te halen. Let op: Je kan dit slechts eenmaal doen!","text-pre-url":"Je vertrouwelijke informatie kan opgevraagd worden via deze URL:","text-secret-create-disabled":"Het aanmaken van nieuwe geheimen is in dit geval uitgeschakeld.","title-explanation":"Dit is hoe het werkt\u0026hellip;","title-new-secret":"Nieuwe vertrouwelijke info opslaan","title-reading-secret":"Vertrouwelijke info lezen\u0026hellip;","title-secret-create-disabled":"Geheime creatie uitgeschakeld...","title-secret-created":"Vertrouwelijke info opgeslaan!"}'),
'nl': JSON.parse('{"alert-secret-not-found":"De gegevens die je zocht bestaan niet (meer)\u0026hellip; - Als je hier informatie verwachtte dan is de link mogelijk al door iemand anders bekeken!","alert-something-went-wrong":"Er ging iets verkeerd, sorry\u0026hellip;","btn-create-secret":"Geheim aanmaken!","btn-create-secret-processing":"Geheim wordt aangemaakt...","btn-new-secret":"Nieuw","btn-reveal-secret":"Toon mij het geheim!","btn-reveal-secret-processing":"Geheim wordt ontsleuteld...","btn-show-explanation":"Hoe werkt dit?","expire-default":"Standaard vervaltermijn","expire-n-days":"{n} dag | {n} dagen","expire-n-hours":"{n} uur | {n} uur","expire-n-minutes":"{n} minuut | {n} minuten","expire-n-seconds":"{n} seconde | {n} seconden","items-explanation":["Je vult vertrouwelijke informatie in op deze pagina.","Je browser versleutelt de ingevulde tekst via een automatisch gegenereerd wachtwoord.","Alleen de versleutelde data wordt naar de server gestuurd (de onversleutelde data of het wachtwoord worden nooit verstuurd!)","De server slaat de versleutelde data voor een bepaalde periode op","Je geeft de URL met ID en het gegenereerde wachtwoord aan de ontvanger.","De ontvanger kan de vertrouwelijke informatie exact eenmaal bekijken: indien het niet lukt heeft mogelijk iemand anders de info gezien!","Nadat het versleutelde geheim eenmalig bekeken is, wordt deze van de server verwijderd"],"label-expiry":"Verwijder na:","label-secret-data":"Vertrouwelijke info:","label-secret-files":"Bestanden toevoegen:","text-attached-files":"De afzender heeft bestanden toegevoegd. Deze zijn niet gecontroleerd, gebruik deze enkel als je de afzender vertrouwt!","text-burn-hint":"Open de URL niet zelf, deze is slechts eenmalig te gebruiken. Geef de URL aan de ontvanger.","text-burn-time":"Deze vertrouwelijke informatie wordt automatisch gewist indien niet bekeken voor:","text-hint-burned":"\u003cstrong\u003eOpgelet:\u003c/strong\u003e Je ziet deze informatie alleen nu. Je kan het niet meer opnieuw opvragen als je de pagina verlaat.","text-max-filesize":"Maximum grootte: {maxSize}","text-max-filesize-exceeded":"De bestanden die je toevoegde zijn te groot: {curSize} / {maxSize}","text-powered-by":"Mogelijk gemaakt door","text-pre-reveal-hint":"Gebruik deze knop om het geheim weer te geven. Let op: Je kan dit slechts eenmaal doen!","text-pre-url":"Het geheim kan opgevraagd worden via deze URL:","text-secret-create-disabled":"Het aanmaken van nieuwe geheimen is in deze omgeving uitgeschakeld.","title-explanation":"Dit is hoe het werkt\u0026hellip;","title-new-secret":"Nieuw geheim aanmaken","title-reading-secret":"Geheim wordt gelezen\u0026hellip;","title-secret-create-disabled":"Aanmaken geheimen uitgeschakeld...","title-secret-created":"Geheim aangemaakt!"}'),
'pl': JSON.parse('{"alert-secret-not-found":"To nie jest sekret, którego szukasz\u0026hellip; - Jeśli spodziewałeś się tu sekretu, to może być on zagrożony, ponieważ ktoś inny mógł już otworzyć ten link.","alert-something-went-wrong":"Coś poszło nie tak. Bardzo mi przykro\u0026hellip;","btn-create-secret":"Stwórz sekret!","btn-create-secret-processing":"Sekret jest tworzony...","btn-new-secret":"Nowy sekret","btn-reveal-secret":"Pokaż mi sekret!","btn-reveal-secret-processing":"Sekret jest odszyfrowywany...","btn-show-explanation":"Jak to działa?","expire-default":"Domyślne wygasanie","expire-n-days":"{n} dzień | {n} dni","expire-n-hours":"{n} godzina | {n} godzin(y)","expire-n-minutes":"{n} minuta | {n} minut(y)","expire-n-seconds":"{n} sekunda | {n} sekund(y)","items-explanation":["Wpisujesz sekret w pole na tej stronie","Twoja przeglądarka szyfruje sekret korzystając z wygenerowanego hasła","Tylko zaszyfrowany sekret jest wysyłany na serwer (ani czysty sekret, ani hasło nie są nigdy wysyłane!)","Serwer przechowuje zaszyfrowany sekret przez określony czas","Przekazujesz wyświetlony adres URL zawierający identyfikator sekretu oraz hasło deszyfrujące do odbiorcy","Odbiorca może zobaczyć sekret tylko raz: jeżeli nie może tego zrobić, to sekret mógł być już wyświetlony przez kogoś innego!","Po jednorazowym odczytaniu zaszyfrowanego sekretu, jest on usuwany z serwera"],"label-expiry":"Wygasa po:","label-secret-data":"Dane sekretu:","label-secret-files":"Dołącz pliki:","text-attached-files":"Nadawca dołączył pliki do tego skeretu. Upewnij się, że ufasz nadawcy, bo pliki nie są sprawdzane!","text-burn-hint":"Pamiętaj, aby nie przechodzić na ten adres URL samemu, ponieważ to zniszczy sekret. Po prostu przekaż go odbiorcy!","text-burn-time":"Jeżeli nie zostanie wyświetlony, ten sekret zostanie automatycznie usunięty:","text-hint-burned":"\u003cstrong\u003eUwaga:\u003c/strong\u003e Zobaczysz to tylko raz. Gdy odświeżysz stronę, to sekret nie będzie już dostępny, więc lepiej skopiuj go teraz\u0026hellip;","text-max-filesize":"Maksymalny rozmiar: {maxSize}","text-max-filesize-exceeded":"Wybrane załączniki przekraczają makrymalny rozmiar: {curSize} / {maxSize}","text-powered-by":"Obsługiwane przez","text-pre-reveal-hint":"Aby odsłonić sekret, naciśnij ten przycisk, jednak wiedz, że to zniszczy sekret. Możesz go zobaczyć tylko raz!","text-pre-url":"Twój sekret został stworzony i zachowany pod tym adresem URL:","text-secret-create-disabled":"Tworzenie nowych sekretów jest wyłączone na tej instancji.","title-explanation":"Oto, jak to działa\u0026hellip;","title-new-secret":"Stwórz nowy sekret","title-reading-secret":"Odczytywanie Twojego sekretu\u0026hellip;","title-secret-create-disabled":"Tworzenie sekretów wyłączone\u0026hellip;","title-secret-created":"Sekret utworzony!"}'),
'nl-BE': JSON.parse('{"alert-secret-not-found":"De gegevens die je zocht bestaan niet (meer)\u0026hellip; - Als je hier informatie verwachtte dan is de link mogelijk al door iemand anders bekeken!","alert-something-went-wrong":"Er ging iets verkeerd, sorry\u0026hellip;","btn-create-secret":"Nieuwe vertrouwelijke info aanmaken!","btn-create-secret-processing":"Vertrouwelijke info wordt versleuteld...","btn-new-secret":"Nieuw","btn-reveal-secret":"Toon mij de vertrouwelijke info!","btn-reveal-secret-processing":"Vertrouwelijke info wordt ontsleuteld...","btn-show-explanation":"Hoe werkt dit?","expire-default":"Standaard termijn","expire-n-days":"{n} dag | {n} dagen","expire-n-hours":"{n} uur | {n} uur","expire-n-minutes":"{n} minuut | {n} minuten","expire-n-seconds":"{n} seconde | {n} seconden","items-explanation":["Je vult vertrouwelijke informatie in op deze pagina.","Je browser versleutelt de ingevulde tekst via een automatisch gegenereerd wachtwoord.","Alleen de versleutelde data wordt naar de server gestuurd. (De leesbare versie of het wachtwoord worden nooit verstuurd!)","De server slaat de versleutelde data gedurende een beperkte periode op.","Je geeft de URL met identificatie en het gegenereerde wachtwoord aan de ontvanger.","De ontvanger kan de vertrouwelijke informatie exact eenmaal bekijken: indien het niet lukt heeft mogelijk iemand anders de info gezien!","De versleutelde data wordt van de server gewist van zodra de ontvanger het bekeken heeft."],"label-expiry":"Verwijder na:","label-secret-data":"Vertrouwelijke info:","label-secret-files":"Bestanden toevoegen:","text-attached-files":"De afzender heeft bestanden toegevoegd. Deze werden niet gecontroleerd, gebruik deze enkel als je de afzender vertrouwt!","text-burn-hint":"Bezoek de URL niet zelf: je kan deze slechts eenmaal gebruiken. Geef de URL aan de ontvanger.","text-burn-time":"Deze vertrouwelijke informatie wordt automatisch gewist indien niet bekeken voor:","text-hint-burned":"\u003cstrong\u003eOpgelet:\u003c/strong\u003e Je ziet deze informatie alleen nu. Je kan het niet meer opnieuw opvragen als je de pagina verlaat.","text-max-filesize":"Maximum grootte: {maxSize}","text-max-filesize-exceeded":"De bestanden die je toevoegde zijn te groot: {curSize} / {maxSize}","text-powered-by":"Mogelijk gemaakt door","text-pre-reveal-hint":"Gebruik deze knop om de vertrouwelijke info op te halen. Let op: Je kan dit slechts eenmaal doen!","text-pre-url":"Je vertrouwelijke informatie kan opgevraagd worden via deze URL:","text-secret-create-disabled":"Het aanmaken van nieuwe geheimen is in dit geval uitgeschakeld.","title-explanation":"Dit is hoe het werkt\u0026hellip;","title-new-secret":"Nieuwe vertrouwelijke info opslaan","title-reading-secret":"Vertrouwelijke info lezen\u0026hellip;","title-secret-create-disabled":"Geheime creatie uitgeschakeld...","title-secret-created":"Vertrouwelijke info opgeslaan!"}'),
'pl': JSON.parse('{"alert-insecure-environment":"Odwiedzasz tę instancję przez niezabezpieczone połączenie. Nie będziesz mógł tworzyć ani odczytywać sekretów.","alert-secret-not-found":"To nie jest sekret, którego szukasz\u0026hellip; - Jeśli spodziewałeś się tu sekretu, to może być on zagrożony, ponieważ ktoś inny mógł już otworzyć ten link.","alert-something-went-wrong":"Coś poszło nie tak. Bardzo mi przykro\u0026hellip;","btn-create-secret":"Stwórz sekret!","btn-create-secret-processing":"Sekret jest tworzony...","btn-new-secret":"Nowy sekret","btn-reveal-secret":"Pokaż mi sekret!","btn-reveal-secret-processing":"Sekret jest odszyfrowywany...","btn-show-explanation":"Jak to działa?","expire-default":"Domyślne wygasanie","expire-n-days":"{n} dzień | {n} dni","expire-n-hours":"{n} godzina | {n} godzin(y)","expire-n-minutes":"{n} minuta | {n} minut(y)","expire-n-seconds":"{n} sekunda | {n} sekund(y)","items-explanation":["Wpisujesz sekret w pole na tej stronie","Twoja przeglądarka szyfruje sekret korzystając z wygenerowanego hasła","Tylko zaszyfrowany sekret jest wysyłany na serwer (ani czysty sekret, ani hasło nie są nigdy wysyłane!)","Serwer przechowuje zaszyfrowany sekret przez określony czas","Przekazujesz wyświetlony adres URL zawierający identyfikator sekretu oraz hasło deszyfrujące do odbiorcy","Odbiorca może zobaczyć sekret tylko raz: jeżeli nie może tego zrobić, to sekret mógł być już wyświetlony przez kogoś innego!","Po jednorazowym odczytaniu zaszyfrowanego sekretu, jest on usuwany z serwera"],"label-expiry":"Wygasa po:","label-secret-data":"Dane sekretu:","label-secret-files":"Dołącz pliki:","text-attached-files":"Nadawca dołączył pliki do tego sekretu. Upewnij się, że ufasz nadawcy, bo pliki nie są sprawdzane!","text-burn-hint":"Pamiętaj, aby nie przechodzić na ten adres URL samemu, ponieważ to zniszczy sekret. Po prostu przekaż go odbiorcy!","text-burn-time":"Jeżeli nie zostanie wyświetlony, ten sekret zostanie automatycznie usunięty:","text-hint-burned":"\u003cstrong\u003eUwaga:\u003c/strong\u003e Zobaczysz to tylko raz. Gdy odświeżysz stronę, to sekret nie będzie już dostępny, więc lepiej skopiuj go teraz\u0026hellip;","text-invalid-files-selected":"Co najmniej jeden z załączonych plików nie jest dopuszczalny jako załącznik.","text-max-filesize":"Maksymalny rozmiar: {maxSize}","text-max-filesize-exceeded":"Wybrane załączniki przekraczają maksymalny rozmiar: {curSize} / {maxSize}","text-powered-by":"Obsługiwane przez","text-pre-reveal-hint":"Aby odsłonić sekret, naciśnij ten przycisk, jednak wiedz, że to zniszczy sekret. Możesz go zobaczyć tylko raz!","text-pre-url":"Twój sekret został stworzony i zachowany pod tym adresem URL:","text-secret-burned":"Sekret został pomyślnie zniszczony.","text-secret-create-disabled":"Tworzenie nowych sekretów jest wyłączone na tej instancji.","title-explanation":"Oto, jak to działa\u0026hellip;","title-new-secret":"Stwórz nowy sekret","title-reading-secret":"Odczytywanie Twojego sekretu\u0026hellip;","title-secret-create-disabled":"Tworzenie sekretów wyłączone\u0026hellip;","title-secret-created":"Sekret utworzony!","tooltip-burn-secret":"Zniszcz sekret teraz!","tooltip-copy-to-clipboard":"Skopiuj do schowka","tooltip-download-as-file":"Pobierz jako plik"}'),
'pt-BR': JSON.parse('{"alert-secret-not-found":"Esta não é o segredo que você está procurando… - Se você esperava que o segredo estaria aqui, ele pode ter sido comprometido por alguém que já acessou o link.","alert-something-went-wrong":"Desculpe, algo deu errado…","btn-create-secret":"Criar segredo!","btn-new-secret":"Novo segredo","btn-reveal-secret":"Mostrar o segredo!","btn-show-explanation":"Como funciona?","expire-n-days":"{n} dia | {n} dias","expire-n-hours":"{n} hora | {n} horas","expire-n-minutes":"{n} minutos | {n} minutos","expire-n-seconds":"{n} segundos | {n} segundos","items-explanation":["Você insere o segredo no campo de texto desta página","Seu navegador criptografa o segredo usando uma senha gerada","Somente o segredo criptografado é enviado para o servidor (nem o segredo em texto claro, nem a senha é enviada para o servidor!)","O servidor armazena o segredo criptografado por um certo tempo","Você envia a URL mostrada contendo a ID e a senha de descriptografia para o destinatário","O destinatário pode ver o segredo apenas uma vez: se ele não pode ver, o segredo pode ter sido visto por outra pessoa!","Após o segredo ter sido obtido uma vez, o mesmo é deletado do servidor"],"label-secret-data":"Informação secreta:","text-burn-hint":"Importante você lembrar de não acessar esta URL, pois isto irá indisponibilizar o segredo. Apenas encaminhe para outra pessoa!","text-hint-burned":"Atenção: Você está vendo esta informação apenas uma vez. Logo que você recarregar a página o segredo ficará indisponível. É recomendado que você copie a informação agora…","text-powered-by":"Powered by","text-pre-reveal-hint":"Para revelar o segredo clique neste botão, mas lembre-se que esta ação vai destruir o segredo. Você só pode ver uma única vez!","text-pre-url":"Seu segredo foi criado e armazenado na seguinte URL:","text-secret-create-disabled":"A criação de novos segredos é desativada nesse caso.","title-explanation":"É assim como funciona…","title-new-secret":"Criar um novo segredo","title-reading-secret":"Lendo seu segredo…","title-secret-create-disabled":"Criação secreta desativada...","title-secret-created":"Segredo criado!"}'),
'ru': JSON.parse('{"alert-secret-not-found":"Секрет недоступен\u0026hellip; - Помните, он может быть скомпрометирован. Возможно кто-то другой уже открыл вашу ссылку.","alert-something-went-wrong":"Что-то пошло не так. Приносим свои извинения\u0026hellip;","btn-create-secret":"Создать секрет!","btn-create-secret-processing":"Секрет создаётся ...","btn-new-secret":"Новый секрет","btn-reveal-secret":"Показать секрет!","btn-reveal-secret-processing":"Секрет декодируется ...","btn-show-explanation":"Как это работает?","expire-default":"Значение по умолчанию до исчезновения","expire-n-days":"{n} день | {n} дней","expire-n-hours":"{n} час | {n} часов","expire-n-minutes":"{n} минут | {n} минут","expire-n-seconds":"{n} секунда | {n} секунд","items-explanation":["Вы вводите секрет в поле на этой странице.","Ваш браузер шифрует секрет с помощью сгенерированного пароля.","На сервер отправляется только зашифрованный секрет (ни текст секрета, ни пароль никогда не отправляются!)","Сервер хранит зашифрованный секрет в течение определенного времени.","Вы передаете отображаемый URL-адрес, содержащий идентификатор и пароль для расшифровки, получателю.","Получатель может просмотреть секрет ровно один раз: если он не смог, секрет возможно был просмотрен кем-то другим!","После того как зашифрованный секрет был извлечен, он удаляется с сервера."],"label-expiry":"Исчезнет через:","label-secret-data":"Секретные данные:","label-secret-files":"Прикреплённые файлы:","text-attached-files":"Отправитель прикрепил файлы к секрету. Убедитесь, что вы доверяете отправителю, так как файлы не были проверены!","text-burn-hint":"Пожалуйста, не переходите по этому URL для проверки, так как это удалит секрет. Просто скопируйте и передайте его!","text-burn-time":"Если этот секрет не был просмотрен ранее, он будет автоматически удален:","text-hint-burned":"\u003cstrong\u003eВнимание:\u003c/strong\u003e Секрет будет показан только один раз. Как только вы перезагрузите страницу, секрет исчезнет, скопируйте его незамедлительно\u0026hellip;","text-max-filesize":"Максимальный размер: {maxSize}","text-max-filesize-exceeded":"Ваш файл(ы) слишком большой для прикрепления к секрету: {curSize} / {maxSize}","text-powered-by":"Основан","text-pre-reveal-hint":"Чтобы раскрыть секрет, нажмите эту кнопку, но имейте в виду, что это приведет к уничтожению секрета. Вы можете просмотреть его только один раз!","text-pre-url":"Ваш секрет создан и сохранён, его URL:","text-secret-create-disabled":"Создание новых секретов в этом случае отключено.","title-explanation":"Как это работает\u0026hellip;","title-new-secret":"Создать новый секрет","title-reading-secret":"Читаем ваш секрет\u0026hellip;","title-secret-create-disabled":"Секретное создание отключено...","title-secret-created":"Секрет создан!"}'),
'sv': JSON.parse('{"alert-secret-not-found":"Hemlighet hittades inte\u0026hellip; - Om du förväntade dig att hemligheten skulle finnas här kan den vara röjd då någon annan kan ha öppnat denna länk tidigare.","alert-something-went-wrong":"Något gick fel. Jag ber om ursäkt för detta!\u0026hellip;","btn-create-secret":"Skapa hemliget!","btn-create-secret-processing":"Hemlighet håller på att skapas..","btn-new-secret":"Ny hemlighet.","btn-reveal-secret":"Visa mig hemligheten!","btn-reveal-secret-processing":"Hemlighet håller på att dekrypteras..","btn-show-explanation":"Hur fungerar detta?","expire-default":"Standard utgångstid","expire-n-days":"{n} dag | {n} dagar","expire-n-hours":"{n} timme | {n} timmar","expire-n-minutes":"{n} minut | {n} minuter","expire-n-seconds":"{n} sekund | {n} sekunder","items-explanation":["Skriv in en hemlighet i rutan nedan","Din webbläsare krypterar hemligheten med hjälp av ett genererat lösenord","Endast den krypterade hemligheten skickas till servern. (varken lösenordet eller hemligheten i klartext skickas!)","Servern lagrar den krypterade hemligheten för en begränsad tid","Du skickar URL-länken med ID-numret och avkrypteringslösenordet till mottagaren","Mottagaren kan se hemligheten exakt en gång: Om detta misslyckas kan hemligheten redan ha setts av någon annan!","När hemligheten har setts en gång, raderas den från servern"],"label-expiry":"Expirerar om:","label-secret-data":"Hemlig data:","label-secret-files":"Bifoga filer:","text-attached-files":"Avsändaren har bifogat filer till denna hemlighet. Se till att du litar på avsändaren, eftersom filerna inte har kontrollerats!","text-burn-hint":"Kom ihåg att inte gå till denna URL själv eftersom detta skulle förbruka hemligheten. Skicka bara vidare den till mottagaren!","text-burn-time":"Om hemligheten inte visas innan, kommer den att förbrukas automatiskt:","text-hint-burned":"\u003cstrong\u003eObservera:\u003c/strong\u003e Du kan endast se denna sida en gång. Så fort du laddar om sidan kommer hemligheten att försvinna så kopiera den nu\u0026hellip;","text-max-filesize":"Maximal storlek: {maxSize}","text-max-filesize-exceeded":"Filerna du valt är för stora för att kunna bifogas: {curSize} / {maxSize}","text-powered-by":"Drivs av","text-pre-reveal-hint":"För att visa hemligheten klicka på denna knapp. Var medveten om att när du gör det kommer hemligheten att förbrukas, du kan endast se den en gång!","text-pre-url":"Din hemlighet har skapats och lagrats med denna URL:","text-secret-create-disabled":"Skapandet av nya hemligheter blockeras i detta fall.","title-explanation":"Såhär fungerar det\u0026hellip;","title-new-secret":"Skapa ny hemlighet","title-reading-secret":"Läs din hemlighet\u0026hellip;","title-secret-create-disabled":"Hemlig skapelse avaktiverad...","title-secret-created":"Hemlighet skapad!"}'),
'sv': JSON.parse('{"alert-secret-not-found":"Hemlighet hittades inte\u0026hellip; - Om du förväntade dig att hemligheten skulle finnas här kan den vara röjd då någon annan kan ha öppnat denna länk tidigare.","alert-something-went-wrong":"Något gick fel. Jag ber om ursäkt för detta!\u0026hellip;","btn-create-secret":"Skapa hemlighet!","btn-create-secret-processing":"Hemlighet håller på att skapas..","btn-new-secret":"Ny hemlighet.","btn-reveal-secret":"Visa mig hemligheten!","btn-reveal-secret-processing":"Hemlighet håller på att dekrypteras..","btn-show-explanation":"Hur fungerar detta?","expire-default":"Standard utgångstid","expire-n-days":"{n} dag | {n} dagar","expire-n-hours":"{n} timme | {n} timmar","expire-n-minutes":"{n} minut | {n} minuter","expire-n-seconds":"{n} sekund | {n} sekunder","items-explanation":["Skriv in en hemlighet i rutan nedan","Din webbläsare krypterar hemligheten med hjälp av ett genererat lösenord","Endast den krypterade hemligheten skickas till servern. (varken lösenordet eller hemligheten i klartext skickas!)","Servern lagrar den krypterade hemligheten för en begränsad tid","Du skickar URL-länken med ID-numret och avkrypteringslösenordet till mottagaren","Mottagaren kan se hemligheten exakt en gång: Om detta misslyckas kan hemligheten redan ha setts av någon annan!","När hemligheten har setts en gång, raderas den från servern"],"label-expiry":"Expirerar om:","label-secret-data":"Hemlig data:","label-secret-files":"Bifoga filer:","text-attached-files":"Avsändaren har bifogat filer till denna hemlighet. Se till att du litar på avsändaren, eftersom filerna inte har kontrollerats!","text-burn-hint":"Kom ihåg att inte gå till denna URL själv eftersom detta skulle förbruka hemligheten. Skicka bara vidare den till mottagaren!","text-burn-time":"Om hemligheten inte visas innan, kommer den att förbrukas automatiskt:","text-hint-burned":"\u003cstrong\u003eObservera:\u003c/strong\u003e Du kan endast se denna sida en gång. Så fort du laddar om sidan kommer hemligheten att försvinna så kopiera den nu\u0026hellip;","text-max-filesize":"Maximal storlek: {maxSize}","text-max-filesize-exceeded":"Filerna du valt är för stora för att kunna bifogas: {curSize} / {maxSize}","text-powered-by":"Drivs av","text-pre-reveal-hint":"För att visa hemligheten klicka på denna knapp. Var medveten om att när du gör det kommer hemligheten att förbrukas, du kan endast se den en gång!","text-pre-url":"Din hemlighet har skapats och lagrats med denna URL:","text-secret-create-disabled":"Skapandet av nya hemligheter blockeras i detta fall.","title-explanation":"Såhär fungerar det\u0026hellip;","title-new-secret":"Skapa ny hemlighet","title-reading-secret":"Läs din hemlighet\u0026hellip;","title-secret-create-disabled":"Skapande av hemlighet avaktiverat...","title-secret-created":"Hemlighet skapad!"}'),
'tr': JSON.parse('{"alert-secret-not-found":"Aradığınız sır bu değil… - Sırrın burada olmasını bekliyorsanız, bu link başkası tarafından açılmış ve sırrınız tehlikede olabilir.","alert-something-went-wrong":"Bir şeyler ters gitti. Bunun için çok üzgünüm…","btn-create-secret":"Sır oluştur!","btn-new-secret":"Yeni sır","btn-reveal-secret":"Sırrı göster!","btn-show-explanation":"Nasıl çalışır?","expire-n-days":"{n} gün | {n} gün","expire-n-hours":"{n} saat | {n} saat","expire-n-minutes":"{n} dakika | {n} dakika","expire-n-seconds":"{n} saniye | {n} saniye","items-explanation":["Bu sayfadaki alana sırrınızı giriniz","Internet tarayıcınız oluşturulan şifre yardımı ile sırrınızı enkripte eder","Sadece ektripte edilmiş sır sunucuya gönderilir (ne sır metni nede şifre gönderilmez!)","Sunucu enkripte edilmiş sırrı bünyesinde belli bir süre saklar","Gösterilen linki, id ve deşifre bilgisi ile birlikte alıcıya gönder","Alcı sırrı tam olarak sadece bir kez görebilir: Eğer göremez ise, sır bir başkası tarafından daha önce görülmüş olabilir!","Sır bir kez gösterildikten sonra hemen sunucudan silinir"],"label-secret-data":"Sır bilgisi:","text-burn-hint":"Lütfen linki kendiniz acmayın, bu sırrın silinmesine neden olur. Linki sadece alıcıya gönderin!","text-hint-burned":"Dikkat: Bunu sadece bir kez göreceksiniz. Sayfayı güncellediğinizde yada kapattiğınızda sır kaybolacaktır, belkide şimdi sırrı kopyalamanız akıllıca olacaktır…","text-powered-by":"Tarafından desteklenmektedir","text-pre-reveal-hint":"Sırrı görmek için bu düğmeye tıklayın, ama bunu yaptıktan sonra sırrın silineceğini unutmayın. Bunu sadece bir kez görebilirsin!","text-pre-url":"Sırrınız oluşturuldu ve bu link kullanılarak kaydedildi:","text-secret-create-disabled":"Bu durumda yeni gizli dizilerin oluşturulması devre dışı bırakılır.","title-explanation":"Bu sekilde çalışır…","title-new-secret":"Yeni sır oluştur","title-reading-secret":"Sırrınız okunuyor…","title-secret-create-disabled":"Gizli yaratım devre dışı bırakıldı...","title-secret-created":"Sır oluşturuldu!"}'),
'uk': JSON.parse('{"alert-secret-not-found":"Це не секрет, який ви шукаєте\u0026hellip; - Якщо ви очікували, що секрет буде тут, він міг бути скомпрометований, оскільки хтось інший міг уже відкрити посилання.","alert-something-went-wrong":"Щось пішло не так. Ми дуже шкодуємо про це\u0026hellip;","btn-create-secret":"Створіть секрет!","btn-create-secret-processing":"Секрет створюється...","btn-new-secret":"Новий секрет","btn-reveal-secret":"Показати мені секрет!","btn-reveal-secret-processing":"Секрет розшифровується…","btn-show-explanation":"Як це працює?","expire-default":"Термін дії за замовчуванням","expire-n-days":"{n} день | {n} днів","expire-n-hours":"{n} година | {n} годин","expire-n-minutes":"{n} хвилина | {n} хвилин","expire-n-seconds":"{n} секунда | {n} секунд","items-explanation":["Уведіть секрет у поле на цій сторінці","Ваш браузер шифрує секрет за допомогою згенерованого пароля","На сервер надсилається лише зашифрований секрет (ані простий секрет, ані пароль ніколи не надсилаються!)","Сервер певний час зберігає зашифрований секрет","Ви передаєте одержувачу відображену URL-адресу, яка містить ідентифікатор і пароль для розшифровки","Одержувач може переглянути секрет лише один раз: якщо він не може, секрет міг переглянути хтось інший!","Після того, як зашифрований секрет було отримано один раз, він видаляється з сервера"],"label-expiry":"Термін дії закінчується:","label-secret-data":"Секретні дані:","label-secret-files":"Прикріпити файли:","text-attached-files":"Відправник прикріпив файли до секрету. Переконайтеся, що ви довіряєте відправнику, оскільки файли не перевірялися!","text-burn-hint":"Пам’ятайте, що не переходьте за цією URL-адресою самостійно, оскільки це знищить секрет. Просто передайте це комусь іншому!","text-burn-time":"Якщо раніше його не переглядати, цей секрет буде автоматично видалено:","text-hint-burned":"\u003cstrong\u003eУвага:\u003c/strong\u003e Ви бачите це лише раз. Щойно ви перезавантажите сторінку, секрет зникне, тому, можливо, скопіюйте його зараз\u0026hellip;","text-max-filesize":"Максимальний розмір: {maxSize}","text-max-filesize-exceeded":"Вибрані вами файли завеликі, щоб вкласти: {curSize} / {maxSize}","text-powered-by":"Powered by","text-pre-reveal-hint":"Щоб розкрити секрет, натисніть цю кнопку, але майте на увазі, що це знищить секрет. Ви можете переглянути його лише один раз!","text-pre-url":"Ваш секрет було створено та збережено за допомогою цієї URL-адреси:","text-secret-create-disabled":"Створення нових секретів у цьому екзепмлярі вимкнено.","title-explanation":"Ось як це працює\u0026hellip;","title-new-secret":"Створіть новий секрет","title-reading-secret":"Читання вашого секрету\u0026hellip;","title-secret-create-disabled":"Створення секрету вимкнено…","title-secret-created":"Секрет створений!"}'),
'zh': JSON.parse('{"alert-secret-not-found":"不存在该机密 - 如果这不符合您的预期,那么该链接可能已经泄露,且您的机密已经被他人查看过了。","alert-something-went-wrong":"运行异常,对此我深感抱歉…","btn-create-secret":"创建机密","btn-create-secret-processing":"机密正在创建中…","btn-new-secret":"新的机密","btn-reveal-secret":"向我展示机密","btn-reveal-secret-processing":"机密正在解密中…","btn-show-explanation":"它如何运作?","expire-default":"默认过期时间","expire-n-days":"{n} 天 | {n} 天","expire-n-hours":"{n} 小时 | {n} 小时","expire-n-minutes":"{n} 分钟 | {n} 分钟","expire-n-seconds":"{n} 秒 | {n} 秒","items-explanation":["您在当前页面的文本框内输入机密内容","您的浏览器会生成随机密钥并对机密内容进行加密","您的浏览器会发送加密后的机密内容到服务器(明文内容和随机密钥都不会被发送!)","服务器将接收加密的机密并在指定的时间段内保留(过期会自动销毁)","您需要将包含 ID 和随机密钥的访问链接发送给收件人","收件人仅有一次查看机密的机会(如果无法查看,则表示机密已被其他人查看过!)","服务器会在机密被查看一次后立即将其销毁"],"label-expiry":"过期时间:","label-secret-data":"机密内容:","label-secret-files":"附加文件:","text-attached-files":"发件人在此机密中附加了文件,但该文件未经检查,请确保发件人可信!","text-burn-hint":"请您记住不要自己访问此链接,因为这会导致机密被销毁。请原封不动的转发给收件人吧!","text-burn-time":"如果机密没有被查看,将会在此时间自动销毁:","text-hint-burned":"\u003cstrong\u003e注意\u003c/strong\u003e您只能查看一次机密请尽快拷贝刷新页面也会导致机密消失","text-max-filesize":"附加文件大小上限:{maxSize}","text-max-filesize-exceeded":"您选择的文件过大,无法附加:{curSize} / {maxSize}","text-powered-by":"Powered by","text-pre-reveal-hint":"点击按钮即可查看机密,但请注意该操作会立即销毁机密,而且您也只能查看一次!","text-pre-url":"您的机密已创建,访问链接如下:","text-secret-create-disabled":"该实例被禁止创建机密。","title-explanation":"这就是它的运作原理…","title-new-secret":"创建一个机密","title-reading-secret":"查看您的机密…","title-secret-create-disabled":"创建机密被禁止…","title-secret-created":"机密已创建!"}'),
'zh': JSON.parse('{"alert-secret-not-found":"不存在该机密 - 如果这不符合您的预期,那么该链接可能已经泄露,且您的机密已经被他人查看过了。","alert-something-went-wrong":"运行异常,对此我深感抱歉…","btn-create-secret":"创建机密","btn-create-secret-processing":"机密正在创建中…","btn-new-secret":"新的机密","btn-reveal-secret":"向我展示机密","btn-reveal-secret-processing":"机密正在解密中…","btn-show-explanation":"它如何运作?","expire-default":"默认过期时间","expire-n-days":"{n} 天 | {n} 天","expire-n-hours":"{n} 小时 | {n} 小时","expire-n-minutes":"{n} 分钟 | {n} 分钟","expire-n-seconds":"{n} 秒 | {n} 秒","items-explanation":["您在当前页面的文本框内输入机密内容","您的浏览器会生成随机密钥并对机密内容进行加密","您的浏览器会发送加密后的机密内容到服务器(明文内容和随机密钥都不会被发送!)","服务器将接收加密的机密并在指定的时间段内保留(过期会自动销毁)","您需要将包含 ID 和随机密钥的访问链接发送给收件人","收件人仅有一次查看机密的机会(如果无法查看,则表示机密已被其他人查看过!)","服务器会在机密被查看一次后立即将其销毁"],"label-expiry":"过期时间:","label-secret-data":"机密内容:","label-secret-files":"附加文件:","text-attached-files":"发件人在此机密中附加了文件,但该文件未经检查,请确保发件人可信!","text-burn-hint":"请您记住不要自己访问此链接,因为这会导致机密被销毁。请原封不动的转发给收件人吧!","text-burn-time":"如果机密没有被查看,将会在此时间自动销毁:","text-hint-burned":"\u003cstrong\u003e注意\u003c/strong\u003e您只能查看一次机密请尽快拷贝刷新页面也会导致机密消失","text-invalid-files-selected":"选中了至少一个不允许被作为附件的文件。","text-max-filesize":"附加文件大小上限:{maxSize}","text-max-filesize-exceeded":"您选择的文件过大,无法附加:{curSize} / {maxSize}","text-powered-by":"Powered by","text-pre-reveal-hint":"点击按钮即可查看机密,但请注意该操作会立即销毁机密,而且您也只能查看一次!","text-pre-url":"您的机密已创建,访问链接如下:","text-secret-create-disabled":"该实例被禁止创建机密。","title-explanation":"这就是它的运作原理…","title-new-secret":"创建一个机密","title-reading-secret":"查看您的机密…","title-secret-create-disabled":"创建机密被禁止…","title-secret-created":"机密已创建!"}'),
'zh-TW': JSON.parse('{"alert-secret-not-found":"這不是您正在尋找的機密\u0026hellip; - 如果您期望機密會出現在這裡,它可能已經被泄漏了,因為可能有其他人已經打開了此連結。","alert-something-went-wrong":"看樣子出了一些問題,對此我非常抱歉\u0026hellip;","btn-create-secret":"創建機密!","btn-new-secret":"新的機密","btn-reveal-secret":"向我展示機密!","btn-show-explanation":"這是如何工作的?","items-explanation":["您在當前頁面上的欄位輸入一個機密","您的瀏覽器使用生成的密碼加密剛才的機密","只有加密後的機密被發送到伺服器(無論是機密的明文內容還是加密的密碼都不會被發送!)","伺服器將加密的機密存儲一定時間","您將顯示的包含 ID 和解密密碼的 URL 連結發送給收件人","收件人只能查看一次機密:如果他們無法查看,就代表這個機密已經被其他人看過來!","當加密的機密被取回一次以後,他將從伺服器上被刪除"],"label-secret-data":"機密數據","text-burn-hint":"請注意您自己不要訪問這個 URL 地址,否則這會破壞機密。 就這樣把它發送給別人就可以了!","text-hint-burned":"\u003cstrong\u003e請注意\u003c/strong\u003e 您只能看到這一次! 一旦您刷新當前頁面,機密就會消失,所以應該現在就複製機密\u0026hellip;","text-powered-by":"強力驅動通過","text-pre-reveal-hint":"要揭露這個機密,請點擊此按鈕,但注意這樣做之後會破壞此機密。 您只能查看一次!","text-pre-url":"您的機密是使用此 URL 創建和存儲:","title-explanation":"這就是它的工作方式\u0026hellip;","title-new-secret":"創建一個機密","title-reading-secret":"讀取你的機密\u0026hellip;","title-secret-created":"機密已創建!"}'),

View File

@ -24,9 +24,23 @@ const i18n = new VueI18n({
messages,
})
Vue.mixin({
beforeRouteLeave(_to, _from, next) {
// Before leaving the component, reset the errors the component displayed
this.$emit('error', null)
next()
},
})
new Vue({
components: { app },
computed: {
isSecureEnvironment() {
return Boolean(window.crypto.subtle)
},
},
data: {
customize: {},
darkTheme: false,

View File

@ -8,4 +8,8 @@ $web-font-path: '';
textarea {
font-family: monospace !important;
}
.cursor-pointer {
cursor: pointer;
}
}

View File

@ -1,24 +1,25 @@
package main
import (
"errors"
"fmt"
"time"
"github.com/Luzifer/ots/pkg/storage"
"github.com/Luzifer/ots/pkg/storage/memory"
"github.com/Luzifer/ots/pkg/storage/redis"
)
var errSecretNotFound = errors.New("secret not found")
type storage interface {
Create(secret string, expireIn time.Duration) (string, error)
ReadAndDestroy(id string) (string, error)
}
func getStorageByType(t string) (storage, error) {
func getStorageByType(t string) (storage.Storage, error) {
switch t {
case "mem":
return newStorageMem(), nil
return memory.New(), nil
case "redis":
return newStorageRedis()
s, err := redis.New()
if err != nil {
return s, fmt.Errorf("creating redis storage: %w", err)
}
return s, nil
default:
return nil, fmt.Errorf("storage type %q not found", t)
}

View File

@ -1,55 +0,0 @@
package main
import (
"time"
"github.com/gofrs/uuid"
)
type memStorageSecret struct {
Expiry time.Time
Secret string
}
type storageMem struct {
store map[string]memStorageSecret
}
func newStorageMem() storage {
return &storageMem{
store: make(map[string]memStorageSecret),
}
}
func (s storageMem) Create(secret string, expireIn time.Duration) (string, error) {
var (
expire time.Time
id = uuid.Must(uuid.NewV4()).String()
)
if expireIn > 0 {
expire = time.Now().Add(expireIn)
}
s.store[id] = memStorageSecret{
Expiry: expire,
Secret: secret,
}
return id, nil
}
func (s storageMem) ReadAndDestroy(id string) (string, error) {
secret, ok := s.store[id]
if !ok {
return "", errSecretNotFound
}
defer delete(s.store, id)
if !secret.Expiry.IsZero() && secret.Expiry.Before(time.Now()) {
return "", errSecretNotFound
}
return secret.Secret, nil
}