💾 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

View Raw

More Information

⬅️ Previous capture (2023-03-20)

➡️ Next capture (2023-09-08)

-=-=-=-=-=-=-

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