From 320feb3eae1a5eb3beea38a381ff153c5e6eb190 Mon Sep 17 00:00:00 2001 From: Nguyễn Gia Phong Date: Sat, 4 Mar 2023 14:09:16 +0900 Subject: Add applicant table --- src/http.cr | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/http.cr (limited to 'src/http.cr') diff --git a/src/http.cr b/src/http.cr new file mode 100644 index 0000000..63eee07 --- /dev/null +++ b/src/http.cr @@ -0,0 +1,128 @@ +# 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 . + +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: #{Path[PROGRAM_NAME].basename} 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"] + +opennic_page = Page.new cfg["opennic"]["local"], cfg["opennic"]["remote"], + cfg["general"]["api"], db +opennic_page.write +icann_page = Page.new cfg["icann"]["local"], cfg["icann"]["remote"], + cfg["general"]["api"], db +icann_page.write + +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 + + others = db.members.each_value.chain db.applicants.each_value + others.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? + db.add_applicant params["nick"], params["opennic"], params["icann"] + # 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 errors, params + else + context.response.print icann_page.build 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 -- cgit 1.4.1