radd 1.1.0 → 1.3.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: a1b144c324d733fdabbee7eada3e800e3e9c1cba8aee8e2de1a02c67fd6e5a34
4
- data.tar.gz: 27a536a5b377f7762a6c8bf26c9978e27ac08218745cca493af8f45680fc9f10
3
+ metadata.gz: aff371d343007b06413d5aeffa546a25824130d0ef8052b1ae7d300f97b665c7
4
+ data.tar.gz: cb860484d7cb6ad5531be423bffbdabecd96bdc5e9544fe97489cd747a6237f5
5
5
  SHA512:
6
- metadata.gz: f46802e9687b2cd65da92e5866dce85e14a2a8de8fb04b1b1bf29431b485c4979c50f71292d850af787d7e7163a13c5b3282e5ac45548482d66f70ea8d1be5cf
7
- data.tar.gz: d2a5de1d3da7ad395f77eda95bd72f39796bdb7b64c072ca1df336207c064ca405624f0f09142bac1c44ee937436696d0afbfbbb094000f24d4444e5b3d5b825
6
+ metadata.gz: 3eef89dbaad0dfc736443237743a9123452df584cb3c60f1f09a8fe83efe13ba70de21bd1c4ee7fd0e9043477250ed03d08e2a3652cbf4db39f47ac9202b0a36
7
+ data.tar.gz: 5c822bafd10f4fde3714a7eea8051bce9e34197f261ed6eeb8d452ec1a87c0688cd4b6a48ddf1fffdecfc1e71f5976ef850f699bbd7a064d98df07fdd939be53
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,34 +2,103 @@ 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 start
9
+ def load_config(**opts)
8
10
  config = {}
9
11
  parser = OptionParser.new
10
- parser.banner = 'Usage: radd -i IP -d DOMAIN [options]'
11
- parser.on('--ip IP', 'Public IP address') do |ip|
12
- config['ip'] = ip
12
+ parser.banner = 'Usage: radd COMMAND [--config FILE]'
13
+ parser.on('--config FILE', 'Config file') do |file|
14
+ config['file'] = file
13
15
  end
14
- parser.on('--domain DOMAIN', 'Root FQDN') do |domain|
15
- config['domain'] = domain
16
+
17
+ parser.parse!
18
+
19
+ Radd.configure!(config['file'], **opts)
20
+ end
21
+
22
+ def run
23
+ command = ARGV.shift
24
+ unless COMMANDS.include?(command)
25
+ puts "Unknown command #{command}"
26
+ exit(1)
16
27
  end
17
- parser.on('--http-port [PORT]', 'HTTP port') do |port|
18
- config['http_port'] = port
28
+ send(command)
29
+ end
30
+
31
+ def help
32
+ puts <<~EOS
33
+ Usage:
34
+ radd COMMAND [--config FILE]
35
+
36
+ You must specify one of the following commands:
37
+
38
+ setup Create the database
39
+ add Add new record
40
+ list List available records
41
+ start Run the server
42
+
43
+ EOS
44
+ end
45
+
46
+ def setup
47
+ require_relative 'schema'
48
+ load_config(skip_db: true, skip_models: true)
49
+ Radd::Schema.create
50
+ end
51
+
52
+ def add
53
+ load_config
54
+ print "Enter name: "
55
+ name = STDIN.gets.chomp
56
+ print "Password: "
57
+ password = STDIN.gets.chomp
58
+ print "Re-type password: "
59
+ password_confirmation = STDIN.gets.chomp
60
+ raise "Password mismatch!" unless password == password_confirmation
61
+ record = Radd::Record.new(password: password)
62
+ record.name = name
63
+ record.save
64
+ puts "Added record '#{name}'\n"
65
+ end
66
+
67
+ def delete
68
+ load_config
69
+ print "Enter name: "
70
+ name = STDIN.gets.chomp
71
+ if record = Radd::Record.where(name: name).first
72
+ print "Do you really want to delete #{name} (y/N)?"
73
+ confirm = STDIN.gets.chomp
74
+ if 'y' == confirm
75
+ record.delete
76
+ puts "Record #{name} deleted"
77
+ end
78
+ else
79
+ puts "Record #{name} not found"
19
80
  end
20
- parser.on('--dns-port [PORT]', 'DNS port') do |port|
21
- config['dns_port'] = port
81
+ end
82
+
83
+ def list
84
+ load_config
85
+ puts
86
+ records = Radd::Record.all
87
+ tab = [records.map(&:name).map(&:size).max, 24].compact.max
88
+ records.each do |record|
89
+ puts "#{record.name.ljust(tab)} #{record.ip.to_s.ljust(15)} #{record.updated_at || 'never updated'}\n"
22
90
  end
23
- parser.parse!
91
+ puts
92
+ end
24
93
 
25
- Radd.configure!(config)
94
+ def start
95
+ load_config
26
96
  puts "Starting Radd server for #{Radd.domain}"
27
97
 
28
98
  dns, http = Radd::Nameserver.new, Radd::Webserver.new
29
99
 
30
100
  Async do
31
101
  dns_task, http_task = dns.run, http.run
32
-
33
102
  watchdog = Async do
34
103
  sleep(1) while !http_task.failed? && !dns_task.failed?
35
104
  puts "Task failed!"
@@ -0,0 +1,40 @@
1
+ module Radd
2
+
3
+ class << self
4
+ attr_reader :domain, :ip, :db, :http_host, :http_port, :dns_host, :dns_port, :ttl
5
+
6
+ def configure!(file, skip_models: false, skip_db: false)
7
+ file_path = Pathname.new(file || 'radd.yml')
8
+ file_path = Radd.root + file_path unless file_path.absolute?
9
+ raise ConfigurationError, "could not open config file #{file_path}" unless file_path.file?
10
+ config = YAML.load(file_path.read)
11
+ raise ConfigurationError, 'domain missing' unless config['domain']
12
+ @domain = config['domain']
13
+ raise ConfigurationError, 'invalid IP' unless Radd.valid_ip?(config['ip'])
14
+ @ip = config['ip']
15
+ uri = URI.parse("http://#{config['http']}")
16
+ @http_host = uri.host || '127.0.0.1'
17
+ @http_port = uri.port || 3003
18
+ uri = URI.parse("dns://#{config['dns']}")
19
+ @dns_host = uri.host || '0.0.0.0'
20
+ @dns_port = uri.port || 53
21
+ raise ConfigurationError, 'invalid TTL' if config['ttl'] && config['ttl'] < 1
22
+ @ttl = config['ttl'] || 300
23
+ db_path = Pathname.new(config['db'] || 'radd.sqlite3')
24
+ db_path = Radd.root + db_path unless db_path.absolute?
25
+ @db = db_path
26
+ raise ConfigurationError, 'invalid database' if !skip_db && !Radd.db.file?
27
+ #
28
+ # Late loading required by Sequel architecture
29
+ #
30
+ require_relative 'db'
31
+ require_relative 'record' unless skip_models
32
+ end
33
+
34
+ # Check whether +ip+ is a valid IP address string
35
+ def valid_ip?(ip)
36
+ !!(ip && ip.match(Resolv::IPv4::Regex))
37
+ end
38
+ end
39
+
40
+ end
data/lib/radd/db.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  require 'sequel'
2
2
 
3
- DB = Sequel.connect("sqlite://#{Pathname.new(__FILE__).dirname.join('..', '..', 'db', 'radd.sqlite3')}")
3
+ DB = Sequel.connect("sqlite://#{Radd.db}")
@@ -0,0 +1,7 @@
1
+ module Radd
2
+ class RaddError < StandardError; end
3
+ class ConfigurationError < StandardError; end
4
+ class Forbidden < RaddError; end
5
+ class InvalidRequest < RaddError; end
6
+ class UpdateError < RaddError; end
7
+ end
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,6 +1,6 @@
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)
@@ -12,7 +12,7 @@ class Radd::Nameserver < Async::DNS::Server
12
12
  ip = Radd.query(name)
13
13
  end
14
14
  end
15
- return transaction.respond!(ip, ttl: 300) if ip
15
+ return transaction.respond!(ip, ttl: Radd.ttl) if ip
16
16
  transaction.fail!(:NXDomain)
17
17
  end
18
18
  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.1.0'
2
+ VERSION = '1.3.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,52 +10,32 @@ 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'
17
+ require_relative 'radd/errors'
18
+ require_relative 'radd/config'
13
19
  require_relative 'radd/ip'
14
- require_relative 'radd/db'
15
- require_relative 'radd/record'
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'
25
+ require_relative 'radd/cli'
20
26
 
21
27
  module Radd
22
- class RaddError < StandardError; end
23
- class ConfigurationError < StandardError; end
24
- class Forbidden < RaddError; end
25
- class InvalidRequest < RaddError; end
26
- class UpdateError < RaddError; end
27
-
28
28
  class << self
29
- def configure!(config)
30
- @config = config.slice(*%w[domain ip host dns_port http_port])
31
- raise Radd::ConfigurationError, 'domain missing' unless Radd.domain
32
- raise Radd::ConfigurationError, 'invalid IP' unless Radd.valid_ip?(Radd.ip)
33
- end
34
-
35
- def domain
36
- config['domain']
37
- end
38
-
39
- def ip
40
- config['ip']
41
- end
42
-
43
- def host
44
- config['host'] || '127.0.0.1'
29
+ def root=(path)
30
+ @root = Pathname.new(path)
45
31
  end
46
32
 
47
- def dns_port
48
- config['dns_port'] || 5300
49
- end
50
-
51
- def http_port
52
- config['http_port'] || 3000
33
+ def root
34
+ @root ||= Pathname.new(Dir.pwd)
53
35
  end
54
36
 
55
- # Check whether +ip+ is a valid IP address string
56
- def valid_ip?(ip)
57
- !!(ip && ip.match(Resolv::IPv4::Regex))
37
+ def logger
38
+ @logger = Logger.new(STDOUT)
58
39
  end
59
40
 
60
41
  # Check whether +name+ is authorized with +password+
@@ -70,10 +51,6 @@ module Radd
70
51
  return unless record = Record.active.where(name: name).first
71
52
  record.ip
72
53
  end
73
-
74
- def run
75
-
76
- end
77
54
 
78
55
  private
79
56
 
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.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Grosser
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-23 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: optparse
27
+ name: logger
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
@@ -38,7 +38,7 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
40
  - !ruby/object:Gem::Dependency
41
- name: async-http
41
+ name: optparse
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
@@ -52,7 +52,7 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: protocol-rack
55
+ name: async-http
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
@@ -66,7 +66,7 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: async-dns
69
+ name: protocol-rack
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
@@ -80,13 +80,13 @@ dependencies:
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
82
  - !ruby/object:Gem::Dependency
83
- name: irb
83
+ name: async-dns
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
- type: :development
89
+ type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
@@ -149,6 +149,34 @@ dependencies:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: rake
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
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
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
152
180
  description: Minimal dynamic DNS service
153
181
  email: mtgrosser@gmx.net
154
182
  executables:
@@ -156,22 +184,25 @@ executables:
156
184
  extensions: []
157
185
  extra_rdoc_files: []
158
186
  files:
159
- - CHANGELOG
160
187
  - LICENSE
161
188
  - README.md
162
- - bin/console
163
189
  - bin/radd
164
190
  - lib/radd.rb
165
191
  - lib/radd/app.rb
166
192
  - lib/radd/cli.rb
193
+ - lib/radd/config.rb
167
194
  - lib/radd/db.rb
168
195
  - lib/radd/dns.rb
169
196
  - lib/radd/dns_service.rb
197
+ - lib/radd/errors.rb
170
198
  - lib/radd/http.rb
171
199
  - lib/radd/http_service.rb
172
200
  - lib/radd/ip.rb
201
+ - lib/radd/middleware.rb
173
202
  - lib/radd/nameserver.rb
174
203
  - lib/radd/record.rb
204
+ - lib/radd/schema.rb
205
+ - lib/radd/tasks.rb
175
206
  - lib/radd/update.rb
176
207
  - lib/radd/version.rb
177
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
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
- require 'radd'
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require 'irb'
14
- IRB.start