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 'data_mapper'
|
||||||
gem 'dm-postgres-adapter'
|
gem 'dm-postgres-adapter'
|
||||||
gem 'pg'
|
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 'data_mapper'
|
||||||
require 'json'
|
require 'json'
|
||||||
require 'base64'
|
require 'base64'
|
||||||
|
require 'bcrypt'
|
||||||
|
|
||||||
######################
|
######################
|
||||||
### Configuration ###
|
### Configuration ###
|
||||||
|
@ -22,12 +23,14 @@ ALLOWED_IP = ["127.0.0.1"]
|
||||||
# Dynette Entry class
|
# Dynette Entry class
|
||||||
class Entry
|
class Entry
|
||||||
include DataMapper::Resource
|
include DataMapper::Resource
|
||||||
|
include BCrypt
|
||||||
|
|
||||||
property :id, Serial
|
property :id, Serial
|
||||||
property :public_key, String
|
property :public_key, String
|
||||||
property :subdomain, String
|
property :subdomain, String
|
||||||
property :current_ip, String
|
property :current_ip, String
|
||||||
property :created_at, DateTime
|
property :created_at, DateTime
|
||||||
|
property :recovery_password, Text
|
||||||
|
|
||||||
has n, :ips
|
has n, :ips
|
||||||
end
|
end
|
||||||
|
@ -130,6 +133,14 @@ get '/' do
|
||||||
"Wanna play the dynette ?"
|
"Wanna play the dynette ?"
|
||||||
end
|
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 availables DynDNS domains
|
||||||
get '/domains' do
|
get '/domains' do
|
||||||
DOMAINS.to_json
|
DOMAINS.to_json
|
||||||
|
@ -158,9 +169,17 @@ post '/key/:public_key' do
|
||||||
halt 409, { :error => "Key already exists for domain #{entry.subdomain}" }.to_json
|
halt 409, { :error => "Key already exists for domain #{entry.subdomain}" }.to_json
|
||||||
end
|
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
|
# 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)
|
entry.ips << Ip.create(:ip_addr => request.ip)
|
||||||
|
|
||||||
if entry.save
|
if entry.save
|
||||||
halt 201, { :public_key => entry.public_key, :subdomain => entry.subdomain, :current_ip => entry.current_ip }.to_json
|
halt 201, { :public_key => entry.public_key, :subdomain => entry.subdomain, :current_ip => entry.current_ip }.to_json
|
||||||
else
|
else
|
||||||
|
@ -201,10 +220,21 @@ end
|
||||||
|
|
||||||
# Delete a sub-domain
|
# Delete a sub-domain
|
||||||
delete '/domains/:subdomain' do
|
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
|
halt 403, { :error => "Access denied"}.to_json
|
||||||
end
|
end
|
||||||
if entry = Entry.first(:subdomain => params[:subdomain])
|
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
|
Ip.first(:entry_id => entry.id).destroy
|
||||||
if entry.destroy
|
if entry.destroy
|
||||||
halt 200, "OK".to_json
|
halt 200, "OK".to_json
|
||||||
|
@ -212,6 +242,7 @@ delete '/domains/:subdomain' do
|
||||||
halt 412, { :error => "A problem occured during DNS deletion" }.to_json
|
halt 412, { :error => "A problem occured during DNS deletion" }.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
halt 404
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get all registered sub-domains
|
# Get all registered sub-domains
|
||||||
|
|
Loading…
Reference in a new issue