radd 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3839d4afb39cef8bced2dd146aff7ef7a08dd7e130f2bd534cd820856e6f9c90
4
- data.tar.gz: d3ff659868d45d89068ab1628090c7f40370407cd2a162d17ddca5c5bf1c8a21
3
+ metadata.gz: f9fddd23ad9d4e45fcfef31f6e5d976b13511fc6089a3f77267395ff8d62bfb6
4
+ data.tar.gz: 530c66e7a340a9709de98025c4cc697b50d1358371d7ef7e351cd756a30611f8
5
5
  SHA512:
6
- metadata.gz: 5d79c2896c91af7aee9d4d4c4d86f250f03f1f5fcf8233379c47be57eab5d6d519b0dc2008db930326a6420f0afcc91c7aedbe57c0563919b7acb647bc3d259c
7
- data.tar.gz: ad478c2fea9f293b3164d3ab7e73b059b87ce84a2493063962261c0c0b82922d39b0e3a1ac88185b61ccde9738cfb52db997971495900c7aabc6766682a8974c
6
+ metadata.gz: f0a3ff47b7c18794df7a34972dacc6ec4434c9dc1b749797655518e79d5509fbc4d01326d34906c3904c22498663cce5224d4214b593cf91588398c3c29ab6cf
7
+ data.tar.gz: 431664ea22d21ba782c577c940746bfd43083855054cf60befb9b6414d52d3ba281524448eb8639b19a77549440749f43a0528564c6fa0e22992146eaf00175b
data/README.md CHANGED
@@ -1,12 +1,39 @@
1
- radd
2
- ====
1
+ # radd
3
2
 
4
3
  Minimal dynamic DNS service
5
4
 
6
- rake radd:db:create
7
5
 
8
- rake radd:add
6
+ ## Installation
9
7
 
10
- rake radd:list
8
+ ```
9
+ gem install radd
10
+ ```
11
11
 
12
- rake radd:delete
12
+ ## Configuration
13
+
14
+ ```yaml
15
+ # radd.yml
16
+ ip: 10.1.2.3
17
+ http: 127.0.0.1:3000
18
+ dns: 0.0.0.0:53
19
+ domain: example.com
20
+ mname: ns.example.com
21
+ rname: hostmaster.example.com
22
+ db: radd.sqlite3
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```
28
+ radd COMMAND [--config FILE]
29
+ ```
30
+
31
+ You must specify one of the following commands:
32
+
33
+ ```
34
+ setup Create the database
35
+ add Add new record
36
+ delete Delete record
37
+ list List available records
38
+ start Run the server
39
+ ```
data/bin/radd CHANGED
@@ -6,4 +6,4 @@ require 'bundler/setup'
6
6
  require 'radd'
7
7
  require 'radd/cli'
8
8
 
9
- Radd::Cli.start
9
+ Radd::Cli.run
data/lib/radd/app.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  Radd::App = Rack::Builder.app do
2
+ use Radd::Middleware
3
+
2
4
  map '/ip' do
3
5
  run Radd::IP
4
6
  end
data/lib/radd/cli.rb CHANGED
@@ -2,19 +2,100 @@ require 'optparse'
2
2
 
3
3
  module Radd::Cli
4
4
 
5
+ COMMANDS = %w[help setup add delete list start].freeze
6
+
5
7
  class << self
6
8
 
7
- def load_config
9
+ def load_config(**opts)
8
10
  config = {}
9
11
  parser = OptionParser.new
10
- parser.banner = 'Usage: radd --config [FILE]'
12
+ parser.banner = 'Usage: radd COMMAND [--config FILE]'
11
13
  parser.on('--config FILE', 'Config file') do |file|
12
14
  config['file'] = file
13
15
  end
14
16
 
15
17
  parser.parse!
16
18
 
17
- Radd.configure!(config['file'])
19
+ Radd.configure!(config['file'], **opts)
20
+ end
21
+
22
+ def run
23
+ command = ARGV.shift
24
+ if COMMANDS.include?(command)
25
+ send(command)
26
+ elsif command.nil?
27
+ help
28
+ exit 1
29
+ elsif '--help' == command
30
+ help
31
+ else
32
+ puts "Unknown command #{command}"
33
+ exit 1
34
+ end
35
+ end
36
+
37
+ def help
38
+ puts <<~EOS
39
+ Usage:
40
+ radd COMMAND [--config FILE]
41
+
42
+ You must specify one of the following commands:
43
+
44
+ setup Create the database
45
+ add Add new record
46
+ delete Delete record
47
+ list List available records
48
+ start Run the server
49
+
50
+ EOS
51
+ end
52
+
53
+ def setup
54
+ require_relative 'schema'
55
+ load_config(skip_db: true, skip_models: true)
56
+ Radd::Schema.create
57
+ end
58
+
59
+ def add
60
+ load_config
61
+ print "Enter name: "
62
+ name = STDIN.gets.chomp
63
+ print "Password: "
64
+ password = STDIN.gets.chomp
65
+ print "Re-type password: "
66
+ password_confirmation = STDIN.gets.chomp
67
+ raise "Password mismatch!" unless password == password_confirmation
68
+ record = Radd::Record.new(password: password)
69
+ record.name = name
70
+ record.save
71
+ puts "Added record '#{name}'\n"
72
+ end
73
+
74
+ def delete
75
+ load_config
76
+ print "Enter name: "
77
+ name = STDIN.gets.chomp
78
+ if record = Radd::Record.where(name: name).first
79
+ print "Do you really want to delete #{name} (y/N)?"
80
+ confirm = STDIN.gets.chomp
81
+ if 'y' == confirm
82
+ record.delete
83
+ puts "Record #{name} deleted"
84
+ end
85
+ else
86
+ puts "Record #{name} not found"
87
+ end
88
+ end
89
+
90
+ def list
91
+ load_config
92
+ puts
93
+ records = Radd::Record.all
94
+ tab = [records.map(&:name).map(&:size).max, 24].compact.max
95
+ records.each do |record|
96
+ puts "#{record.name.ljust(tab)} #{record.ip.to_s.ljust(15)} #{record.updated_at || 'never updated'}\n"
97
+ end
98
+ puts
18
99
  end
19
100
 
20
101
  def start
@@ -25,7 +106,6 @@ module Radd::Cli
25
106
 
26
107
  Async do
27
108
  dns_task, http_task = dns.run, http.run
28
-
29
109
  watchdog = Async do
30
110
  sleep(1) while !http_task.failed? && !dns_task.failed?
31
111
  puts "Task failed!"
data/lib/radd/config.rb CHANGED
@@ -1,44 +1,38 @@
1
1
  module Radd
2
2
 
3
3
  class << self
4
- def configure!(file)
4
+ attr_reader :domain, :ip, :db, :http_host, :http_port, :dns_host, :dns_port, :ttl,
5
+ :mname, :rname
6
+
7
+ def configure!(file, skip_models: false, skip_db: false)
5
8
  file_path = Pathname.new(file || 'radd.yml')
6
9
  file_path = Radd.root + file_path unless file_path.absolute?
7
- raise Radd::ConfigurationError, "could not open config file #{file_path}" unless file_path.file?
8
- @config = YAML.load(file_path.read).slice(*%w[domain ip host dns_port http_port db])
9
- raise Radd::ConfigurationError, 'domain missing' unless Radd.domain
10
- raise Radd::ConfigurationError, 'invalid IP' unless Radd.valid_ip?(Radd.ip)
11
- db_path = Pathname.new(@config.delete('db') || 'db/radd.sqlite3')
10
+ raise ConfigurationError, "could not open config file #{file_path}" unless file_path.file?
11
+ config = YAML.load(file_path.read)
12
+ raise ConfigurationError, 'domain missing' unless config['domain']
13
+ @domain = config['domain']
14
+ raise ConfigurationError, 'invalid IP' unless Radd.valid_ip?(config['ip'])
15
+ @ip = config['ip']
16
+ uri = URI.parse("http://#{config['http']}")
17
+ @http_host = uri.host || '127.0.0.1'
18
+ @http_port = uri.port || 3003
19
+ uri = URI.parse("dns://#{config['dns']}")
20
+ @dns_host = uri.host || '0.0.0.0'
21
+ @dns_port = uri.port || 53
22
+ raise ConfigurationError, 'invalid TTL' if config['ttl'] && config['ttl'] < 1
23
+ @ttl = config['ttl'] || 300
24
+ raise ConfigurationError, 'master name missing' unless config['mname']
25
+ @mname = config['mname']
26
+ @rname = config['rname'] || "hostmaster.#{domain}"
27
+ db_path = Pathname.new(config['db'] || 'radd.sqlite3')
12
28
  db_path = Radd.root + db_path unless db_path.absolute?
13
- @config['db'] = db_path
14
- raise Radd::ConfigurationError, 'invalid database' unless Radd.db.file?
29
+ @db = db_path
30
+ raise ConfigurationError, 'invalid database' if !skip_db && !Radd.db.file?
31
+ #
15
32
  # Late loading required by Sequel architecture
33
+ #
16
34
  require_relative 'db'
17
- require_relative 'record'
18
- end
19
-
20
- def domain
21
- @config['domain']
22
- end
23
-
24
- def ip
25
- @config['ip']
26
- end
27
-
28
- def db
29
- @config['db']
30
- end
31
-
32
- def host
33
- @config['host'] || '127.0.0.1'
34
- end
35
-
36
- def dns_port
37
- @config['dns_port'] || 5300
38
- end
39
-
40
- def http_port
41
- @config['http_port'] || 3000
35
+ require_relative 'record' unless skip_models
42
36
  end
43
37
 
44
38
  # Check whether +ip+ is a valid IP address string
data/lib/radd/ip.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Radd
2
2
  # IP address query responder
3
3
  IP = Proc.new do |env|
4
- [200, {"Content-Type" => "text/plain"}, [env['REMOTE_ADDR']]]
4
+ [200, {"Content-Type" => "text/plain"}, ["#{env['rack.request'].ip}\n"]]
5
5
  end
6
- end
6
+ end
@@ -0,0 +1,12 @@
1
+ module Radd
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ env["rack.request"] = Rack::Request.new(env)
9
+ @app.call(env)
10
+ end
11
+ end
12
+ end
@@ -1,18 +1,17 @@
1
1
  class Radd::Nameserver < Async::DNS::Server
2
2
  def initialize
3
- super(Async::DNS::Endpoint.for(Radd.host, port: Radd.dns_port))
3
+ super(Async::DNS::Endpoint.for(Radd.dns_host, port: Radd.dns_port))
4
4
  end
5
5
 
6
6
  def process(name, resource_class, transaction)
7
7
  name = name.downcase
8
- if Resolv::DNS::Resource::IN::A == resource_class
9
- if Radd.domain == name
10
- ip = Radd.ip
11
- else
12
- ip = Radd.query(name)
13
- end
8
+ case resource_class
9
+ when Resolv::DNS::Resource::IN::A
10
+ ip = Radd.domain == name ? Radd.ip : Radd.query(name)
11
+ return transaction.respond!(ip, ttl: Radd.ttl) if ip
12
+ when Resolv::DNS::Resource::IN::SOA
13
+ return transaction.respond!(Radd.mname, Radd.rname, Radd.serial, 10800, 1800, 604800, 1800)
14
14
  end
15
- return transaction.respond!(ip, ttl: 300) if ip
16
15
  transaction.fail!(:NXDomain)
17
16
  end
18
17
  end
@@ -0,0 +1,17 @@
1
+ module Radd
2
+ module Schema
3
+ def self.create
4
+ if DB.table_exists?(:records)
5
+ puts "Database #{Radd.db} exists"
6
+ exit(1)
7
+ end
8
+ DB.create_table?(:records) do
9
+ String :name, primary_key: true
10
+ String :password_hash
11
+ String :ip
12
+ DateTime :updated_at
13
+ end
14
+ puts "Created database #{Radd.db}"
15
+ end
16
+ end
17
+ end
data/lib/radd/tasks.rb ADDED
@@ -0,0 +1,39 @@
1
+ namespace :radd do
2
+ task :config do
3
+ Radd::Cli.load_config
4
+ end
5
+
6
+ namespace :db do
7
+ desc 'Create database'
8
+ task create: :config do
9
+ load './db/schema.rb'
10
+ end
11
+ end
12
+
13
+ desc 'Add record'
14
+ task add: :config do
15
+ print "Enter name: "
16
+ name = STDIN.gets.chomp
17
+ print "Password: "
18
+ password = STDIN.gets.chomp
19
+ print "Re-type password: "
20
+ password_confirmation = STDIN.gets.chomp
21
+ raise "Password mismatch!" unless password == password_confirmation
22
+ record = Radd::Record.new(password: password)
23
+ record.name = name
24
+ record.save
25
+ puts "Added record '#{name}'\n"
26
+ end
27
+
28
+ desc 'List all records'
29
+ task list: :config do
30
+ puts
31
+ records = Radd::Record.all
32
+ tab = [records.map(&:name).map(&:size).max, 24].compact.max
33
+ records.each do |record|
34
+ puts "#{record.name.ljust(tab)} #{record.ip.to_s.ljust(15)} #{record.updated_at || 'never updated'}\n"
35
+ end
36
+ puts
37
+ end
38
+
39
+ end
data/lib/radd/update.rb CHANGED
@@ -15,7 +15,14 @@ module Radd
15
15
  end
16
16
 
17
17
  def ip
18
- addr = env['REMOTE_ADDR']
18
+ @ip ||= begin
19
+ addr = env['rack.request'].params['ip'] || remote_ip
20
+ addr && Radd.valid_ip?(addr) && addr
21
+ end
22
+ end
23
+
24
+ def remote_ip
25
+ addr = env['rack.request'].ip
19
26
  addr && Radd.valid_ip?(addr) && addr
20
27
  end
21
28
 
@@ -24,6 +31,7 @@ module Radd
24
31
  raise InvalidRequest.new('Invalid IP address') unless ip
25
32
  record.ip = ip
26
33
  record.save
34
+ Radd.logger.info "Updated record #{record.name} to #{ip} from #{remote_ip}"
27
35
  [200, {'Content-Type' => 'text/plain'}, ["OK #{ip}"]]
28
36
  rescue RaddError => boom
29
37
  status = case boom
@@ -32,8 +40,10 @@ module Radd
32
40
  else
33
41
  500
34
42
  end
43
+ Radd.logger.error boom
35
44
  respond status, "ERROR #{boom.message}"
36
45
  rescue Exception => e
46
+ Radd.logger.error e
37
47
  respond 500, "ERROR"
38
48
  end
39
49
 
data/lib/radd/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Radd
2
- VERSION = '1.2.0'
2
+ VERSION = '1.4.0'
3
3
  end
@@ -1,6 +1,9 @@
1
1
  class Radd::Webserver < Async::HTTP::Server
2
2
  def initialize
3
- endpoint = Async::HTTP::Endpoint.parse("http://#{Radd.host}:#{Radd.http_port}")
3
+ endpoint = Async::HTTP::Endpoint.parse("http://#{Radd.http_host}:#{Radd.http_port}")
4
+ # TODO: support UNIX endpoints
5
+ # endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1")
6
+ # endpoint.endpoint = IO::Endpoint.unix("/tmp/radd1.sock")
4
7
  middleware = Protocol::Rack::Adapter.new(Radd::App)
5
8
  super(middleware, endpoint)
6
9
  end
data/lib/radd.rb CHANGED
@@ -2,6 +2,7 @@ require 'yaml'
2
2
  require 'sequel'
3
3
  require 'resolv'
4
4
  require 'bcrypt'
5
+ require 'logger'
5
6
 
6
7
  require 'async'
7
8
  require 'async/dns'
@@ -9,11 +10,15 @@ require 'async/http/server'
9
10
  require 'async/http/endpoint'
10
11
  require 'protocol/rack/adapter'
11
12
 
13
+ # require 'io/endpoint'
14
+ # require 'io/endpoint/unix_endpoint'
15
+
12
16
  require_relative 'radd/version'
13
17
  require_relative 'radd/errors'
14
18
  require_relative 'radd/config'
15
19
  require_relative 'radd/ip'
16
20
  require_relative 'radd/update'
21
+ require_relative 'radd/middleware'
17
22
  require_relative 'radd/nameserver'
18
23
  require_relative 'radd/webserver'
19
24
  require_relative 'radd/app'
@@ -24,11 +29,15 @@ module Radd
24
29
  def root=(path)
25
30
  @root = Pathname.new(path)
26
31
  end
27
-
32
+
28
33
  def root
29
34
  @root ||= Pathname.new(Dir.pwd)
30
35
  end
31
-
36
+
37
+ def logger
38
+ @logger = Logger.new(STDOUT)
39
+ end
40
+
32
41
  # Check whether +name+ is authorized with +password+
33
42
  def authorized?(name, password)
34
43
  return false unless record = Record.where(name: name).first
@@ -42,7 +51,11 @@ module Radd
42
51
  return unless record = Record.active.where(name: name).first
43
52
  record.ip
44
53
  end
45
-
54
+
55
+ def serial
56
+ Radd.db&.stat&.mtime.to_i
57
+ end
58
+
46
59
  private
47
60
 
48
61
  def config
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: radd
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Grosser
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-28 00:00:00.000000000 Z
10
+ date: 2026-04-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rake
13
+ name: yaml
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
@@ -24,7 +24,7 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: yaml
27
+ name: logger
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
@@ -94,13 +94,13 @@ dependencies:
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
96
  - !ruby/object:Gem::Dependency
97
- name: irb
97
+ name: rack
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
102
  version: '0'
103
- type: :development
103
+ type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
@@ -108,7 +108,7 @@ dependencies:
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0'
110
110
  - !ruby/object:Gem::Dependency
111
- name: rack
111
+ name: sequel
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - ">="
@@ -122,7 +122,7 @@ dependencies:
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
124
  - !ruby/object:Gem::Dependency
125
- name: sequel
125
+ name: sqlite3
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - ">="
@@ -136,7 +136,7 @@ dependencies:
136
136
  - !ruby/object:Gem::Version
137
137
  version: '0'
138
138
  - !ruby/object:Gem::Dependency
139
- name: sqlite3
139
+ name: bcrypt
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - ">="
@@ -150,13 +150,27 @@ dependencies:
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
152
  - !ruby/object:Gem::Dependency
153
- name: bcrypt
153
+ name: rake
154
154
  requirement: !ruby/object:Gem::Requirement
155
155
  requirements:
156
156
  - - ">="
157
157
  - !ruby/object:Gem::Version
158
158
  version: '0'
159
- type: :runtime
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: irb
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ type: :development
160
174
  prerelease: false
161
175
  version_requirements: !ruby/object:Gem::Requirement
162
176
  requirements:
@@ -170,7 +184,6 @@ executables:
170
184
  extensions: []
171
185
  extra_rdoc_files: []
172
186
  files:
173
- - CHANGELOG
174
187
  - LICENSE
175
188
  - README.md
176
189
  - bin/radd
@@ -185,8 +198,11 @@ files:
185
198
  - lib/radd/http.rb
186
199
  - lib/radd/http_service.rb
187
200
  - lib/radd/ip.rb
201
+ - lib/radd/middleware.rb
188
202
  - lib/radd/nameserver.rb
189
203
  - lib/radd/record.rb
204
+ - lib/radd/schema.rb
205
+ - lib/radd/tasks.rb
190
206
  - lib/radd/update.rb
191
207
  - lib/radd/version.rb
192
208
  - lib/radd/webserver.rb
data/CHANGELOG DELETED
@@ -1,7 +0,0 @@
1
- * 0.2.0
2
-
3
- Use RubyDNS
4
-
5
- * 0.1.0
6
-
7
- Use knot DNS