mirror of
https://github.com/YunoHost/dynette.git
synced 2024-09-03 20:06:17 +02:00
[enh] Adding optionnal recovery password + delete interface using password (#4)
* Prototype of optionnal recovery password + delete interface using password * Use crypto/hash computation built in browser instead of code found on the interwebz.. * Adding some minimal CSS to the delete interface * async/await in delete interface to avoid a whole indent level
This commit is contained in:
parent
425ed1c6f2
commit
255e539322
3 changed files with 192 additions and 2 deletions
1
Gemfile
1
Gemfile
|
@ -6,3 +6,4 @@ gem 'json'
|
|||
gem 'data_mapper'
|
||||
gem 'dm-postgres-adapter'
|
||||
gem 'pg'
|
||||
gem 'bcrypt'
|
||||
|
|
158
delete.html
Normal file
158
delete.html
Normal file
|
@ -0,0 +1,158 @@
|
|||
|
||||
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* Adapted from https://github.com/minimalcss/form/tree/master/demo */
|
||||
input {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
/*color: inherit;*/
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
.button {
|
||||
text-align:center;
|
||||
color: #ffffff;
|
||||
background-color: #4c9ed9;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom:0.5em;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
.input {
|
||||
padding: 10px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: lightgray;
|
||||
background-color: white;
|
||||
}
|
||||
.input:focus
|
||||
{
|
||||
border-color: gray;
|
||||
}
|
||||
.input::-webkit-input-placeholder
|
||||
{
|
||||
color: gray;
|
||||
}
|
||||
.input::-moz-placeholder
|
||||
{
|
||||
color: gray;
|
||||
}
|
||||
.input:-ms-input-placeholder
|
||||
{
|
||||
color: gray;
|
||||
}
|
||||
.input::placeholder
|
||||
{
|
||||
color: gray;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 2em;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
form {
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.input {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<script>
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
/* START SHA256 CODE - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
|
||||
async function sha256(message) {
|
||||
const msgBuffer = new TextEncoder('utf-8').encode(message); // encode as UTF-8
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); // hash the message
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert ArrayBuffer to Array
|
||||
const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join(''); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
/* END SHA256 CODE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
|
||||
async function sendDeleteRequest()
|
||||
{
|
||||
// Compute 'true' password
|
||||
var domain = document.getElementById("domain").value;
|
||||
var user_password = document.getElementById("password").value;
|
||||
var true_password = await sha256(domain+":"+user_password);
|
||||
|
||||
// Prepare request
|
||||
var url = "./domains/"+domain
|
||||
var params = "recovery_password="+true_password;
|
||||
var xhttp = new XMLHttpRequest();
|
||||
|
||||
// Prepare handler
|
||||
xhttp.onreadystatechange = function()
|
||||
{
|
||||
if (xhttp.readyState == 4)
|
||||
{
|
||||
if (xhttp.status == 200)
|
||||
{
|
||||
document.getElementById("debug").innerHTML = xhttp.responseText;
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("debug").innerHTML = "Error ? " + xhttp.responseText;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("debug").innerHTML = "Sending request...";
|
||||
}
|
||||
};
|
||||
|
||||
// Actually send the request
|
||||
xhttp.open("DELETE", url, true);
|
||||
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
xhttp.send(params);
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<form>
|
||||
<label class="label" for="domain">Domain to delete:</label>
|
||||
<input class="input" type="text" id="domain">
|
||||
<label class="label" for="password">Password:</label>
|
||||
<input class="input" type="password" id="password">
|
||||
<input type="button" class="button" value="Submit" onclick="sendDeleteRequest();">
|
||||
<span id="debug"></span>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
35
dynette.rb
35
dynette.rb
|
@ -5,6 +5,7 @@ require 'sinatra'
|
|||
require 'data_mapper'
|
||||
require 'json'
|
||||
require 'base64'
|
||||
require 'bcrypt'
|
||||
|
||||
######################
|
||||
### Configuration ###
|
||||
|
@ -22,12 +23,14 @@ ALLOWED_IP = ["127.0.0.1"]
|
|||
# Dynette Entry class
|
||||
class Entry
|
||||
include DataMapper::Resource
|
||||
include BCrypt
|
||||
|
||||
property :id, Serial
|
||||
property :public_key, String
|
||||
property :subdomain, String
|
||||
property :current_ip, String
|
||||
property :created_at, DateTime
|
||||
property :recovery_password, Text
|
||||
|
||||
has n, :ips
|
||||
end
|
||||
|
@ -130,6 +133,14 @@ get '/' do
|
|||
"Wanna play the dynette ?"
|
||||
end
|
||||
|
||||
# Delete interface for user with recovery password
|
||||
get '/delete' do
|
||||
f = File.open("delete.html", "r")
|
||||
|
||||
content_type 'text/html'
|
||||
f.read
|
||||
end
|
||||
|
||||
# Get availables DynDNS domains
|
||||
get '/domains' do
|
||||
DOMAINS.to_json
|
||||
|
@ -158,9 +169,17 @@ post '/key/:public_key' do
|
|||
halt 409, { :error => "Key already exists for domain #{entry.subdomain}" }.to_json
|
||||
end
|
||||
|
||||
# If user provided a recovery password, hash and salt it before storing it
|
||||
if params.has_key?("recovery_password")
|
||||
recovery_password = BCrypt::Password.create(params[:recovery_password])
|
||||
else
|
||||
recovery_password = ""
|
||||
end
|
||||
|
||||
# Process
|
||||
entry = Entry.new(:public_key => params[:public_key], :subdomain => params[:subdomain], :current_ip => request.ip, :created_at => Time.now)
|
||||
entry = Entry.new(:public_key => params[:public_key], :subdomain => params[:subdomain], :current_ip => request.ip, :created_at => Time.now, :recovery_password => recovery_password)
|
||||
entry.ips << Ip.create(:ip_addr => request.ip)
|
||||
|
||||
if entry.save
|
||||
halt 201, { :public_key => entry.public_key, :subdomain => entry.subdomain, :current_ip => entry.current_ip }.to_json
|
||||
else
|
||||
|
@ -201,10 +220,21 @@ end
|
|||
|
||||
# Delete a sub-domain
|
||||
delete '/domains/:subdomain' do
|
||||
unless ALLOWED_IP.include? request.ip
|
||||
unless (ALLOWED_IP.include? request.ip) || (params.has_key?("recovery_password"))
|
||||
halt 403, { :error => "Access denied"}.to_json
|
||||
end
|
||||
if entry = Entry.first(:subdomain => params[:subdomain])
|
||||
|
||||
# For non-admin
|
||||
unless (ALLOWED_IP.include? request.ip)
|
||||
# If no recovery password was provided when registering domain,
|
||||
# or if wrong password is provided, deny access
|
||||
if (entry.recovery_password == "") || (BCrypt::Password.new(entry.recovery_password) != params[:recovery_password])
|
||||
halt 403, { :error => "Access denied" }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Ip.first(:entry_id => entry.id).destroy
|
||||
if entry.destroy
|
||||
halt 200, "OK".to_json
|
||||
|
@ -212,6 +242,7 @@ delete '/domains/:subdomain' do
|
|||
halt 412, { :error => "A problem occured during DNS deletion" }.to_json
|
||||
end
|
||||
end
|
||||
halt 404
|
||||
end
|
||||
|
||||
# Get all registered sub-domains
|
||||
|
|
Loading…
Reference in a new issue