# Hybrid web ring 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 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] db = Database.new cfg["path"]["db"] members = db.members page = Page.new cfg["url"]["static"], cfg["url"]["api"] File.write Path[cfg["path"]["static"]] / "index.xhtml", page.build 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 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" 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 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 == 3 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" context.response.print page.build members, errors, params # TODO: schedule dynamic check end # TODO: support Unix socket address = server.bind_tcp cfg["listen"]["port"].to_i puts "Listening on http://#{address}" server.listen