2013-06-15 21:06:14 +02:00
#!/usr/bin/ruby
require 'rubygems'
require 'sinatra'
require 'data_mapper'
require 'json'
2013-07-07 12:46:39 +02:00
require 'base64'
2017-07-24 01:55:54 +02:00
require 'bcrypt'
2013-06-15 21:06:14 +02:00
2016-04-26 12:18:16 +02:00
######################
### Configuration ###
######################
2013-08-07 12:40:49 +02:00
DataMapper . setup ( :default , ENV [ 'DATABASE_URL' ] || " postgres://dynette:myPassword@localhost/dynette " )
2013-06-19 09:52:35 +02:00
DOMAINS = [ " nohost.me " , " noho.st " ]
2013-08-07 12:40:49 +02:00
ALLOWED_IP = [ " 127.0.0.1 " ]
2013-06-15 21:06:14 +02:00
2016-04-26 12:18:16 +02:00
###############
### Classes ###
###############
# Dynette Entry class
2013-06-15 21:06:14 +02:00
class Entry
include DataMapper :: Resource
2017-07-24 01:55:54 +02:00
include BCrypt
2013-06-15 21:06:14 +02:00
property :id , Serial
2017-09-21 06:24:29 +02:00
# we need at least 90 chars for hmac-sha512 keys
property :public_key , String , :length = > 100
2017-09-06 05:29:31 +02:00
2017-10-09 12:34:55 +02:00
# for historical reasons, tsig algo was md5, so we assume that every
2017-09-06 05:29:31 +02:00
# entry is using md5 while we provide automatic upgrade code inside
2017-09-19 16:54:50 +02:00
# yunohost to move to sha512 instead (and register new domains using sha512)
2017-09-06 05:29:31 +02:00
# it would be good to depreciate md5 in the futur but that migh be complicated
property :key_algo , String , :default = > " hmac-md5 "
2013-06-15 21:06:14 +02:00
property :subdomain , String
property :current_ip , String
2013-06-16 10:21:06 +02:00
property :created_at , DateTime
2017-07-24 01:55:54 +02:00
property :recovery_password , Text
2013-06-15 21:06:14 +02:00
has n , :ips
end
2016-04-26 12:18:16 +02:00
# IP class
2013-06-15 21:06:14 +02:00
class Ip
include DataMapper :: Resource
property :id , Serial
property :ip_addr , String
belongs_to :entry
end
2016-04-26 12:18:16 +02:00
# IP Log class
2013-06-16 00:13:29 +02:00
class Iplog
include DataMapper :: Resource
property :ip_addr , String , :key = > true
property :visited_at , DateTime
end
2016-04-26 12:18:16 +02:00
# IP ban class
2013-06-16 00:13:29 +02:00
class Ipban
include DataMapper :: Resource
property :ip_addr , String , :key = > true
end
2016-04-26 12:18:16 +02:00
################
### Handlers ###
################
# 404 Error handler
2013-06-16 10:21:06 +02:00
not_found do
content_type :json
halt 404 , { :error = > " Not found " } . to_json
end
2016-04-26 12:18:16 +02:00
##############
### Routes ###
##############
2016-04-26 09:24:33 +02:00
# Common tasks and settings for every route
2013-06-16 00:13:29 +02:00
before do
2016-04-26 09:56:20 +02:00
# Always return json
content_type :json
# Allow CORS
headers [ 'Access-Control-Allow-Origin' ] = '*'
2016-04-26 09:24:33 +02:00
# Ban IP on flood
2013-06-16 00:13:29 +02:00
if Ipban . first ( :ip_addr = > request . ip )
2016-04-26 09:56:20 +02:00
halt 410 , { :error = > " Your ip is banned from the service " } . to_json
2013-06-16 00:13:29 +02:00
end
2013-06-16 10:21:06 +02:00
unless %w[ domains test all ban unban ] . include? request . path_info . split ( '/' ) [ 1 ]
if iplog = Iplog . last ( :ip_addr = > request . ip )
if iplog . visited_at . to_time > Time . now - 30
2016-04-26 09:56:20 +02:00
halt 410 , { :error = > " Please wait 30sec " } . to_json
2013-06-16 10:21:06 +02:00
else
iplog . update ( :visited_at = > Time . now )
end
2013-06-16 00:13:29 +02:00
else
2013-06-16 10:21:06 +02:00
Iplog . create ( :ip_addr = > request . ip , :visited_at = > Time . now )
2013-06-16 00:13:29 +02:00
end
end
2016-04-26 09:24:33 +02:00
2013-06-16 10:21:06 +02:00
end
2013-06-16 09:45:02 +02:00
2013-06-16 10:21:06 +02:00
# Check params
[ '/test/:subdomain' , '/key/:public_key' , '/ips/:public_key' , '/ban/:ip' , '/unban/:ip' ] . each do | path |
before path do
if params . has_key? ( " public_key " )
2013-07-07 13:20:39 +02:00
public_key = Base64 . decode64 ( params [ :public_key ] . encode ( 'ascii-8bit' ) )
2017-09-18 19:48:07 +02:00
# might be 88
2017-09-18 22:43:37 +02:00
unless public_key . length == 24 or public_key . length == 32
2013-07-07 13:00:21 +02:00
halt 400 , { :error = > " Key is invalid: #{ public_key . to_s . encode ( 'UTF-8' , { :invalid = > :replace , :undef = > :replace , :replace = > '?' } ) } " } . to_json
2013-06-16 10:21:06 +02:00
end
2013-06-16 09:45:02 +02:00
end
2017-09-18 19:48:07 +02:00
if params . has_key? ( " key_algo " ) and not [ " hmac-md5 " , " hmac-sha512 " ] . include? params [ :key_algo ]
halt 400 , { :error = > " key_algo value is invalid: #{ public_key } , it should be either 'hmac-sha512' or 'hmac-md5' (but you should **really** use 'hmac-sha512') " } . to_json
end
2013-06-16 10:21:06 +02:00
if params . has_key? ( " subdomain " )
2014-05-09 16:33:50 +02:00
unless params [ :subdomain ] . match / ^([a-z0-9]{1}([a-z0-9 \ -]*[a-z0-9])*)( \ .[a-z0-9]{1}([a-z0-9 \ -]*[a-z0-9])*)*( \ .[a-z]{1}([a-z0-9 \ -]*[a-z0-9])*)$ /
2013-06-16 10:21:06 +02:00
halt 400 , { :error = > " Subdomain is invalid: #{ params [ :subdomain ] } " } . to_json
end
unless DOMAINS . include? params [ :subdomain ] . gsub ( params [ :subdomain ] . split ( '.' ) [ 0 ] + '.' , '' )
halt 400 , { :error = > " Subdomain #{ params [ :subdomain ] } is not part of available domains: #{ DOMAINS . join ( ', ' ) } " } . to_json
end
2013-06-16 09:45:02 +02:00
end
2013-06-16 10:21:06 +02:00
if params . has_key? ( " ip " )
unless params [ :ip ] . match / ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) \ .){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ /
halt 400 , { :error = > " IP is invalid: #{ params [ :ip ] } " } . to_json
end
2013-06-16 09:45:02 +02:00
end
end
2013-06-16 00:13:29 +02:00
end
2016-04-26 12:18:16 +02:00
# Main page, return some basic text
2013-06-15 21:57:12 +02:00
get '/' do
2016-04-26 09:56:20 +02:00
content_type 'text/html'
2013-06-16 00:17:33 +02:00
" Wanna play the dynette ? "
2013-06-15 21:57:12 +02:00
end
2013-06-15 21:06:14 +02:00
2017-07-24 01:55:54 +02:00
# Delete interface for user with recovery password
get '/delete' do
f = File . open ( " delete.html " , " r " )
content_type 'text/html'
f . read
end
2016-04-26 12:18:16 +02:00
# Get availables DynDNS domains
2013-06-16 09:45:02 +02:00
get '/domains' do
DOMAINS . to_json
end
2016-04-26 12:18:16 +02:00
# Check for sub-domain vailability
2013-06-16 01:06:25 +02:00
get '/test/:subdomain' do
if entry = Entry . first ( :subdomain = > params [ :subdomain ] )
2013-06-16 10:21:06 +02:00
halt 409 , { :error = > " Subdomain already taken: #{ entry . subdomain } " } . to_json
2013-06-16 01:06:25 +02:00
else
2013-06-16 10:21:06 +02:00
halt 200 , " Domain #{ params [ :subdomain ] } is available " . to_json
2013-06-16 01:06:25 +02:00
end
end
2016-04-26 12:18:16 +02:00
# Register a sub-domain
2013-06-16 10:21:06 +02:00
post '/key/:public_key' do
2013-07-07 13:28:56 +02:00
params [ :public_key ] = Base64 . decode64 ( params [ :public_key ] . encode ( 'ascii-8bit' ) )
2013-06-15 21:57:12 +02:00
# Check params
2013-06-16 09:45:02 +02:00
halt 400 , { :error = > " Please indicate a subdomain " } . to_json unless params . has_key? ( " subdomain " )
2013-06-15 21:57:12 +02:00
# If already exists
2013-06-15 22:19:33 +02:00
if entry = Entry . first ( :subdomain = > params [ :subdomain ] )
2013-06-16 10:21:06 +02:00
halt 409 , { :error = > " Subdomain already taken: #{ entry . subdomain } " } . to_json
2013-06-15 22:19:33 +02:00
end
if entry = Entry . first ( :public_key = > params [ :public_key ] )
2013-06-16 10:21:06 +02:00
halt 409 , { :error = > " Key already exists for domain #{ entry . subdomain } " } . to_json
2013-06-15 22:19:33 +02:00
end
2013-06-15 22:13:57 +02:00
2017-07-24 01:55:54 +02:00
# 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
2017-09-18 19:48:07 +02:00
if params . has_key? ( " key_algo " )
key_algo = params [ :key_algo ]
else # default until we'll one day kill it
key_algo = " hmac-md5 "
end
2013-06-15 22:13:57 +02:00
# Process
2017-09-18 19:48:07 +02:00
entry = Entry . new ( :public_key = > params [ :public_key ] , :subdomain = > params [ :subdomain ] , :current_ip = > request . ip , :created_at = > Time . now , :recovery_password = > recovery_password , :key_algo = > key_algo )
2013-06-15 21:57:12 +02:00
entry . ips << Ip . create ( :ip_addr = > request . ip )
2017-07-24 01:55:54 +02:00
2013-06-15 21:06:14 +02:00
if entry . save
2013-06-16 09:45:02 +02:00
halt 201 , { :public_key = > entry . public_key , :subdomain = > entry . subdomain , :current_ip = > entry . current_ip } . to_json
2013-06-15 21:06:14 +02:00
else
2013-06-16 09:45:02 +02:00
halt 412 , { :error = > " A problem occured during DNS registration " } . to_json
2013-06-15 21:06:14 +02:00
end
end
2017-09-19 19:18:07 +02:00
# Migrate a key from hmac-md5 to hmac-sha512 because it's 2017
2017-09-20 19:19:22 +02:00
put '/migrate_key_to_sha512/' do
2017-09-19 19:18:07 +02:00
# TODO check parameters
params [ :public_key_md5 ] = Base64 . decode64 ( params [ :public_key_md5 ] . encode ( 'ascii-8bit' ) )
params [ :public_key_sha512 ] = Base64 . decode64 ( params [ :public_key_sha512 ] . encode ( 'ascii-8bit' ) )
# TODO signing handling
# TODO check entry exists
entry = Entry . first ( :public_key = > params [ :public_key_md5 ] ,
:key_algo = > " hmac-md5 " )
unless request . ip == entry . current_ip
entry . ips << Ip . create ( :ip_addr = > request . ip )
end
entry . current_ip = request . ip
entry . public_key = params [ :public_key_sha512 ]
entry . key_algo = " hmac-sha512 "
unless entry . save
halt 412 , { :error = > " A problem occured during key algo migration " } . to_json
end
2017-09-21 05:58:49 +02:00
# I don't have any other way of communicating with this dynette.cron.py
# this is awful
File . open ( " /tmp/dynette_flush_bind_cache " , " w " ) . close
2017-10-09 09:42:58 +02:00
# let's try flusing here, hope that could help ... (this design is so awful)
` /usr/sbin/rndc flush `
2017-09-21 05:58:49 +02:00
2017-09-20 05:53:33 +02:00
halt 201 , { :public_key = > entry . public_key , :subdomain = > entry . subdomain , :current_ip = > entry . current_ip } . to_json
2017-09-19 19:18:07 +02:00
end
2016-04-26 12:18:16 +02:00
# Update a sub-domain
2013-06-16 10:21:06 +02:00
put '/key/:public_key' do
2013-07-07 13:28:56 +02:00
params [ :public_key ] = Base64 . decode64 ( params [ :public_key ] . encode ( 'ascii-8bit' ) )
2013-06-15 22:13:57 +02:00
entry = Entry . first ( :public_key = > params [ :public_key ] )
unless request . ip == entry . current_ip
entry . ips << Ip . create ( :ip_addr = > request . ip )
end
entry . current_ip = request . ip
if entry . save
2013-06-16 09:45:02 +02:00
halt 201 , { :public_key = > entry . public_key , :subdomain = > entry . subdomain , :current_ip = > entry . current_ip } . to_json
2013-06-15 22:13:57 +02:00
else
2013-06-16 09:45:02 +02:00
halt 412 , { :error = > " A problem occured during DNS update " } . to_json
2013-06-15 22:13:57 +02:00
end
end
2016-04-26 12:18:16 +02:00
# Delete a sub-domain from key
2013-06-16 10:21:06 +02:00
delete '/key/:public_key' do
2014-05-09 16:33:50 +02:00
unless ALLOWED_IP . include? request . ip
2016-04-26 09:56:20 +02:00
halt 403 , { :error = > " Access denied " } . to_json
2014-05-09 16:33:50 +02:00
end
2013-07-07 13:28:56 +02:00
params [ :public_key ] = Base64 . decode64 ( params [ :public_key ] . encode ( 'ascii-8bit' ) )
2013-06-16 00:13:29 +02:00
if entry = Entry . first ( :public_key = > params [ :public_key ] )
2014-05-09 16:33:50 +02:00
Ip . first ( :entry_id = > entry . id ) . destroy
if entry . destroy
halt 200 , " OK " . to_json
else
halt 412 , { :error = > " A problem occured during DNS deletion " } . to_json
end
end
end
2016-04-26 12:18:16 +02:00
# Delete a sub-domain
2014-05-09 16:33:50 +02:00
delete '/domains/:subdomain' do
2017-07-24 01:55:54 +02:00
unless ( ALLOWED_IP . include? request . ip ) || ( params . has_key? ( " recovery_password " ) )
2016-04-26 09:56:20 +02:00
halt 403 , { :error = > " Access denied " } . to_json
2014-05-09 16:33:50 +02:00
end
if entry = Entry . first ( :subdomain = > params [ :subdomain ] )
2017-07-24 01:55:54 +02:00
# 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
2014-05-09 16:33:50 +02:00
Ip . first ( :entry_id = > entry . id ) . destroy
2013-06-16 09:45:02 +02:00
if entry . destroy
halt 200 , " OK " . to_json
else
halt 412 , { :error = > " A problem occured during DNS deletion " } . to_json
end
2013-06-16 00:13:29 +02:00
end
2017-07-24 01:55:54 +02:00
halt 404
2013-06-16 00:13:29 +02:00
end
2016-04-26 12:18:16 +02:00
# Get all registered sub-domains
2013-06-15 21:06:14 +02:00
get '/all' do
2013-07-07 10:03:15 +02:00
unless ALLOWED_IP . include? request . ip
2016-04-26 09:56:20 +02:00
halt 403 , { :error = > " Access denied " } . to_json
2013-06-15 21:06:14 +02:00
end
Entry . all . to_json
end
2016-04-26 12:18:16 +02:00
# Get all registered sub-domains for a specific DynDNS domain
2013-06-16 10:39:41 +02:00
get '/all/:domain' do
2013-07-07 10:03:15 +02:00
unless ALLOWED_IP . include? request . ip
2016-04-26 09:56:20 +02:00
halt 403 , { :error = > " Access denied " } . to_json
2013-06-16 10:39:41 +02:00
end
result = [ ]
Entry . all . each do | entry |
result . push ( entry ) if params [ :domain ] == entry . subdomain . gsub ( entry . subdomain . split ( '.' ) [ 0 ] + '.' , '' )
end
halt 200 , result . to_json
end
2016-04-26 12:18:16 +02:00
# ?
2013-06-16 10:21:06 +02:00
get '/ips/:public_key' do
2013-07-07 13:28:56 +02:00
params [ :public_key ] = Base64 . decode64 ( params [ :public_key ] . encode ( 'ascii-8bit' ) )
2013-07-07 10:03:15 +02:00
unless ALLOWED_IP . include? request . ip
2016-04-26 09:56:20 +02:00
halt 403 , { :error = > " Access denied " } . to_json
2013-06-15 22:33:45 +02:00
end
2013-06-16 00:13:29 +02:00
ips = [ ]
Entry . first ( :public_key = > params [ :public_key ] ) . ips . all . each do | ip |
ips . push ( ip . ip_addr )
end
ips . to_json
end
2016-04-26 12:18:16 +02:00
# Ban an IP address for 30 seconds
2013-06-16 09:45:02 +02:00
get '/ban/:ip' do
2013-07-07 10:03:15 +02:00
unless ALLOWED_IP . include? request . ip
2016-04-26 09:56:20 +02:00
halt 403 , { :error = > " Access denied " } . to_json
2013-06-16 00:13:29 +02:00
end
2013-06-16 09:45:02 +02:00
Ipban . create ( :ip_addr = > params [ :ip ] )
2013-06-16 00:13:29 +02:00
Ipban . all . to_json
end
2016-04-26 12:18:16 +02:00
# Unban an IP address
2013-06-16 09:45:02 +02:00
get '/unban/:ip' do
2013-07-07 10:03:15 +02:00
unless ALLOWED_IP . include? request . ip
2016-04-26 09:56:20 +02:00
halt 403 , { :error = > " Access denied " } . to_json
2013-06-16 00:13:29 +02:00
end
2013-06-16 09:45:02 +02:00
Ipban . first ( :ip_addr = > params [ :ip ] ) . destroy
2013-06-16 00:13:29 +02:00
Ipban . all . to_json
2013-06-15 22:33:45 +02:00
end
2016-04-26 12:18:16 +02:00
2014-05-09 16:33:50 +02:00
#DataMapper.auto_migrate! # Destroy db content
DataMapper . auto_upgrade!