summary refs log tree commit diff homepage
path: root/hybring.cr
blob: 4b374698510d3a1380cb9714a51680ee16181085 (plain) (blame)
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
# 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 <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
  type Database = Void*
  type Statement = Void*
  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*) : 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 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|
  # 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

  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 errors, params
  # TODO: schedule dynamic check
end

address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen