💾 Archived View for gemini.rmf-dev.com › repo › Vaati › gmiChat › files › 5d6213a81dade17c62dcbd7623… captured on 2023-05-24 at 18:06:48. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-03-20)
-=-=-=-=-=-=-
0 require Logger
1
2 # Gemini Server
3
4 defmodule Gmi do
5
6 def init() do
7 :routes = :ets.new(:routes, [:set, :protected, :named_table])
8 end
9
10 def verify_fun(_, {:extension, _}, state) do
11 {:unknown, state}
12 end
13
14 def verify_fun(_, _, state) do
15 {:valid, state}
16 end
17
18 def listen(port \\ 1965) do
19 :ok = :ssl.start()
20 {:ok, socket} = :ssl.listen(port, [
21 certfile: "cert.pem",
22 keyfile: "key.pem",
23 active: false,
24 binary: true,
25 packet: :line,
26 reuseaddr: true,
27 verify_fun: {&Gmi.verify_fun/3, {0, 0}},
28 verify: :verify_peer
29 ])
30 Logger.info("Listening on port #{port}")
31
32 accept(socket)
33 end
34
35 defp accept(socket) do
36 {:ok, client} = :ssl.transport_accept(socket)
37 spawn(fn -> handshake(client) end)
38 accept(socket)
39 end
40
41 defp getaddr(socket) do
42 {:ok, {addr, port}} = :ssl.peername(socket)
43 " [#{elem(addr, 0)}.#{elem(addr, 1)}.#{elem(addr, 2)}.#{elem(addr, 3)}:#{port}]"
44 end
45
46 defp get_cert(socket) do
47 :ssl.peercert(socket)
48 end
49
50 defp handshake(socket) do
51 try do
52 {:ok, client} = :ssl.handshake(socket)
53 read(client)
54 rescue
55 e ->
56 Logger.error("Client handshake failure :" <>
57 getaddr(socket) <> "\n" <>
58 Exception.format(:error, e, __STACKTRACE__))
59 :ssl.close(socket)
60 end
61 end
62
63 def add_route(route, func) do
64 :ets.insert(:routes, {get_route(rmslash(route)), func})
65 end
66
67 defp get_route_iter(url, route, start, n, url_str) do
68 cond do
69 url == [] && n == 0 ->
70 []
71 url == [] or (hd(url) == ?/ && n != start) ->
72 str = String.slice(url_str, start..n - 1)
73 route =
74 if String.first(str) == ":" do
75 route ++ [String.to_atom(String.slice(str, 1..-1))]
76 else route ++ [str] end
77 if url == [] do
78 route
79 else
80 get_route_iter(tl(url), route, n + 1, n + 1, url_str)
81 end
82 hd(url) == ?/ && n == start ->
83 get_route_iter(tl(url), route, start + 1, n + 1, url_str)
84 true ->
85 get_route_iter(tl(url), route, start, n + 1, url_str)
86 end
87 end
88
89 defp get_route(url) do
90 get_route_iter(to_charlist(url), [], 0, 0, url)
91 end
92
93 defp format_query(query, start \\ 0, n \\ 0, out \\ "", len \\ -1) do
94 len = if len == -1 do String.length(query) else len end
95 cond do
96 len == n ->
97 out <> String.slice(query, start, n - start)
98 String.at(query, n) == "%" ->
99 format_query(query, n + 3, n + 3,
100 out <> String.slice(query, start, n - start) <>
101 List.to_string([elem(
102 Integer.parse(String.slice(query, n + 1, 2), 16), 0
103 )]),
104 len)
105 true ->
106 format_query(query, start, n + 1, out, len)
107 end
108 end
109
110 defp rmhost(data) do
111 if String.length(data) == 0 || String.first(data) == "/" do
112 data
113 else
114 rmhost(String.slice(data, 1..-1))
115 end
116 end
117
118 defp rmslash(data) do
119 if String.length(data) > 0 && String.last(data) == "/" do
120 rmslash(String.slice(data, 0..-2))
121 else
122 data
123 end
124 end
125
126 defp rmquery(data, start \\ -1) do
127 n = fn ->
128 if start == -1 do
129 String.length(data) - 1
130 else
131 start
132 end
133 end.()
134 cond do
135 n <= 0 ->
136 {data, ""}
137 String.at(data, n) == "?" ->
138 {String.slice(data, 0, n), String.slice(data, n + 1..-1)}
139 true ->
140 rmquery(data, n - 1)
141 end
142 end
143
144 defp get_url(data) do
145 cond do
146 String.slice(data, 0..8) != "gemini://" ->
147 {:error, "Invalid request, no protocol specification"}
148 String.slice(data, String.length(data) - 1, 2) != "\r\n" ->
149 {:error, "Invalid request, no CRLF"}
150 true ->
151 rmquery(rmhost(rmslash(String.slice(data, 9..-2))))
152 end
153 end
154
155 defp compare_route(routes, route, generic \\ []) do
156 l1 = length(routes)
157 l2 = length(route)
158 cond do
159 l1 != l2 ->
160 nil
161 l1 == 0 ->
162 []
163 !is_atom(hd(routes)) && hd(routes) != hd(route) ->
164 nil
165 true ->
166 generic = if is_atom(hd(routes)) do
167 generic ++ [{hd(routes), hd(route)}]
168 else generic end
169 if tl(routes) == [] do
170 generic
171 else
172 compare_route(tl(routes), tl(route), generic)
173 end
174 end
175 end
176
177 defp get_generic(route, routes) do
178 generic = compare_route(elem(hd(routes), 0), route)
179 cond do
180 generic != nil ->
181 {hd(routes), generic}
182 tl(routes) == [] ->
183 {nil, nil}
184 true ->
185 get_generic(route, tl(routes))
186 end
187 end
188
189 defp read(socket) do
190 try do
191 {state, data} = :ssl.recv(socket, 1024)
192 if state == :ok do
193 {url, query} = get_url(data)
194 if url == :error do
195 raise url
196 end
197
198 table = :ets.select(:routes, [{:"$1", [], [:"$1"]}])
199 {route, args} = get_generic(get_route(url), table)
200
201 url = if url != "" do url else url <> "/" end
202 url = if query == "" do url else url <> "?<*>" end
203 {has_cert, cert} = get_cert(socket)
204 args = if has_cert == :ok and is_list(args) do
205 args ++ [{:cert, :crypto.hash(:sha, cert) |> Base.encode16}]
206 else args end
207 {:ok, addr} = :ssl.peername(socket)
208 args = if args == nil do [] else args end
209 args = args ++ [{:addr, addr}, {:query, format_query(query)}]
210
211 if route == nil do
212 Logger.info("Not found : " <> url <> getaddr(socket))
213 :ssl.send(socket, "59 Page not found\r\n")
214 else
215 Logger.info("Request : " <> url <> getaddr(socket))
216 :ssl.send(socket, elem(route, 1).(args))
217 end
218 else
219 Logger.error("Client read failure : " <> data <> getaddr(socket))
220 end
221 rescue
222 e ->
223 Logger.error("Client read failure :" <>
224 getaddr(socket) <> "\n" <>
225 Exception.format(:error, e, __STACKTRACE__))
226 after
227 :ssl.close(socket)
228 end
229 end
230
231 def content(data) do
232 "20 text/gemini\r\n" <> data
233 end
234
235 def input(data) do
236 "10 " <> data <> "\r\n"
237 end
238
239 def input_secret(data) do
240 "11 " <> data <> "\r\n"
241 end
242
243 def redirect(data) do
244 "30 " <> data <> "\r\n"
245 end
246
247 def failure(data) do
248 "40 " <> data <> "\r\n"
249 end
250
251 def bad_request(data) do
252 "59 " <> data <> "\r\n"
253 end
254
255 def cert_required(data) do
256 "60 " <> data <> "\r\n"
257 end
258
259 end
260