Compare commits

..

5 Commits

Author SHA1 Message Date
Калинин Сергей Валерьевич
c4ded33507 Добавлена работа с файлами. Добавлен показ изображений и загрузка файлов. Изменен дизайн 2025-07-21 13:10:19 +03:00
svk
10c9635894 Обновить vault.go 2024-10-28 16:09:39 +03:00
Калинин Сергей Валерьевич
7ecdf7a606 Добавлена возможность шифровать данные (wrap).
Добавлена установка времени жизни токена (TTL).
Добавлено соответствие ограничения длины пароля и длины текста для шифрования.
Добавлена подготовка токена к расшифровке (ограничение длины, удаление пробельных символов).
2024-10-28 16:03:45 +03:00
svk
41e1a553f6 Обновить README.md 2024-10-17 15:08:06 +03:00
Калинин Сергей Валерьевич
897bb3de01 Добавлена поддержка wrap 2024-10-17 15:04:35 +03:00
7 changed files with 680 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
# Vault Wrap/Unwrap # Vault Wrap/Unwrap
ВЭБ-интерфейс к сервису vault/unwrap для безопасной передачи секретов. Также генератор паролей. ВЭБ-интерфейс к сервису Hashicorp Vault wrap/unwrap для безопасной передачи секретов. Также генератор паролей.
## Запуск ## Запуск
@@ -14,23 +14,26 @@ vault-wrap -action-address "https://secret.example.ru:8443" -vault-url "https://
Запуск с доступом по http: Запуск с доступом по http:
``` ```
vault-wrap -action-address "http://saecret.example.ru:8080" -vault-url "https://vault.example.ru:8200" -tls-cert cert.pem -tls-key privaty.key -listen-port 8080 vault-wrap -action-address "http://secret.example.ru:8080" -vault-url "https://vault.example.ru:8200" -tls-cert cert.pem -tls-key privaty.key -listen-port 8080
``` ```
Если сервис запущен за обратным прокси, то в качестве адреса сервиса -action-address требуется указать адрес прокси, т.е. адрес (FQDN) на который будут приходить запросы. Если сервис запущен за обратным прокси, то в качестве адреса сервиса -action-address требуется указать адрес прокси, т.е. адрес (FQDN) на который будут приходить запросы.
Для работы шифрования (wrap) нужен vault токен с доступом к vault wrap/unwrap. Токен задается через переменную окружения VAULT_TOKEN.
## Ключи командной строки ## Ключи командной строки
- action-address string - Адрес данного сервиса (https://secret.example.ru). Адрес который будет подставляться в форму в html-шаблон - action-address string - Адрес данного сервиса (https://secret.example.ru). Адрес который будет подставляться в форму в html-шаблон
- debug - Вывод отладочных сообщений - debug - Вывод отладочных сообщений
- listen-port string - Номер порта сервиса (default "8080") - listen-port string - Номер порта сервиса (default "8080")
- log-file string - Путь до лог-файла (default "vault-unwrap.log") - log-file string - Путь до лог-файла (default "vault-unwrap.log")
- max-text-length - Максимальная длина текста для шифрования и длина пароля для генератора (default 100)
- template-dir string -Каталог с шаблонами (default "html-template") - template-dir string -Каталог с шаблонами (default "html-template")
- template-file string Файл-шаблон для ВЭБ-странцы (default "index.html") - template-file string Файл-шаблон для ВЭБ-странцы (default "index.html")
- tls - Использовать SSL/TLS - tls - Использовать SSL/TLS
- tls-cert string - TLS сертификат (полный путь к файлу) - tls-cert string - TLS сертификат (полный путь к файлу)
- tls-key string - TLS ключ (полный путь к файлу) - tls-key string - TLS ключ (полный путь к файлу)
- vault-url string - Адрес сервера Hashicorp Vault (https://host.name:8200) - token-ttl - Время жизни wrap-токена в секундах (default "3600")
- vault-url string - Адрес сервера Hashicorp Vault (https://host.name:8200)
- help - Вывод справочной информации - help - Вывод справочной информации

View File

@@ -8,10 +8,13 @@ services:
environment: environment:
- ACTION_ADDRESS=${ACTION_ADDRESS:-https://secret.example.ru} - ACTION_ADDRESS=${ACTION_ADDRESS:-https://secret.example.ru}
- VAULT_ADDRESS=${VAULT_ADDRESS} - VAULT_ADDRESS=${VAULT_ADDRESS}
- VAULT_TOKEN=${WRAP_TOKEN}
- LISTEN_PORT=8080 - LISTEN_PORT=8080
- TLS_KEY_FILE=${TLS_KEY_FILE} - TLS_KEY_FILE=${TLS_KEY_FILE}
- TLS_CERT_FILE=${TLS_CERT_FILE} - TLS_CERT_FILE=${TLS_CERT_FILE}
- TZ=Europe/Moscow - TZ=Europe/Moscow
- MAX_TEXT_LENGTH=${MAX_TEXT_LENGTH:-100}
- TOKEN_TTL=${TOKEN_TTL:-3600}
restart: always restart: always
# ports: # ports:
# - 1234:8080 # - 1234:8080
@@ -26,16 +29,48 @@ services:
max-size: "10m" max-size: "10m"
max-file: "5" max-file: "5"
labels: labels:
- "tra.enable=true" - "traefik.enable=true"
- "tra.http.routers.secret.rule=Host(`secret.example.ru`)" - "traefik.http.routers.secret.rule=Host(`secret.example.ru`)"
- "tra.http.services.secret.loadbalancer.server.port=8080" - "traefik.http.services.secret.loadbalancer.server.port=8080"
- "tra.docker.network=reverse-proxy" - "traefik.docker.network=reverse-proxy"
- "tra.http.routers.secret.tls=true" - "traefik.http.routers.secret.tls=true"
- "tra.http.services.secret.loadbalancer.server.scheme=http" - "traefik.http.services.secret.loadbalancer.server.scheme=http"
networks: networks:
- default - default
- vault-wrap - vault-wrap
traefik:
image: traefik:v3.0
container_name: traefik
command:
# - --entrypoints.web.address=:80
# - --entrypoints.web-secure.address=:443
# - --providers.docker=true
- --providers.file.directory=/configuration/
- --providers.file.watch=true
volumes:
- traefik-dynamic-conf:/configuration/
- /home/gitlab-runner/traefik/traefik.yml:/traefik.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-ssl:/ssl/:ro
ports:
- 80:80
# - 8080:8080
- 888:888
- 443:443
restart: always
networks:
- default
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.rule=Host(`example.ru`)"
- "traefik.http.routers.traefik.tls=true"
# - "traefik.http.routers.traefik.tls.certresolver=letsEncrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.services.traefik.loadbalancer.server.port=888"
- "traefik.http.services.traefik.loadbalancer.server.scheme=https"
networks: networks:
default: default:
name: reverse-proxy name: reverse-proxy
@@ -46,3 +81,5 @@ networks:
volumes: volumes:
vault-wrap-log: vault-wrap-log:
vault-wrap-conf: vault-wrap-conf:
traefik-dynamic-conf:
traefik-ssl:

View File

@@ -3,6 +3,6 @@ set -u
while true ;do while true ;do
# /go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -vault-url "${VAULT_ADDRESS}" -tls-cert "/usr/local/share/vault-wrap/${TLS_CERT_FILE}" -tls-key "/usr/local/share/vault-wrap/${TLS_KEY_FILE}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log -listen-port "${LISTEN_PORT}" -tls # /go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -vault-url "${VAULT_ADDRESS}" -tls-cert "/usr/local/share/vault-wrap/${TLS_CERT_FILE}" -tls-key "/usr/local/share/vault-wrap/${TLS_KEY_FILE}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log -listen-port "${LISTEN_PORT}" -tls
/go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log /go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log -token-ttl ${TOKEN_TTL}
sleep 120 sleep 120
done done

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}
.file-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
img {
max-width: 100%;
height: auto;
margin: 20px 0;
}
.file-info {
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 5px;
}
.download-btn {
display: inline-block;
padding: 10px 20px;
background: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 10px;
}
.action-btn {
display: inline-block;
padding: 10px 20px;
background: #f0f0f0;
color: #333;
text-decoration: none;
border-radius: 5px;
margin: 10px;
}
</style>
</head>
<body>
<div class="file-container">
<h1>{{.Title}}</h1>
{{if .IsImage}}
<img src="data:{{.MimeType}};base64,{{.Base64Data}}" alt="File preview">
{{else}}
<div class="file-info">
<p>Этот файл не может быть отображен в браузере</p>
</div>
{{end}}
<div class="file-info">
<p><strong>Тип файла:</strong> {{.MimeType}}</p>
<p><strong>Размер:</strong> {{.FileSize}} KB</p>
<p><strong>Имя файла:</strong> {{.FileName}}</p>
<a href="data:{{.MimeType}};base64,{{.Base64Data}}"
class="download-btn"
download="{{.FileName}}">
Скачать файл
</a>
<a href="/" class="action-btn">На главную</a>
<a href="javascript:history.back()" class="action-btn">Назад</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Unwrap Form</title>
<style>
textarea { width: 100%; max-width: 500px; }
table { border-collapse: collapse; }
hr { margin: 15px 0; }
</style>
</head>
<body>
<table>
<!-- Форма для wrap/unwrap текста -->
<tr><td>
<form method="post" action="{{.URL}}/wrap">
<table>
<tr><td>Введите текст или токен:</td></tr>
<tr><td>
<textarea name="input_token" cols="50" rows="10" maxlength="{{.MAXTEXTLENGTH}}">{{.TEXT}}</textarea>
</td></tr>
<tr><td align="right">
<button type="submit">Зашифровать</button>
<button type="submit" formaction="{{.URL}}/unwrap">Расшифровать</button>
</td></tr>
</table>
</form>
</td></tr>
<tr><td><hr></td></tr>
<!-- Отдельная форма для загрузки файла -->
<tr><td>
<form method="post" action="{{.URL}}/wrapfile" enctype="multipart/form-data">
<input type="file" name="file" id="file" required>
<button type="submit">Зашифровать файл</button>
</form>
</td></tr>
<tr><td><hr></td></tr>
<!-- Форма для генерации пароля -->
<tr><td>
<form method="post" action="{{.URL}}/genpassword">
<table>
<tr><td align="right">
Длина пароля (от 15 до {{.MAXTEXTLENGTH}}):
<input type="number" name="passlength" min="15" max="{{.MAXTEXTLENGTH}}" value="32">
<button type="submit">Сгенерировать пароль</button>
</td></tr>
</table>
</form>
</td></tr>
</table>
</body>
</html>

View File

@@ -1,38 +1,206 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Unwrap Form</title> <title>Data Wrap/Unwrap Form</title>
</head> <style>
<body> body {
<table> font-family: Arial, sans-serif;
<tr><td> max-width: 800px;
<!-- <a href={{.URL}}/unwrap>Расшифровать</a> | margin: 0 auto;
<a href={{.URL}}/genpassword>Сгенерировать пароль</a>--> padding: 20px;
<tr><td><p></p></td></tr> background-color: #f9f9f9;
<tr><td> display: flex;
<form method="post"> flex-direction: column;
<table> min-height: 100vh;
<tr><td> box-sizing: border-box; /* Добавлено */
Введите токен: }
</td></tr> .content {
<tr><td> flex: 1;
<textarea id="wrapped_token" name="input_token" cols=50 rows=10>{{ .TEXT }}</textarea> padding-bottom: 20px; /* Добавлено */
</td></tr> }
<tr><td align=right> .form-container {
<button type="submit" formaction="{{.URL}}/unwrap">Расшифровать</button> background: white;
<tr><td><hr></td></tr> padding: 20px;
</td></tr> border-radius: 8px;
<tr><td align=right> box-shadow: 0 0 10px rgba(0,0,0,0.1);
Длина пароля (от 15 до 1024) margin-bottom: 15px; /* Уменьшено */
<input type="text" name="passlength"/ size=4 pattern="[0-9]{2,4}"> }
<button type="submit" formaction="{{.URL}}/genpassword">Сгенерировать пароль</button> h1 {
</td></tr> color: #333;
</form> text-align: center;
</td></tr> margin-bottom: 25px; /* Уменьшено */
<tr><td> font-size: 1.8em; /* Добавлено */
</td></tr> }
</table> textarea {
</body> width: 100%;
max-width: 100%;
min-height: 120px; /* Уменьшено */
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
resize: vertical;
box-sizing: border-box; /* Добавлено */
}
input[type="file"] {
margin: 8px 0; /* Уменьшено */
width: 100%; /* Добавлено */
}
input[type="number"] {
width: 60px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 4px; /* Уменьшено */
transition: background 0.3s;
}
button:hover {
background: #45a049;
}
hr {
border: 0;
height: 1px;
background: #ddd;
margin: 15px 0; /* Уменьшено */
}
.form-title {
font-weight: bold;
margin-bottom: 8px; /* Уменьшено */
color: #555;
font-size: 0.95em; /* Добавлено */
}
.button-group {
text-align: right;
margin-top: 8px; /* Уменьшено */
}
footer {
text-align: center;
padding: 12px 0;
color: #777;
font-size: 0.85em; /* Уменьшено */
border-top: 1px solid #eee;
background: white;
margin-top: auto; /* Важно для прижатия */
position: sticky;
bottom: 0;
}
.footer-links {
margin-top: 6px; /* Уменьшено */
}
.footer-links a {
color: #4CAF50;
text-decoration: none;
margin: 0 6px; /* Уменьшено */
font-size: 0.85em; /* Уменьшено */
}
.footer-links a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
body {
padding: 15px;
}
.form-container {
padding: 15px;
}
h1 {
font-size: 1.5em;
margin-bottom: 20px;
}
button {
padding: 6px 12px;
font-size: 0.9em;
}
.btn-link {
background: #9C27B0;
}
.btn-link:hover {
background: #7B1FA2;
}
}
</style>
</head>
<body>
<div class="content">
<!-- <h1>Сервис передачи секретов</h1> -->
<div class="form-container">
<div class="form-title">Введите текст или токен:</div>
<form method="post" action="{{.URL}}/wrap">
<textarea id="tokenTextarea" name="input_token" maxlength="{{.MAXTEXTLENGTH}}" placeholder="Введите текст или токен...">{{.TEXT}}</textarea>
<div class="button-group">
<button type="submit">Зашифровать</button>
<button type="submit" formaction="{{.URL}}/unwrap">Расшифровать</button>
<button type="button" class="btn-copy" onclick="copyToken()">Скопировать</button>
</div>
</form>
</div>
<div class="form-container">
<div class="form-title">Выберите файл (до 100кб):</div>
<form method="post" action="{{.URL}}/wrapfile" enctype="multipart/form-data">
<input type="file" name="file" id="file" required>
<div class="button-group">
<button type="submit">Зашифровать файл</button>
</div>
</form>
</div>
<div class="form-container">
<div class="form-title">Генерация пароля:</div>
<form method="post" action="{{.URL}}/genpassword">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;">
<div style="margin-bottom: 8px;">
Длина пароля (от 15 до {{.MAXTEXTLENGTH}}):
<input type="number" name="passlength" min="15" max="{{.MAXTEXTLENGTH}}" value="32">
</div>
<button type="submit" style="margin-left: auto;">Сгенерировать пароль</button>
</div>
</form>
</div>
</div>
<footer>
<div>© <script>document.write(new Date().getFullYear())</script> SVK</div>
</footer>
<script>
function copyToken() {
const textarea = document.getElementById('tokenTextarea');
const fullText = textarea.value;
const token = fullText.split('\n')[0].trim();
copyToClipboard(token, '.btn-copy');
}
function copyToClipboard(text, buttonSelector) {
const tempInput = document.createElement('input');
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
const copyBtn = document.querySelector(buttonSelector);
const originalText = copyBtn.textContent;
copyBtn.textContent = 'Скопировано!';
setTimeout(() => {
copyBtn.textContent = originalText;
}, 2000);
}
</script>
</body>
</html> </html>

328
vault.go
View File

@@ -1,16 +1,27 @@
// -------------------------------------------
// Hashicorp Vault wrap/unwrap web service
// Distributed under GNU Public License
// Author: Sergey Kalinin svk@nuk-svk.ru
// Home page: https://nuk-svk.ru https://git.nuk-svk.ru
// -------------------------------------------
package main package main
import ( import (
// "context" // "context"
"log" "log"
// "time" // "time"
"io"
"os" "os"
"fmt" "fmt"
"flag" "flag"
"regexp" "regexp"
"bytes" "bytes"
"strconv" "strconv"
"strings"
"time"
"encoding/json" "encoding/json"
"encoding/base64"
"crypto/tls" "crypto/tls"
"net/http" "net/http"
"html/template" "html/template"
@@ -41,15 +52,20 @@ var (
TemplateFile string TemplateFile string
ActionAddress string ActionAddress string
VaultAddress string VaultAddress string
VaultToken string
Data string Data string
ListenPort string ListenPort string
TlsEnable bool TlsEnable bool
TlsCertFile string TlsCertFile string
TlsKeyFile string TlsKeyFile string
MaxTextLength int
TokenTTL string
) )
type TemplateData struct { type TemplateData struct {
URL string URL string
TEXT string TEXT string
MAXTEXTLENGTH int
TOKENTTL string
} }
type UnwrappedData struct { type UnwrappedData struct {
Rerquest_id string `json: "request_id"` Rerquest_id string `json: "request_id"`
@@ -57,24 +73,71 @@ type UnwrappedData struct {
Renewable bool `json: "renewable"` Renewable bool `json: "renewable"`
Lease_daration int `json:"lease_duration"` Lease_daration int `json:"lease_duration"`
Data map[string]string `json: "data"` Data map[string]string `json: "data"`
Wrap_info string `json: "wrap_info"` Wrap_info *WrapInfo `json: "wrap_info"`
Warnings string `json: "warnings"` Warnings string `json: "warnings"`
Auth string `json: "auth"` Auth string `json: "auth"`
Mount_type string `json: "mount_type"` Mount_type string `json: "mount_type"`
Error string `json: "errors"` Errors string `json: "errors"`
} }
func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string) string { type WrapInfo struct {
Token string `json:"token"`
Ttl int `json:"ttl"`
CreationTime time.Time `json:"creation_time"`
CreationPath string `json:"creation_path"`
}
type FileViewerData struct {
Title string
Base64Data string
MimeType string
FileSize string
FileName string
IsImage bool
}
// const MaxTextLength = 100
func vaultDataWrap(vaultAddr string, dataType string, content string) string {
secret := make(map[string]string)
// switch dataType {
// case "text":
// secret["wrapped_data"] = text
// case "file":
// secret["file"] = text
// default:
// secret["wrapped_data"] = text
// }
// Определяем ключ для данных
if Debug {
log.Println("Тип данныж:", dataType)
}
if dataType == "text" {
// Для текста используем ключ "text"
secret["text"] = content
} else {
// Для файла используем имя файла как ключ
// Если dataType не "text" и не пустое, считаем его именем файла
if dataType == "" {
dataType = "file" // fallback, если имя не указано
}
secret[dataType] = content
}
postBody, err := json.Marshal(secret)
if err != nil {
log.Println("Errored marshaling the text:", content)
}
customTransport := &(*http.DefaultTransport.(*http.Transport)) customTransport := &(*http.DefaultTransport.(*http.Transport))
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{Transport: customTransport} client := &http.Client{Transport: customTransport}
// client := &http.Client{} // client := &http.Client{}
req, _ := http.NewRequest("POST", vaultAddr, nil) req, _ := http.NewRequest("POST", vaultAddr, bytes.NewBuffer(postBody))
req.Header.Add("Accept", "application/json") req.Header.Add("Accept", "application/json")
req.Header.Add("X-Vault-Token", vaultToken) req.Header.Add("X-Vault-Token", VaultToken)
req.Header.Add("X-Vault-Wrap-TTL", TokenTTL)
resp, err := client.Do(req) resp, err := client.Do(req)
@@ -82,11 +145,16 @@ func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string)
log.Println("Errored when sending request to the Vault server") log.Println("Errored when sending request to the Vault server")
} }
var result map[string]interface{} var result UnwrappedData
json.NewDecoder(resp.Body).Decode(&result) json.NewDecoder(resp.Body).Decode(&result)
secret := result["data"].(map[string]interface{})["data"].(map[string]interface{})[vaultSecretName] // wrappedToken := result.Data
log.Println(result) if Debug {
return fmt.Sprint(secret) log.Println(resp)
log.Println("result", result)
log.Println("token", result.Wrap_info.Token)
}
return result.Wrap_info.Token
} }
func vaultDataUnWrap(vaultAddr string, vaultWrapToken string) map[string]string { func vaultDataUnWrap(vaultAddr string, vaultWrapToken string) map[string]string {
@@ -115,9 +183,13 @@ func vaultDataUnWrap(vaultAddr string, vaultWrapToken string) map[string]string
json.NewDecoder(resp.Body).Decode(&result) json.NewDecoder(resp.Body).Decode(&result)
secret := result.Data secret := result.Data
if Debug { if Debug {
log.Println(result) log.Println("result", result)
log.Println(secret) log.Println("secret", secret)
} }
// log.Println("Length=", len(secret))
// if len(secret) == 0 {
// log.Println("Error:", result.Errors)
// }
// fmt.Sprint(secret) // fmt.Sprint(secret)
// for v, k := range secret { // for v, k := range secret {
// log.Println(k, v) // log.Println(k, v)
@@ -151,8 +223,9 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) {
// templateData.UUID = uuid // templateData.UUID = uuid
templateData.URL = ActionAddress templateData.URL = ActionAddress
templateData.TEXT = Data templateData.TEXT = Data
templateData.MAXTEXTLENGTH = MaxTextLength
templateData.TOKENTTL = TokenTTL
// templateData.URL = FishingUrl + "/" + arrUsers[i].messageUUID // templateData.URL = FishingUrl + "/" + arrUsers[i].messageUUID
if body, err := ParseTemplate(template, templateData); err == nil { if body, err := ParseTemplate(template, templateData); err == nil {
@@ -160,39 +233,100 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) {
} }
} }
func showFileViewer(w http.ResponseWriter, r *http.Request, fileData []byte, fileName string) {
// Определяем MIME-тип файла
mimeType := http.DetectContentType(fileData)
isImage := strings.HasPrefix(mimeType, "image/")
// Кодируем в base64
base64Data := base64.StdEncoding.EncodeToString(fileData)
// Подготавливаем данные для шаблона
data := FileViewerData{
Title: "Просмотр файла",
Base64Data: base64Data,
MimeType: mimeType,
FileSize: fmt.Sprintf("%.2f", float64(len(fileData))/1024),
FileName: fileName,
IsImage: isImage,
}
// Парсим шаблон
tmpl, err := template.ParseFiles(filepath.Join(TemplateDir, "file-viewer.html"))
if err != nil {
http.Error(w, "Error loading template", http.StatusInternalServerError)
return
}
// Отображаем шаблон
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Error rendering template", http.StatusInternalServerError)
}
}
// hvs.CAES - 95 // hvs.CAES - 95
// s.Dj7kZS - 26 // s.Dj7kZS - 26
func getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
func unwrapDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
r.ParseForm() r.ParseForm()
token := r.FormValue("input_token") token := r.FormValue("input_token")
vaultPath := VaultAddress + "/v1/sys/wrapping/unwrap" vaultPath := VaultAddress + "/v1/sys/wrapping/unwrap"
Data = "" Data = ""
// fmt.Fprintln(w, r.URL.RawQuery)
// log.Println(w, r.URL.RawQuery) // Нормализация токена
if Debug {
log.Printf("Текст для расшифровки: %s ", token)
log.Printf("Адрес сервера Hashicorp Vault: %s ", vaultPath)
}
// Проверка текста на соответствие шаблону
re := regexp.MustCompile(`^(hvs|s)\.[\w\-\_]+`) re := regexp.MustCompile(`^(hvs|s)\.[\w\-\_]+`)
if Debug { token = strings.TrimSpace(strings.ReplaceAll(token, "\r\n", ""))
fmt.Println(re.Match([]byte(token)))
} if token != "" && re.MatchString(token) {
if token != "" && re.Match([]byte(token)) { unwrappedData := vaultDataUnWrap(vaultPath, token)
b := new(bytes.Buffer)
for key, value := range vaultDataUnWrap(vaultPath, token) { // 1. Сначала проверяем наличие текстовых данных
fmt.Fprintf(b, "%s: %s\n", key, value) if textData, exists := unwrappedData["text"]; exists {
Data = textData
} else {
// 2. Если текста нет, ищем файловые данные (любой ключ, кроме "text")
var fileData []byte
var fileName string
for key, value := range unwrappedData {
if key != "text" { // Пропускаем текстовые данные, если они есть
// Декодируем base64
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
log.Printf("Ошибка декодирования файла %s: %v", key, err)
continue
}
fileData = decoded
fileName = key
break
}
}
if fileData != nil {
// Если нашли файл - показываем просмотрщик
showFileViewer(w, r, fileData, fileName)
return
} else {
// Если не нашли ни текста, ни файла - выводим все данные как текст
b := new(bytes.Buffer)
for key, value := range unwrappedData {
fmt.Fprintf(b, "%s: %s\n", key, value)
}
Data = b.String()
}
} }
Data = b.String()
if Debug { if Data == "" {
log.Println(Data) Data = "Ошибка! Токен не содержит данных."
} }
} else if token != "" {
Data = "Введенные данные не соответствуют формату. Введите корректный токен."
} }
getStaticPage(w, r) getStaticPage(w, r)
// http.Redirect(w, r, "http://"+r.Host, http.StatusMovedPermanently)
} }
func genPassword(w http.ResponseWriter, r *http.Request) { func genPassword(w http.ResponseWriter, r *http.Request) {
@@ -209,7 +343,7 @@ func genPassword(w http.ResponseWriter, r *http.Request) {
} }
// w.Write([]byte("Длина пароля " + passLength + "/n")) // w.Write([]byte("Длина пароля " + passLength + "/n"))
passwordLength, err := strconv.Atoi(passLength) passwordLength, err := strconv.Atoi(passLength)
if passwordLength > 1024 { if passwordLength > MaxTextLength {
log.Printf("Oversized password length") log.Printf("Oversized password length")
Data = "Превышена длина пароля" Data = "Превышена длина пароля"
getStaticPage(w, r) getStaticPage(w, r)
@@ -218,7 +352,7 @@ func genPassword(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
res, err := password.Generate(passwordLength, 10, 5, false, true) res, err := password.Generate(passwordLength, 5, 5, false, true)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
@@ -242,6 +376,111 @@ func genPasswordDefault(w http.ResponseWriter, r *http.Request) {
getStaticPage(w, r) getStaticPage(w, r)
} }
func wrapDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
secret := r.FormValue("input_token")
if len([]rune(secret)) > MaxTextLength {
log.Println("Длина текста превышает заданные ограничения:", MaxTextLength)
Data = fmt.Sprintf("Длина текста превышает заданные ограничения: %s > %s", strconv.Itoa(len([]rune(secret))), strconv.Itoa(MaxTextLength))
} else {
vaultPath := VaultAddress + "/v1/sys/wrapping/wrap"
Data = ""
if Debug {
fmt.Println("Введен текст:", secret)
}
if secret != "" {
Data = vaultDataWrap(vaultPath, "text", secret)
if Data == "" {
Data = "Ошибка! Токен не найден."
}
if Debug {
log.Println(Data)
}
// Переводим секунды в часы и дабавляем к токену для информации.
ttl, _ := strconv.Atoi(TokenTTL)
ttl = ttl / 3600
Data = fmt.Sprintf("%s\n\n---\nВремя жизни токена %s ч.", Data, strconv.Itoa(ttl))
} else if secret != "" {
Data = "Введите текст для шифровки."
}
}
getStaticPage(w, r)
}
func wrapDataFromFile(w http.ResponseWriter, r *http.Request) {
MaxFileLength := 100000 // Максимальный размер файла (100KB)
// 1. Проверяем метод (должен быть POST)
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 2. Проверяем Content-Type (должен быть multipart/form-data)
contentType := r.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "multipart/form-data") {
http.Error(w, "Expected multipart/form-data", http.StatusBadRequest)
return
}
// 3. Парсим форму с ограничением размера (10 МБ)
maxMemory := 10 << 20 // 10 MB
if err := r.ParseMultipartForm(int64(maxMemory)); err != nil {
log.Printf("Failed to parse multipart form: %v", err)
http.Error(w, "Unable to parse form (file too big?)", http.StatusBadRequest)
return
}
// 4. Получаем файл из формы (включая метаданные)
file, fileHeader, err := r.FormFile("file")
if err != nil {
log.Printf("Failed to get file from form: %v", err)
http.Error(w, "Error retrieving the file", http.StatusBadRequest)
return
}
defer file.Close()
// Получаем оригинальное имя файла
fileName := fileHeader.Filename
if fileName == "" {
fileName = "unnamed_file" // Запасной вариант, если имя не указано
}
if Debug {
log.Printf("Processing file: %s (size: %d bytes)", fileName, fileHeader.Size)
}
// 5. Читаем содержимое файла
fileBytes, err := io.ReadAll(file)
if err != nil {
log.Printf("Failed to read file: %v", err)
http.Error(w, "Error reading the file", http.StatusInternalServerError)
return
}
// 6. Проверяем размер файла
if len(fileBytes) > MaxFileLength {
errMsg := fmt.Sprintf("File too large (%d > %d)", len(fileBytes), MaxFileLength)
http.Error(w, errMsg, http.StatusBadRequest)
return
}
// 7. Кодируем в base64
encoded := base64.StdEncoding.EncodeToString(fileBytes)
// 8. Обёртываем данные в Vault с именем файла
vaultPath := VaultAddress + "/v1/sys/wrapping/wrap"
token := vaultDataWrap(vaultPath, fileName, encoded)
if token == "" {
http.Error(w, "Failed to wrap data in Vault", http.StatusInternalServerError)
return
}
ttl, _ := strconv.Atoi(TokenTTL)
ttl = ttl / 3600
Data = fmt.Sprintf("%s\n\n---\nВремя жизни токена %s ч.", token, strconv.Itoa(ttl))
getStaticPage(w, r)
}
func main() { func main() {
var ( var (
logFile string logFile string
@@ -256,6 +495,8 @@ func main() {
flag.StringVar(&TlsCertFile, "tls-cert", "", "TLS сертификат (файл)") flag.StringVar(&TlsCertFile, "tls-cert", "", "TLS сертификат (файл)")
flag.StringVar(&TlsKeyFile, "tls-key", "", "TLS ключ (файл)") flag.StringVar(&TlsKeyFile, "tls-key", "", "TLS ключ (файл)")
flag.BoolVar(&TlsEnable, "tls", false, "Использовать SSL/TLS") flag.BoolVar(&TlsEnable, "tls", false, "Использовать SSL/TLS")
flag.IntVar(&MaxTextLength, "max-text-length", 100 , "Максимальная длина текста для шифрования и длина пароля для генератора")
flag.StringVar(&TokenTTL, "token-ttl", "3600", "Время жизни wrap-токена в секундах")
flag.Parse() flag.Parse()
@@ -281,17 +522,31 @@ func main() {
VaultAddress = os.Getenv("VAULT_ADDRESS") VaultAddress = os.Getenv("VAULT_ADDRESS")
} }
if os.Getenv("VAULT_TOKEN") == "" && VaultToken == "" {
log.Println("Send error: make sure environment variables `VAULT_TOKEN` was set")
} else if os.Getenv("VAULT_TOKEN") != "" && VaultToken == "" {
VaultToken = os.Getenv("VAULT_TOKEN")
}
if os.Getenv("MAX_TEXT_LENGTH") != "" && MaxTextLength == 100 {
MaxTextLength, err = strconv.Atoi(os.Getenv("MAX_TEXT_LENGTH"))
if err != nil {
log.Printf("Ошибка преобразования значения ", os.Getenv("MAX_TEXT_LENGTH"))
}
}
if Debug { if Debug {
log.Printf("Адрес сервера Hashicorp Vault: %s ", VaultAddress) log.Printf("Адрес сервера Hashicorp Vault: %s ", VaultAddress)
} }
rtr := mux.NewRouter() rtr := mux.NewRouter()
rtr.HandleFunc("/unwrap", getDataFromHtmlForm) rtr.HandleFunc("/unwrap", unwrapDataFromHtmlForm)
rtr.HandleFunc("/wrap", wrapDataFromHtmlForm)
rtr.HandleFunc("/wrapfile", wrapDataFromFile).Methods("POST")
rtr.HandleFunc("/genpassword/{passLength:[0-9]+}", genPassword) rtr.HandleFunc("/genpassword/{passLength:[0-9]+}", genPassword)
rtr.HandleFunc("/genpassword", genPassword) rtr.HandleFunc("/genpassword", genPassword)
rtr.HandleFunc("/", getDataFromHtmlForm) rtr.HandleFunc("/", unwrapDataFromHtmlForm)
rtr.PathPrefix("/").Handler(http.FileServer(http.Dir("./static"))) rtr.PathPrefix("/").Handler(http.FileServer(http.Dir("./static")))
http.Handle("/", rtr) http.Handle("/", rtr)
@@ -316,4 +571,3 @@ func main() {
http.ListenAndServe(listenAddr, nil) http.ListenAndServe(listenAddr, nil)
} }
} }