1
0

First commit

This commit is contained in:
MatMoul 2024-10-27 04:43:07 +01:00
parent 1dae13fe7e
commit 35f37491af
20 changed files with 2120 additions and 0 deletions

4
.gitignore vendored
View File

@ -144,3 +144,7 @@ dist
# Built Visual Studio Code Extensions
*.vsix
instances/*
!instances/default
instances/default/cert.*
instances/default/vault.json

View File

@ -0,0 +1,66 @@
{
"servers": [
{
"features": {
"api": false,
"client": true,
"create": true,
"createPath": "/"
},
"http": {
"port": 3080
},
"https": {
"cert": "./cert.crt",
"key": "./cert.key",
"port": 3443
}
},
{
"features": {
"api": true,
"client": false,
"create": true
},
"http": {
"port": 3081
},
"https": {
"cert": "./cert.crt",
"key": "./cert.key",
"port": 3444
}
}
],
"vault": {
"db": {
"type": "file",
"filename": "./vault.json"
},
"crypto": {
"method": "aes-256-gcm",
"key": "Djblt6b8RQ+mQC6/ilpjC6y9bkfUEzkt",
"iv": "dsfnuo3"
},
"publicID": {
"minLength": 37,
"maxLength": 45
},
"otLinkID": {
"minLength": 37,
"maxLength": 45,
"timeout": 360
}
},
"mailer": {
"sender": "Secret Sender <me@domain>",
"server": {
"host": "127.0.0.1",
"port": 25,
"secure": false,
"tls": {
"rejectUnauthorized": false
}
}
}
}

3
instances/default/gencert.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -x509 -nodes -days 3650 -out ./cert.crt -keyout ./cert.key -subj "/C=/ST=/L=/O=/OU=/CN="

3
instances/default/start.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
node ../../src/app.js

11
src/app.js Normal file
View File

@ -0,0 +1,11 @@
import path from "path"
import config from "./core/config.js"
import server from "./core/server.js"
global.__dirname = path.dirname(process.argv[1])
const _config = config.get()
_config.servers.forEach(srvConfig => {
server.start(srvConfig)
})

23
src/core/config.js Normal file
View File

@ -0,0 +1,23 @@
import fs from "fs"
let _configFile = "config.json"
if(process.argv[2]) _configFile = process.argv[2]
let _config = null
const load = () => {
if(! fs.existsSync(_configFile)) {
throw new Error('Config file not found')
}
_config = JSON.parse(fs.readFileSync(_configFile))
}
if(! _config) load()
const get = () => {
return structuredClone(_config)
}
export default {
get,
}

29
src/core/mailer.js Normal file
View File

@ -0,0 +1,29 @@
import nodemailer from 'nodemailer'
import config from './config.js'
const _config = config.get()
const send = (email = {
from: null,
to: '',
subject: '',
text: null,
html: null,
}) => {
if(! email.from) email.from = _config.mailer.sender
const transporter = nodemailer.createTransport(_config.mailer.server)
const mailOptions = {
from: email.from,
to: email.to,
subject: email.subject,
}
if(email.html) {
mailOptions.html = email.html
if(email.text) mailOptions.text = email.text
} else mailOptions.text = email.text || ''
transporter.sendMail(mailOptions, function(error, info){})
}
export default {
send,
}

101
src/core/routes.js Normal file
View File

@ -0,0 +1,101 @@
import bodyParser from "body-parser"
import vault from "./vault.js"
const _title = "Secret Sender"
const load = (router, features) => {
if(features.api == true) {
router.get("/api/getlink/:publicID", (req, res, next) => {
const link = vault.getLink(req.params.publicID)
res.json(link)
})
router.get("/api/createOTLink/:publicID", (req, res, next) => {
const otLink = vault.createOTLink(req.params.publicID)
res.json(otLink)
})
router.get("/api/read/:otLinkID", (req, res, next) => {
const secret = vault.read(req.params.otLinkID)
res.json(secret)
})
if(features.create == true) {
router.post("/api/create", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
const link = vault.create(req.body)
res.json(link)
})
}
}
if(features.client !== false) {
router.get("/link/:publicID", (req, res, next) => {
const link = vault.getLink(req.params.publicID)
if(link) {
router.pug(res, "link.pug", { title : _title, link: link, linkUrl: req.protocol + "://" + req.headers.host + "/link/" + link.publicID })
} else {
res.redirect('/')
}
})
router.post("/requestotl", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
const link = vault.getLink(req.body.publicID)
if(link) {
const otLink = vault.createOTLink(link.publicID)
if(otLink) {
res.json(otLink)
} else {
res.json(null)
}
} else {
res.json(null)
}
})
router.get("/read/:otLinkID", (req, res, next) => {
const secret = vault.read(req.params.otLinkID)
if(secret) {
router.pug(res, "read.pug", { title : _title, secret: secret })
} else {
res.redirect('/')
}
})
if(features.create == true) {
router.get(features.createPath, (req, res, next) => {
router.pug(res, "create.pug", { title: _title })
})
router.post(features.createPath, bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
const link = vault.create(req.body)
if(link) {
res.json(link)
} else {
res.json(null)
}
})
router.post("/newlink", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => {
router.pug(res, "newlink.pug", { title : _title, link: req.body, linkUrl: req.protocol + "://" + req.headers.host + "/link/" + req.body.publicID })
})
}
router.get("/", (req, res, next) => {
router.pug(res, "index.pug", { title: _title })
})
}
}
export default {
load,
}

60
src/core/server.js Normal file
View File

@ -0,0 +1,60 @@
import fs from "fs"
import express from "express"
import http from "http"
import https from "https"
import routes from "./routes.js"
const defaulthandler = (req, res, next) => {
res.redirect("/")
}
const pug = (res, file, value, allowcache) => {
//app.srv.cache(res,allowcache!=false)
res.setHeader("X-Frame-Options", "sameorigin")
res.setHeader("X-XSS-Protection", "1; mode=block")
res.render(file, value)
}
/*
const getremoteip = (req) => {
return req.ip
}
*/
const start = (srvConfig) => {
const app = express()
const router = express.Router()
router.use(express.json())
router.pug = pug
routes.load(router, srvConfig.features)
app.disable("x-powered-by")
app.set('trust proxy', true)
app.set("views", __dirname + "/pug")
app.set("view engine", "pug")
app.use(router)
app.use(express.static(__dirname + "/html", {maxAge: 86400000000}))
app.use(defaulthandler)
if(srvConfig.http) {
const srv = http.createServer({}, app)
srv.listen(srvConfig.http.port, '0.0.0.0')
console.log("Listen http " + srvConfig.http.port + " : " + JSON.stringify(srvConfig.features))
}
if(srvConfig.https && fs.existsSync(srvConfig.https.cert) && fs.existsSync(srvConfig.https.key)) {
const sslsrv = https.createServer({
cert: fs.readFileSync(srvConfig.https.cert),
key: fs.readFileSync(srvConfig.https.key),
}, app)
sslsrv.listen(srvConfig.https.port, '0.0.0.0')
console.log("Listen https " + srvConfig.https.port + " : " + JSON.stringify(srvConfig.features))
}
}
export default {
start,
}

223
src/core/vault.js Normal file
View File

@ -0,0 +1,223 @@
import fs from 'fs'
import cron from 'node-cron'
import crypto from 'crypto'
import config from "./config.js"
import mailer from './mailer.js'
const _config = config.get()
let _cache = null
const _readLinks = []
const _dbFile = _config.vault.db.filename
const init = () => {
if(fs.existsSync(_dbFile)) {
_cache = JSON.parse(fs.readFileSync(_dbFile))
} else {
_cache = []
}
clean()
cron.schedule('* * * * *', clean)
}
const clean = () => {
let needSave = false
for(let i = _cache.length - 1; i >= 0; i--) {
if(calcTimeLeft(_cache[i]) <= 0) {
_cache.splice(i, 1)
needSave = true
}
}
if(needSave === true) save()
for(let i = _readLinks.length - 1; i >= 0; i--) {
if(Math.round(_config.vault.otLinkID.timeout - ((new Date() - new Date(_readLinks[i].date)) / 60000)) <= 0) {
_readLinks.splice(i, 1)
}
}
}
const save = () => {
fs.writeFileSync(_dbFile, JSON.stringify(_cache))
}
const newRandomNumber = (min, max) => {
return Math.floor(Math.random() * (max - min) + min)
}
const newRandomString = (length) => {
const randomBytes = crypto.randomBytes(Math.ceil(length / 2))
return randomBytes.toString('hex').slice(0, length)
}
const calcReadsLeft = (item) => {
if(item.maxReads <= 0) return -1
return item.maxReads - item.readCount
}
const calcTimeLeft = (item) => {
return Math.round(item.expireTime - ((new Date() - new Date(item.date)) / 60000))
}
const encrypt = (data) => {
const cipher = crypto.createCipheriv(_config.vault.crypto.method, _config.vault.crypto.key, _config.vault.crypto.iv)
let ciphertext = cipher.update(data, 'utf8', 'base64')
ciphertext += cipher.final('base64')
let value = cipher.getAuthTag().toString('hex') + '=' + ciphertext
return value
}
const decrypt = (data) => {
if(!data || data === '') return ''
const decipher = crypto.createDecipheriv(
_config.vault.crypto.method,
_config.vault.crypto.key,
_config.vault.crypto.iv
)
const splitIndex = data.indexOf('=')
const tag = data.substring(0, splitIndex)
const value = data.substring(splitIndex + 1)
decipher.setAuthTag(Buffer.from(tag, 'hex'))
let plaintext = decipher.update(value, 'base64', 'utf8')
plaintext += decipher.final('utf8')
return plaintext
}
const emailReadNotify = (data) => {
const email = {
to: data.emailNotify,
subject: "Secret read",
text: 'Secret read'
}
if(data.subject) email.subject = email.subject + ' - ' + data.subject
if(data.message) email.text = data.message
if(data.senderName) email.text = email.text + '\n\n' + data.senderName
mailer.send(email)
}
if(! _cache) init()
const createData = (data = {
senderName: null,
subject: null,
message: null,
secret: null,
emailNotify: null,
expireDays: null,
expireHours: null,
expireTime: null,
maxReads: null,
emailOTL: null,
}) => {
if(! data) return null
const validatedData = {}
if(data.senderName && data.senderName != "") validatedData.senderName = data.senderName
if(data.subject && data.subject != "") validatedData.subject = data.subject
if(data.message && data.message != "") validatedData.message = data.message
if(data.secret && data.secret != "") validatedData.secret = data.secret
if(data.emailNotify && data.emailNotify != "") validatedData.emailNotify = data.emailNotify
if(data.emailOTL && data.emailOTL != "") validatedData.emailOTL = data.emailOTL
if(data.maxReads && data.maxReads != "") validatedData.maxReads = data.maxReads
let expireTime = 0
if(data.expireDays && data.expireDays > 0) expireTime = +data.expireDays * 24 * 60
if(data.expireHours && data.expireHours > 0) expireTime = expireTime + +data.expireHours * 60
if(data.expireTime && data.expireTime > 0) expireTime = +data.expireTime
if(expireTime > 0) validatedData.expireTime = expireTime
if(! validatedData.secret || validatedData.secret === '') return null
if(! validatedData.expireTime) validatedData.expireTime = 24 * 60
else if(validatedData.expireTime < 1) validatedData.expireTime = 1
else if(validatedData.expireTime > 560 * 60) validatedData.expireTime = 560 * 60
if(! validatedData.maxReads || isNaN(validatedData.maxReads)) validatedData.maxReads = 1
else if(validatedData.maxReads < 0) validatedData.maxReads = 0
else if(validatedData.maxReads > 999) validatedData.maxReads = 999
return validatedData
}
const createLinkData = (data) => {
if(data) return {
date: data.date,
publicID: data.publicID,
senderName: data.senderName,
subject: data.subject,
message: data.message,
readsLeft: calcReadsLeft(data),
timeLeft: calcTimeLeft(data),
}
return null
}
const create = (secretData) => {
const data = createData(secretData)
if(! data) return null
data.date = new Date()
data.readCount = 0
data.publicID = newRandomString(newRandomNumber(_config.vault.publicID.minLength, _config.vault.publicID.maxLength))
data.secret = encrypt(data.secret)
_cache.push(data)
save()
const returnData = createLinkData(data)
returnData.emailNotify = data.emailNotify
returnData.emailOTL = data.emailOTL
return returnData
}
const remove = (publicID) => {
for(let i = 0; i < _cache.length; i++) {
if(_cache[i].publicID == publicID) {
_cache.splice(i, 1)
save()
for(let i = _readLinks.length - 1; i >= 0; i--) {
if(_readLinks[i].publicID == publicID) {
_readLinks.splice(i, 1)
}
}
return true
}
}
return false
}
const getLink = (publicID) => {
const data = _cache.find((item) => item.publicID === publicID)
return createLinkData(data)
}
const createOTLink = (publicID) => {
const value = _cache.find((item) => item.publicID === publicID)
if(value) {
const readLink = {
date: new Date(),
otLinkID: newRandomString(newRandomNumber(_config.vault.otLinkID.minLength, _config.vault.otLinkID.maxLength)),
publicID: publicID,
}
_readLinks.push(readLink)
return {
otLinkID: readLink.otLinkID
}
}
return null
}
const removeOTL = (otLinkID) => {
for(let i = _readLinks.length - 1; i >= 0; i--) {
if(_readLinks[i].otLinkID == otLinkID) {
_readLinks.splice(i, 1)
}
}
}
const read = (otLinkID) => {
const linkValue = _readLinks.find((item) => item.otLinkID === otLinkID)
if(linkValue) {
removeOTL(otLinkID)
const value = _cache.find((item) => item.publicID === linkValue.publicID)
if(value) {
value.readCount += 1
if(value.readCount < value.maxReads) {
save()
} else {
remove(value.publicID)
}
value.timeLeft = calcTimeLeft(value)
value.readsLeft = calcReadsLeft(value)
if(value.emailNotify) emailReadNotify(value)
const returnValue = structuredClone(value)
returnValue.secret = decrypt(returnValue.secret)
return returnValue
}
}
return null
}
export default {
create,
getLink,
createOTLink,
read,
}

99
src/html/style.css Normal file
View File

@ -0,0 +1,99 @@
* {
box-sizing: border-box;
scrollbar-color: #3eb0ec #232629;
scrollbar-width: 10px;
}
body {
font: 400 12pt "Droid Sans", sans-serif;
background-color: #454c53;
padding: 0;
margin: 0;
color: #e3d1c7;
}
input {
border: 1px solid transparent;
}
input:focus, textarea:focus {
outline: 1px solid #3eb0ec;
}
input:invalid, textarea:invalid {
border: 1px dashed red;
}
::placeholder {
color: #e3d1c7;
}
.left {
text-align: left;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
.fullwidth {
width: 100%;
}
.inlineleftspace {
margin-left: 4px ;
}
.monospace {
font-family: 'Courier New', Courier, monospace;
}
.form {
max-width: 800px;
min-width: 200px;
margin: auto;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 20px;
}
.panel {
margin-top: 4px;
}
.controlpanel {
margin-top: 30px;
}
.titlebox {
font-size:8pt;
margin-left: 4px;
margin-bottom: 2px;
}
.textbox {
border: none;
color: #e3d1c7;
background-color: #31363b;
padding: 4px;
}
.messagebox {
border: none;
color: #e3d1c7;
background-color: #31363b;
padding: 4px;
resize: vertical;
}
.button {
background-color: #31363b;
color: #e3d1c7;
padding: 4px;
border: solid 1px #2cc4db;
border-radius: 4px;
}
.button:hover {
background-color: #2cc4db;
color: #31363b;
}

1198
src/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
src/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "secret-sender",
"version": "1.0.0",
"main": "app.js",
"type": "module",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.1",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.15",
"pug": "^3.0.3"
}
}

12
src/pug/_main.pug Normal file
View File

@ -0,0 +1,12 @@
doctype html
html(lang='fr-FR')
head
meta(name='viewport', content='width=device-width, height=device-height, initial-scale=1, maximum-scale=1')
meta(charset='utf-8')
title #{title}
link(rel="stylesheet", href="/style.css")
block style
block script
block head
body
block content

86
src/pug/create.pug Normal file
View File

@ -0,0 +1,86 @@
extends _main
block script
script.
const post = (path, value) => {
const form = document.createElement('form');
form.method = 'post'
form.action = path
document.body.appendChild(form)
for (const key in value) {
const formField = document.createElement('input')
formField.type = 'hidden'
formField.name = key
formField.value = value[key]
form.appendChild(formField)
}
form.submit()
}
const createLink = () => {
const data = {}
if(SenderName.value != '') data.senderName = SenderName.value
if(Subject.value != '') data.subject = Subject.value
if(Message.value != '') data.message = Message.value
if(Secret.value != '') data.secret = Secret.value
if(EMailNotify.value != '') data.emailNotify = EMailNotify.value
data.expireDays = ExpireDays.value
data.expireHours = ExpireHours.value
data.maxReads = MaxReads.value
if(EMailOTL.value != '') data.emailOTL = EMailOTL.value
const path = window.location.pathname
fetch(path, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((res) => {
res.json().then((value) => {
if(value) post('/newlink', value)
})
})
}
block content
div.center
div.form
h1 Nouveau lien
div.panel.left
div.titlebox Expéditeur
div
input#SenderName(type="text", placeholder="(Optionel)").textbox.fullwidth
div.panel.left
div.titlebox Sujet
div
input#Subject(type="text", placeholder="(Optionel)").textbox.fullwidth
div.panel.left
div.titlebox Message
div
textarea#Message(rows="3", placeholder="(Optionel)").messagebox.fullwidth
div.panel.left
div.titlebox Secret (*)
div
textarea#Secret(rows="4", placeholder="Votre secret", required).messagebox.fullwidth.monospace
div.panel.left
div.titlebox E-Mail (Notifier)
div
input#EMailNotify(type="email", placeholder="(Optionel) Votre e-mail pour être notifié").messagebox.fullwidth
div.panel.left
div.titlebox Expiration
table.fullwidth
tr
td
input#ExpireDays(type="number", value="2", min="0", max="13").textbox.small
span.inlineleftspace jour(s)
input#ExpireHours(type="number", value="1", min="1", max="23").textbox.small.inlineleftspace
span.inlineleftspace heure(s)
td.right
input#MaxReads(type="number", value="1", min="0", max="999").textbox.small.inlineleftspace
span.inlineleftspace affichage(s)
div.panel.left
div.titlebox E-Mail (OTL)
div
input#EMailOTL(type="email", placeholder="(Optionel) E-mail du destinatire pour OTL").messagebox.fullwidth
div.panel.controlpanel
button(onclick="createLink()").button.fullwidth Créer le lien

5
src/pug/index.pug Normal file
View File

@ -0,0 +1,5 @@
extends _main
block content
div.center
h1 Secret Sender

54
src/pug/link.pug Normal file
View File

@ -0,0 +1,54 @@
extends _main
block script
script.
const requestLink = (publicID) => {
const path = "/requestotl"
fetch(path, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ publicID: publicID }),
}).then((res) => {
res.json().then((value) => {
window.location = '/read/' + value.otLinkID
})
})
}
block content
div.center
div.form
h1 Accéder
if link.senderName
div.panel.left
div.titlebox Expéditeur
div.messagebox=link.senderName
if link.subject
div.panel.left
div.titlebox Sujet
div.messagebox=link.subject
if link.message
div.panel.left
div.titlebox Message
div.messagebox!=link.message.replaceAll('\n', '<br />')
div.panel
table.fullwidth
tr
td.left
if link.timeLeft > 2820
div.infotext Expires dans #{Math.round(link.timeLeft / 60 / 24)} jours
else if link.timeLeft > 120
div.infotext Expires dans #{Math.round(link.timeLeft / 60)} heures
else
div.infotext Expires dans #{link.timeLeft} min
td.right
if link.readsLeft > 0
div.infotext #{link.readsLeft} affichage(s) restant(s)
div.panel.controlpanel
if link.emailOTL
button(onclick="requestLink('" + link.publicID + "')").button.fullwidth Demmander
else
button(onclick="requestLink('" + link.publicID + "')").button.fullwidth Afficher

43
src/pug/newlink.pug Normal file
View File

@ -0,0 +1,43 @@
extends _main
block content
div.center
div.form
h1 Nouveau lien
div.panel
div.messagebox.left.overflow.nowrap #{linkUrl}
if link.senderName
div.panel.left
div.titlebox Expéditeur
div.messagebox=link.senderName
if link.subject
div.panel.left
div.titlebox Sujet
div.messagebox=link.subject
if link.message
div.panel.left
div.titlebox Message
div.messagebox!=link.message.replaceAll('\n', '<br />')
if link.emailNotify
div.panel.left
div.titlebox Notifier
div.messagebox=link.emailNotify
if link.emailOTL
div.panel.left
div.titlebox OTL
div.messagebox=link.emailOTL
div.panel
table.fullwidth
tr
td.left
if link.timeLeft > 2820
div.infotext Expires dans #{Math.round(link.timeLeft / 60 / 24)} jours
else if link.timeLeft > 120
div.infotext Expires dans #{Math.round(link.timeLeft / 60)} heures
else
div.infotext Expires dans #{link.timeLeft} min
td.right
if link.readsLeft > 0
div.infotext #{link.readsLeft} affichage(s) restant(s)
div.panel.controlpanel
button(onclick="navigator.clipboard.writeText('" + linkUrl + "')").button.fullwidth Copier le lien

37
src/pug/read.pug Normal file
View File

@ -0,0 +1,37 @@
extends _main
block content
div.center
div.form
h1 Secret
if secret.senderName
div.panel.left
div.titlebox Expéditeur
div.messagebox=secret.senderName
if secret.subject
div.panel.left
div.titlebox Sujet
div.messagebox=secret.subject
if secret.message
div.panel.left
div.titlebox Message
div.messagebox!=secret.message.replaceAll('\n', '<br />')
div.panel.left
div.titlebox Secret
div#Secret.messagebox.monospace!=secret.secret.replaceAll('\n', '<br />')
if secret.readsLeft > 0 && secret.timeLeft > 0
div.panel
table.fullwidth
tr
td.left
if secret.timeLeft > 2820
div.infotext Expires dans #{Math.round(secret.timeLeft / 60 / 24)} jours
else if secret.timeLeft > 120
div.infotext Expires dans #{Math.round(secret.timeLeft / 60)} heures
else
div.infotext Expires dans #{secret.timeLeft} min
td.right
if secret.readsLeft > 0
div.infotext #{secret.readsLeft} affichage(s) restant(s)
div.panel.controlpanel
button(onclick="navigator.clipboard.writeText(Secret.innerText)").button.fullwidth Copier le secret

44
test-api.sh Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
HTTPPORT=3081
EMAILNOTIFY="${1}"
EMAILOTP="${2}"
echo "Create secret ..."
CREATEDATA='{'
CREATEDATA+=' "senderName": "Me"'
CREATEDATA+=', "subject": "Votre code"'
CREATEDATA+=', "message": "Hello"'
CREATEDATA+=', "secret": "My Secret"'
if [ "${EMAILNOTIFY}" != "" ]; then
CREATEDATA+=', "emailNotify": "'${EMAILNOTIFY}'"'
fi
if [ "${EMAILOTP}" != "" ]; then
CREATEDATA+=', "emailOTP": "'${EMAILOTP}'"'
fi
CREATEDATA+=' }'
echo "${CREATEDATA}"
NEWSECRET=$(curl -s -X POST -H "Content-Type: application/json" \
-d "${CREATEDATA}" \
http://localhost:${HTTPPORT}/api/create)
echo "New secret :"
echo "${NEWSECRET}" | jq
PUBLICID=$(echo "${NEWSECRET}" | jq -r .publicID)
echo ""
echo "Get Link"
curl -s http://localhost:${HTTPPORT}/api/getlink/"${PUBLICID}" | jq
echo ""
echo "Create OTLink"
OTLINKID=$(curl -s http://localhost:${HTTPPORT}/api/createOTLink/"${PUBLICID}" | jq -r .otLinkID)
echo "${OTLINKID}"
echo ""
echo "Read secret first time :"
curl -s http://localhost:${HTTPPORT}/api/read/"${OTLINKID}" | jq
echo "Read secret second time :"
curl -s http://localhost:${HTTPPORT}/api/read/"${OTLINKID}" | jq