summary refs log tree commit diff homepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--http.cr (renamed from hybring.cr)34
-rw-r--r--sqlite.cr17
-rw-r--r--xhtml.cr14
4 files changed, 52 insertions, 16 deletions
diff --git a/.gitignore b/.gitignore
index 66533b2..272ac67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-static/
-*.db
+data/
 *.ini
diff --git a/hybring.cr b/http.cr
index 13ba893..c1de4dc 100644
--- a/hybring.cr
+++ b/http.cr
@@ -1,4 +1,4 @@
-# Hybrid web ring server
+# HTTP server
 # Copyright (C) 2023  Nguyễn Gia Phong
 #
 # This file if part of hybring.
@@ -24,6 +24,7 @@ require "./sqlite"
 require "./xhtml"
 
 MAX_CONTENT_LENGTH = 4096
+MAX_NICK_LENGTH = 32
 OPENNIC_TLD = Set{".bbs", ".chan", ".cyb", ".dyn", ".epic",
                   ".geek", ".gopher", ".indy", ".libre", ".neo",
                   ".null", ".o", ".oss", ".oz", ".parody", ".pirate"}
@@ -37,10 +38,19 @@ if ARGV.empty?
   exit 1
 end
 cfg = INI.parse File.read ARGV[0]
-db = Database.new cfg["path"]["db"]
+opennic_host = URI.parse(cfg["opennic"]["remote"]).host
+
+Dir.mkdir_p Path[cfg["general"]["db"]].parent
+db = Database.new cfg["general"]["db"],
+                  cfg["opennic"]["remote"], cfg["icann"]["remote"]
 members = db.members
-page = Page.new cfg["url"]["static"], cfg["url"]["api"]
-File.write Path[cfg["path"]["static"]] / "index.xhtml", page.build members
+
+opennic_page = Page.new cfg["opennic"]["local"], cfg["opennic"]["remote"],
+                        cfg["general"]["api"]
+opennic_page.write members
+icann_page = Page.new cfg["icann"]["local"], cfg["icann"]["remote"],
+                      cfg["general"]["api"]
+icann_page.write members
 
 server = HTTP::Server.new do |context|
   # Manually crafted request
@@ -49,14 +59,17 @@ server = HTTP::Server.new do |context|
   next http_error context, 411 unless content_length
   next http_error context, 413 if content_length > MAX_CONTENT_LENGTH
 
-  body = context.request.body.try &.gets_to_end || ""
   errors = {} of String => String
   params = {} of String => String
   invalid_param = false
+  body = context.request.body.try &.gets_to_end || ""
   URI::Params.parse body do |key, value|
     params[key] = value
     case key
     when "nick"
+      if value.size > MAX_NICK_LENGTH
+        next errors["nick"] = "Must be within ${MAX_NICK_LENGTH} characters"
+      end
       if /^[0-9a-z]+$/ !~ value
         next errors["nick"] = "Must be ASCII lowercase alphanumeric"
       end
@@ -78,6 +91,7 @@ server = HTTP::Server.new do |context|
       if OPENNIC_TLD.includes? Path[host].extension
         next errors["icann"] = "Must not be under OpenNIC domain"
       end if host
+    when "host"
     else
       break invalid_param = true
     end
@@ -85,7 +99,7 @@ server = HTTP::Server.new do |context|
 
   # Manually crafted request
   next http_error context, 400, "Invalid Parameter" if invalid_param
-  next http_error context, 400, "Missing Parameter" unless params.size == 3
+  next http_error context, 400, "Missing Parameter" unless params.size == 4
 
   members.each do |nick, opennic, icann|
     errors["nick"] = "Must be unique" if nick == params["nick"]
@@ -99,11 +113,15 @@ server = HTTP::Server.new do |context|
     context.response.status_code = 400 unless errors.empty?
   end
   context.response.content_type = "application/xhtml+xml"
-  context.response.print page.build members, errors, params
+  if params["host"] == opennic_host
+    context.response.print opennic_page.build members, errors, params
+  else
+    context.response.print icann_page.build members, errors, params
+  end
   # TODO: schedule dynamic check
 end
 
 # TODO: support Unix socket
-address = server.bind_tcp cfg["listen"]["port"].to_i
+address = server.bind_tcp cfg["general"]["port"].to_i
 puts "Listening on http://#{address}"
 server.listen
diff --git a/sqlite.cr b/sqlite.cr
index 4cca647..5dfbe4d 100644
--- a/sqlite.cr
+++ b/sqlite.cr
@@ -26,6 +26,9 @@ lib SQLite
   type Statement = Void*
 
   fun errstr = sqlite3_errstr(rc : LibC::Int) : LibC::Char*
+  fun mprintf = sqlite3_mprintf(format : LibC::Char*, ...) : LibC::Char*
+  fun free = sqlite3_free(format : Void*, ...)
+
   fun open = sqlite3_open(filename : LibC::Char*, db : Database*) : LibC::Int
   fun prepare = sqlite3_prepare(db : Database, query : LibC::Char*,
                                 length : LibC::Int, stmt : Statement*,
@@ -45,7 +48,9 @@ class Database
     id INTEGER PRIMARY KEY,
     nick TEXT NOT NULL UNIQUE,
     opennic TEXT NOT NULL UNIQUE,
-    icann TEXT NOT NULL UNIQUE)";
+    icann TEXT NOT NULL UNIQUE)"
+  INSERT_MEMBER = "INSERT INTO member (nick, opennic, icann)
+    VALUES (%Q, %Q, %Q)"
 
   class Statement
     def initialize(db, query)
@@ -92,7 +97,7 @@ class Database
     end
   end
 
-  def initialize(path : String)
+  def initialize(path : String, opennic, icann)
     Database.check SQLite.open path, out @ref
     self.exec "PRAGMA user_version" do |row|
       version = row[0].int
@@ -101,6 +106,8 @@ class Database
       if version == 0
         self.exec SCHEMA do end
         self.exec "PRAGMA user_version = #{MIGRATIONS.size}" do end
+        # Avoid out-of-bound when looking for neighbors.
+        self.exec INSERT_MEMBER, "self", opennic, icann do end
       end
     rescue ex
       self.finalize
@@ -108,8 +115,10 @@ class Database
     end
   end
 
-  def exec(query : String)
-    stmt = Statement.new @ref, query
+  def exec(query : String, *values)
+    sql = SQLite.mprintf query, *values
+    stmt = Statement.new @ref, String.new sql
+    SQLite.free sql
     loop do
       rc = stmt.step
       case rc
diff --git a/xhtml.cr b/xhtml.cr
index 7e21309..4749af1 100644
--- a/xhtml.cr
+++ b/xhtml.cr
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with hybring.  If not, see <https://www.gnu.org/licenses/>.
 
+require "uri"
 require "xml"
 
 CSS = "
@@ -35,8 +36,11 @@ CSS = "
     "
 
 class Page
-  def initialize(static_url : String, api_url : String)
+  def initialize(static_dir : String, static_url : String, api_url : String)
+    Dir.mkdir_p static_dir
+    @static_file = Path[static_dir] / "index.xhtml"
     @static_url = static_url
+    @static_host = URI.parse(static_url).host
     @api_url = api_url
   end
 
@@ -87,7 +91,9 @@ class Page
         errors.fetch("opennic", nil), params.fetch("opennic", nil)
       input xml, "icann", "ICANN URL", "e.g. https://example.net",
         errors.fetch("icann", nil), params.fetch("icann", nil)
-      xml.element "span"
+      xml.element "span" do
+        xml.element "input", type: "hidden", name: "host", value: @static_host
+      end
       xml.element "input", type: "submit", value: "Let me in!"
       xml.element "br"
     end
@@ -172,4 +178,8 @@ class Page
       end
     end
   end
+
+  def write(members)
+    File.write @static_file, self.build members
+  end
 end