summary refs log tree commit diff homepage
path: root/http.cr
diff options
context:
space:
mode:
authorNguyễn Gia Phong <mcsinyx@disroot.org>2023-02-23 01:28:29 +0900
committerNguyễn Gia Phong <mcsinyx@disroot.org>2023-02-23 02:28:41 +0900
commit2be9ea60d7f171ee3375258b774191f4cf911752 (patch)
tree073a0adc138bdd99613b60572f420df1053b0a6b /http.cr
parent9310cff78765b2a8588aaa19e8fb10d6183f0979 (diff)
downloadhybring-2be9ea60d7f171ee3375258b774191f4cf911752.tar.gz
Handle requests to both ICANN and OpenNIC hosts
Diffstat (limited to 'http.cr')
-rw-r--r--http.cr127
1 files changed, 127 insertions, 0 deletions
diff --git a/http.cr b/http.cr
new file mode 100644
index 0000000..c1de4dc
--- /dev/null
+++ b/http.cr
@@ -0,0 +1,127 @@
+# HTTP server
+# Copyright (C) 2023  Nguyễn Gia Phong
+#
+# This file if part of hybring.
+#
+# Hybring is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Hybring is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# 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 "http/server"
+require "ini"
+require "uri"
+
+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"}
+
+def http_error(context, status, message = nil)
+  context.response.respond_with_status status, message
+end
+
+if ARGV.empty?
+  puts "usage: #{PROGRAM_NAME} config.ini"
+  exit 1
+end
+cfg = INI.parse File.read ARGV[0]
+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
+
+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
+  next http_error context, 405 if context.request.method != "POST"
+  content_length = context.request.content_length
+  next http_error context, 411 unless content_length
+  next http_error context, 413 if content_length > MAX_CONTENT_LENGTH
+
+  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
+    when "opennic"
+      uri = URI.parse value
+      next errors["opennic"] = "Must be absolute URL" unless uri.absolute?
+      if uri.scheme != "http" && uri.scheme != "https"
+        next errors["opennic"] = "Must be HTTP/S"
+      end
+      host = uri.host
+      unless OPENNIC_TLD.includes? Path[host].extension
+        next errors["opennic"] = "Must be under OpenNIC domain"
+      end if host
+    when "icann"
+      uri = URI.parse value
+      next errors["icann"] = "Must be absolute URL" unless uri.absolute?
+      next errors["icann"] = "Must be HTTPS" unless uri.scheme == "https"
+      host = uri.host # impractical to check for ICANN TLD
+      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
+  end
+
+  # Manually crafted request
+  next http_error context, 400, "Invalid Parameter" if invalid_param
+  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"]
+    errors["opennic"] = "Must be unique" if opennic == params["opennic"]
+    errors["icann"] = "Must be unique" if icann == params["icann"]
+  end
+
+  if errors.empty?
+    # TODO: write feed
+  else
+    context.response.status_code = 400 unless errors.empty?
+  end
+  context.response.content_type = "application/xhtml+xml"
+  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["general"]["port"].to_i
+puts "Listening on http://#{address}"
+server.listen