1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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 <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: #{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
|