summary refs log tree commit diff homepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--hybring.cr86
-rw-r--r--style.css13
-rw-r--r--xhtml.cr154
4 files changed, 250 insertions, 6 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..86df8d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.atom
+*.db
+*.xhtml
diff --git a/hybring.cr b/hybring.cr
index 220559f..1c5015e 100644
--- a/hybring.cr
+++ b/hybring.cr
@@ -1,18 +1,30 @@
-# Hybrid web ring
+# Hybrid web ring server
 # Copyright (C) 2023  Nguyễn Gia Phong
 #
-# This program is free software: you can redistribute it and/or modify
+# 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.
 #
-# This program is distributed in the hope that it will be useful,
+# 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+# along with hybring.  If not, see <https://www.gnu.org/licenses/>.
+
+require "http/server"
+require "uri"
+
+require "./xhtml"
+
+MAX_CONTENT_LENGTH = 4096
+OPENNIC_TLD = Set{".bbs", ".chan", ".cyb", ".dyn", ".epic",
+                  ".geek", ".gopher", ".indy", ".libre", ".neo",
+                  ".null", ".o", ".oss", ".oz", ".parody", ".pirate"}
 
 @[Link("sqlite3")]
 lib SQLite
@@ -21,14 +33,76 @@ lib SQLite
   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*,
-                                query_tail : LibC::Char*)
+                                query_tail : LibC::Char*) : LibC::Int
+  fun step = sqlite3_step(stmt : Statement) : Int32
+  fun column_text = sqlite3_column_text(stmt : Statement,
+                                        col : LibC::Int) : LibC::Char*
+  fun finalize = sqlite3_finalize(stmt : Statement) : LibC::Int
   fun close = sqlite3_close(db : Database) : LibC::Int
 end
 
-DB_INIT = "CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY);";
+DB_INIT = "CREATE TABLE member (
+  id INTEGER PRIMARY KEY,
+  nick TEXT NOT NULL UNIQUE,
+  opennic TEXT NOT NULL UNIQUE,
+  icann TEXT NOT NULL UNIQUE,
+);";
 
 SQLite.open "foo.db", out db
 begin
 ensure
   SQLite.close db
 end
+
+def http_error(context, status, message = nil)
+  context.response.respond_with_status status, message
+end
+
+File.write "index.xhtml", page
+server = HTTP::Server.new do |context|
+  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
+
+  body = context.request.body.try &.gets_to_end || ""
+  errors = {} of String => String
+  params = {} of String => String
+  invalid_param = false
+  URI::Params.parse body do |key, value|
+    params[key] = value
+    case key
+    when "nick"
+    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
+    else
+      break invalid_param = true
+    end
+  end
+  next http_error context, 400, "Invalid Parameter" if invalid_param
+
+  context.response.status_code = 400 unless errors.empty?
+  context.response.content_type = "application/xhtml+xml"
+  context.response.print page errors, params
+  # TODO: schedule dynamic check
+end
+
+address = server.bind_tcp 8080
+puts "Listening on http://#{address}"
+server.listen
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..395c35d
--- /dev/null
+++ b/style.css
@@ -0,0 +1,13 @@
+html {
+    margin: auto;
+    max-width: 72ch;
+}
+h1, h2, h3, h4, h5, h6 { margin-bottom: 1ex }
+form {
+    display: grid;
+    grid-template-columns: max-content 1fr;
+}
+form label { margin-right: 1ch }
+form label:after { content: ":" }
+form input { margin-bottom: 1ex }
+.error { color: red }
diff --git a/xhtml.cr b/xhtml.cr
new file mode 100644
index 0000000..2dc8aea
--- /dev/null
+++ b/xhtml.cr
@@ -0,0 +1,154 @@
+# XHTML generation
+# 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 "xml"
+
+def criteria(xml)
+  xml.element "h2" do xml.text "criteria" end
+  xml.element "p" do
+    xml.text "We accept pretty much any site"
+    xml.text " served under an OpenNIC domain"
+    xml.text " meeting the following requirements."
+  end
+  xml.element "ul" do
+    xml.element "li" do xml.text "Family-friendly content" end
+    xml.element "li" do xml.text "No violence incitation or doxing" end
+    xml.element "li" do xml.text "No user tracking" end
+    xml.element "li" do
+      xml.text "ICANN counterpart accessible via HTTPS"
+    end
+    xml.element "li" do
+      xml.text "Both linking to neighboring members"
+    end
+  end
+  xml.element "p" do
+    xml.text "Note that the two sites needn't be the same,"
+    xml.text " but represent the same entity (person or organization)."
+    xml.text "  The clear net site is for serving visitors"
+    xml.text " without OpenNIC access or preferring secure connections."
+  end
+end
+
+def input(xml, name, label, hint, error, value)
+  if error
+    xml.element "span"
+    xml.element "span", class: "error" do xml.text error end
+  end
+  xml.element "label", for: name do xml.text label end
+  xml.element "input", name: name, placeholder: hint, value: value,
+              required: "required"
+end
+
+def form(xml, errors = {} of String => String,
+         params = {} of String => String)
+  # FIXME: get URL from configuration
+  xml.element "form", action: "http://127.0.0.1:8080", method: "POST" do
+    input xml, "nick", "Nickname", "digits and ASCII lowercase letters",
+      errors.fetch("nick", nil), params.fetch("nick", nil)
+    input xml, "opennic", "OpenNIC URL", "e.g. http://example.null",
+      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 "input", type: "submit", value: "Let me in!"
+  end
+end
+
+def member(xml, nick, opennic, icann, feed? = false)
+  xml.element "p" do
+    xml.text "The following membership is pending for approval."
+    xml.text "  Please subscribe to the "
+    # FIXME: get base URL from configuration
+    xml.element "a", href: "http://127.0.0.1:42069/#{nick}.atom" do
+        xml.text "web feed"
+    end
+    xml.text " for further instructions."
+  end if feed?
+  xml.element "h3" do xml.text nick end
+  xml.text "OpenNIC: "
+  xml.element "a", href: opennic do xml.text opennic end
+  xml.element "br"
+  xml.text "ICANN: "
+  xml.element "a", href: icann do xml.text icann end
+  xml.element "br"
+end
+
+def page(errors = {} of String => String,
+         params = {} of String => String) : String
+  XML.build encoding: "UTF-8", indent: "  " do |xml|
+    xml.element "html", xmlns: "http://www.w3.org/1999/xhtml", lang: "en" do
+      xml.element "head" do
+        xml.element "meta", name: "viewport",
+                    content: "width=device-width,initial-scale=1.0"
+        xml.element "link", rel: "icon", href: "data:,"
+        xml.element "link", rel: "stylesheet",
+                    # FIXME: get base URL from configuration
+                    href: "http://127.0.0.1:42069/style.css"
+        xml.element "title" do xml.text "le cercle libre" end
+      end
+      xml.element "body" do
+        xml.element "h1" do xml.text "le cercle libre" end
+        xml.element "p" do
+          xml.text "The Free Circle is a web ring for sites"
+          xml.text " with an OpenNIC domain."
+        end
+        criteria xml
+
+        xml.element "h2" do xml.text "joining" end
+        # FIXME: query database for this
+        left = right = "http://cercle.libre"
+        if params.empty? # static page
+          xml.element "p" do
+            xml.text "First, add "
+            xml.element "a",
+                        href: "https://html.spec.whatwg.org/#the-a-element" do
+                xml.text "anchors"
+            end
+            xml.text " refering to the following neighbors to your sites"
+            xml.text " on both OpenNIC and ICANN domains."
+            xml.text "  Your neighbors will change as people come and go,"
+            xml.text " so you will be given an Atom feed with instructions"
+            xml.text " to keep the hyperlinks up to date."
+          end
+          xml.element "ul" do
+            xml.element "li" do
+              xml.text "Left: "
+              xml.element "a", href: left do xml.text left end
+            end
+            xml.element "li" do
+              xml.text "Right: "
+              xml.element "a", href: right do xml.text right end
+            end
+          end
+
+          xml.element "p" do
+            xml.text "Then, please fill out the form below."
+          end
+          form xml
+          xml.element "h2" do xml.text "members" end
+          # FIXME: query database for this
+          member xml, "self", "http://cercle.libre", "https://khoanh.loang.net"
+        elsif errors.empty?
+          member xml, params["nick"], params["opennic"], params["icann"], true
+        else
+          form xml, errors, params
+        end
+      end
+    end
+  end
+end