Commit | Line | Data |
---|---|---|
ad64fbfa MW |
1 | /* -*-scala-*- |
2 | * | |
3 | * The database of known peers | |
4 | * | |
5 | * (c) 2018 Straylight/Edgeware | |
6 | */ | |
7 | ||
8 | /*----- Licensing notice --------------------------------------------------* | |
9 | * | |
10 | * This file is part of the Trivial IP Encryption (TrIPE) Android app. | |
11 | * | |
12 | * TrIPE is free software: you can redistribute it and/or modify it under | |
13 | * the terms of the GNU General Public License as published by the Free | |
14 | * Software Foundation; either version 3 of the License, or (at your | |
15 | * option) any later version. | |
16 | * | |
17 | * TrIPE is distributed in the hope that it will be useful, but WITHOUT | |
18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
19 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
20 | * for more details. | |
21 | * | |
22 | * You should have received a copy of the GNU General Public License | |
23 | * along with TrIPE. If not, see <https://www.gnu.org/licenses/>. | |
24 | */ | |
25 | ||
8eabb4ff MW |
26 | package uk.org.distorted.tripe; package object peers { |
27 | ||
ad64fbfa MW |
28 | /*----- Imports -----------------------------------------------------------*/ |
29 | ||
8eabb4ff MW |
30 | import java.io.{BufferedReader, File, FileReader, Reader}; |
31 | import java.net.{InetAddress, Inet4Address, Inet6Address, | |
32 | UnknownHostException}; | |
33 | ||
34 | import scala.collection.mutable.{HashMap, HashSet}; | |
35 | import scala.concurrent.Channel; | |
36 | import scala.util.control.Breaks; | |
37 | import scala.util.matching.Regex; | |
38 | ||
ad64fbfa MW |
39 | /*----- Handy regular expressions -----------------------------------------*/ |
40 | ||
41 | private final val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r; | |
42 | private final val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r; | |
43 | private final val RX_ASSGN = """(?x) ^ | |
8eabb4ff MW |
44 | ([^\s:=] (?: [^:=]* [^\s:=])?) |
45 | \s* [:=] \s* | |
46 | (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*) | |
47 | (?: \s+ (?: [;\#].*)? )? $""".r; | |
ad64fbfa | 48 | private final val RX_CONT = """(?x) ^ \s+ |
8eabb4ff MW |
49 | (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*) |
50 | (?: \s+ (?: [;\#].*)? )? $""".r; | |
ad64fbfa MW |
51 | private final val RX_REF = """(?x) \$ \( ([^)]+) \)""".r; |
52 | private final val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r; | |
53 | private final val RX_PARENT = """(?x) [^\s,]+""".r | |
54 | ||
55 | /*----- Name resolution ---------------------------------------------------*/ | |
8eabb4ff | 56 | |
ad64fbfa MW |
57 | private object BulkResolver { |
58 | private val BREAK = new Breaks; | |
8eabb4ff MW |
59 | } |
60 | ||
ad64fbfa MW |
61 | private class BulkResolver(val nthreads: Int = 8) { |
62 | import BulkResolver.BREAK.{breakable, break}; | |
63 | ||
8eabb4ff MW |
64 | class Host(val name: String) { |
65 | var a4, a6: Seq[InetAddress] = Seq.empty; | |
66 | ||
67 | def addaddr(a: InetAddress) { a match { | |
68 | case _: Inet4Address => a4 +:= a; | |
69 | case _: Inet6Address => a6 +:= a; | |
70 | case _ => (); | |
71 | } } | |
72 | ||
73 | def get(flags: String): Seq[InetAddress] = { | |
74 | var wanta4, wanta6, any, all = false; | |
75 | var b = Seq.newBuilder[InetAddress]; | |
76 | for (ch <- flags) ch match { | |
77 | case '*' => all = true; | |
78 | case '4' => wanta4 = true; any = true; | |
79 | case '6' => wanta6 = true; any = true; | |
80 | case _ => ??? | |
81 | } | |
82 | if (!any) { wanta4 = true; wanta6 = true; } | |
83 | if (wanta4) b ++= a4; | |
84 | if (wanta6) b ++= a6; | |
85 | (all, b.result) match { | |
86 | case (true, aa) => aa | |
87 | case (false, aa@(Nil | Seq(_))) => aa | |
88 | case (false, Seq(a, _*)) => Seq(a) | |
89 | } | |
90 | } | |
91 | } | |
ad64fbfa | 92 | |
8eabb4ff MW |
93 | val ch = new Channel[Host]; |
94 | val map = HashMap[String, Host](); | |
95 | var preparing = true; | |
96 | ||
97 | val workers = Array.tabulate(nthreads) { i => | |
98 | thread(s"resolver worker #$i") { | |
c8292b34 MW |
99 | loopUnit { exit => |
100 | val host = ch.read; if (host == null) exit; | |
8eabb4ff | 101 | println(s";; ${Thread.currentThread.getName} resolving `${host.name}'"); |
c8292b34 MW |
102 | try { |
103 | for (a <- InetAddress.getAllByName(host.name)) host.addaddr(a); | |
104 | } catch { case e: UnknownHostException => () } | |
8eabb4ff MW |
105 | } |
106 | println(s";; ${Thread.currentThread.getName} done'"); | |
107 | ch.write(null); | |
108 | } | |
109 | } | |
110 | ||
111 | def prepare(name: String) { | |
112 | println(s";; prepare host `$name'"); | |
113 | assert(preparing); | |
114 | if (!(map contains name)) { | |
115 | val host = new Host(name); | |
116 | map(name) = host; | |
117 | ch.write(host); | |
118 | } | |
119 | } | |
120 | ||
121 | def finish() { | |
122 | assert(preparing); | |
123 | preparing = false; | |
124 | ch.write(null); | |
125 | for (t <- workers) t.join(); | |
126 | } | |
127 | ||
128 | def resolve(name: String, flags: String): Seq[InetAddress] = | |
129 | map(name).get(flags); | |
130 | } | |
131 | ||
ad64fbfa MW |
132 | /*----- The peer configuration --------------------------------------------*/ |
133 | ||
8eabb4ff MW |
134 | def fmtpath(path: Seq[String]) = |
135 | path.reverse map { i => s"`$i'" } mkString " -> "; | |
136 | ||
137 | class ConfigSyntaxError(val file: File, val lno: Int, val msg: String) | |
138 | extends Exception { | |
139 | override def getMessage(): String = s"$file:$lno: $msg"; | |
140 | } | |
ad64fbfa | 141 | |
8eabb4ff MW |
142 | class MissingConfigSection(val sect: String) extends Exception { |
143 | override def getMessage(): String = | |
144 | s"missing configuration section `$sect'"; | |
145 | } | |
ad64fbfa | 146 | |
8eabb4ff MW |
147 | class MissingConfigItem(val sect: String, val key: String, |
148 | val path: Seq[(String)]) extends Exception { | |
149 | override def getMessage(): String = { | |
150 | val msg = s"missing configuration item `$key' in section `$sect'"; | |
151 | if (path == Nil) msg | |
152 | else msg + s" (wanted while expanding ${fmtpath(path)})" | |
153 | } | |
154 | } | |
ad64fbfa | 155 | |
8eabb4ff MW |
156 | class AmbiguousConfig(val key: String, |
157 | val v0: String, val p0: Seq[String], | |
158 | val v1: String, val p1: Seq[String]) | |
159 | extends Exception { | |
160 | override def getMessage(): String = | |
161 | s"ambiguous answer resolving key `$key': " + | |
162 | s"path ${fmtpath(p0)} yields `$v0', but ${fmtpath(p1)} yields `$v1'"; | |
163 | } | |
164 | ||
165 | class ConfigCycle(val key: String, path: Seq[String]) extends Exception { | |
166 | override def getMessage(): String = | |
167 | s"found a cycle ${fmtpath(path)} looking up key `$key'"; | |
168 | } | |
ad64fbfa | 169 | |
8eabb4ff MW |
170 | class NoHostAddresses(val sect: String, val key: String, val host: String) |
171 | extends Exception { | |
172 | override def getMessage(): String = | |
173 | s"no addresses found for `$host' (key `$key' in section `$sect')"; | |
174 | } | |
175 | ||
ad64fbfa MW |
176 | private sealed abstract class ConfigCacheEntry; |
177 | private case object StillLooking extends ConfigCacheEntry; | |
178 | private case object NotFound extends ConfigCacheEntry; | |
179 | private case class Found(value: String, path: Seq[String]) | |
180 | extends ConfigCacheEntry; | |
8eabb4ff MW |
181 | |
182 | class Config { conf => | |
ad64fbfa MW |
183 | |
184 | class Section private(val name: String) { | |
185 | private val itemmap = HashMap[String, String](); | |
186 | private[this] val cache = HashMap[String, ConfigCacheEntry](); | |
187 | ||
8eabb4ff | 188 | override def toString: String = s"${getClass.getName}($name)"; |
ad64fbfa | 189 | |
8eabb4ff MW |
190 | def parents: Seq[Section] = |
191 | (itemmap.get("@inherit") | |
192 | map { pp => (RX_PARENT.findAllIn(pp) map { conf.section _ }).toList } | |
193 | getOrElse Nil); | |
194 | ||
ad64fbfa | 195 | private def get_internal(key: String, path: Seq[String] = Nil): |
8eabb4ff MW |
196 | Option[(String, Seq[String])] = { |
197 | val incpath = name +: path; | |
198 | ||
199 | for (r <- cache.get(key)) r match { | |
200 | case StillLooking => throw new ConfigCycle(key, incpath) | |
201 | case NotFound => return None | |
202 | case Found(v, p) => return Some((v, p ++ path)); | |
203 | } | |
204 | ||
205 | for (v <- itemmap.get(key)) { | |
206 | cache(key) = Found(v, Seq(name)); | |
207 | return Some((v, incpath)); | |
208 | } | |
209 | ||
210 | cache(key) = StillLooking; | |
211 | ||
212 | ((None: Option[(String, Seq[String])]) /: parents) { (st, parent) => | |
213 | parent.get_internal(key, incpath) match { | |
214 | case None => st; | |
215 | case newst@Some((v, p)) => st match { | |
216 | case None => newst | |
217 | case Some((vv, _)) if v == vv => st | |
218 | case Some((vv, pp)) => | |
219 | throw new AmbiguousConfig(key, v, p, vv, pp) | |
220 | } | |
221 | } | |
222 | } match { | |
223 | case None => cache(key) = NotFound; None | |
224 | case Some((v, p)) => | |
225 | cache(key) = Found(v, p dropRight path.length); | |
226 | Some((v, p)) | |
227 | } | |
228 | } | |
229 | ||
230 | def get(key: String, resolve: Boolean = true, | |
231 | path: Seq[String] = Nil): String = { | |
232 | val v0 = key match { | |
233 | case "name" => itemmap.getOrElse("name", name) | |
234 | case _ => get_internal(key). | |
235 | getOrElse(throw new MissingConfigItem(name, key, path))._1 | |
236 | } | |
237 | expand(key, v0, resolve, path) | |
238 | } | |
239 | ||
ad64fbfa MW |
240 | private def expand(key: String, value: String, resolve: Boolean, |
241 | path: Seq[String]): String = { | |
8eabb4ff MW |
242 | val v1 = RX_REF.replaceAllIn(value, { m => |
243 | Regex.quoteReplacement(get(m.group(1), resolve, path)) | |
244 | }); | |
245 | val v2 = if (!resolve) v1 | |
246 | else RX_RESOLVE.replaceAllIn(v1, { m => | |
247 | resolver.resolve(m.group(2), m.group(1)) match { | |
248 | case Nil => | |
249 | throw new NoHostAddresses(name, key, m.group(2)); | |
250 | case addrs => | |
251 | Regex.quoteReplacement((addrs map { _.getHostAddress }). | |
252 | mkString(" ")); | |
253 | } | |
254 | }) | |
255 | v2 | |
256 | } | |
257 | ||
258 | def items: Seq[String] = { | |
259 | val b = Seq.newBuilder[String]; | |
260 | val seen = HashSet[String](); | |
261 | val visiting = HashSet[String](name); | |
262 | var stack = List(this); | |
263 | ||
264 | while (stack != Nil) { | |
265 | val sect = stack.head; stack = stack.tail; | |
266 | for (k <- sect.itemmap.keys) | |
267 | if (!(seen contains k)) { b += k; seen += k; } | |
268 | for (p <- sect.parents) | |
269 | if (!(visiting contains p.name)) | |
270 | { stack ::= p; visiting += p.name; } | |
271 | } | |
272 | b.result | |
273 | } | |
274 | } | |
ad64fbfa MW |
275 | |
276 | private[this] val sectmap = new HashMap[String, Section]; | |
8eabb4ff MW |
277 | def sections: Iterator[Section] = sectmap.values.iterator; |
278 | def section(name: String): Section = | |
279 | sectmap.getOrElse(name, throw new MissingConfigSection(name)); | |
280 | ||
ad64fbfa | 281 | private[this] val resolver = new BulkResolver; |
8eabb4ff | 282 | |
ad64fbfa | 283 | private[this] def parseFile(path: File): this.type = { |
8eabb4ff | 284 | println(s";; parse ${path.getPath}"); |
c8292b34 | 285 | withCleaner { clean => |
8eabb4ff MW |
286 | val in = new FileReader(path); clean { in.close(); } |
287 | ||
288 | val lno = 1; | |
289 | val b = new StringBuilder; | |
290 | var key: String = null; | |
291 | var sect: Section = null; | |
292 | def flush() { | |
293 | if (key != null) { | |
294 | sect.itemmap(key) = b.result; | |
295 | println(s";; in `${sect.name}', set `$key' to `${b.result}'"); | |
296 | b.clear(); | |
297 | key = null; | |
298 | } | |
299 | } | |
300 | for (line <- lines(in)) line match { | |
301 | case RX_COMMENT() => | |
302 | (); | |
303 | case RX_GRPHDR(grp) => | |
304 | flush(); | |
305 | sect = sectmap.getOrElseUpdate(grp, new Section(grp)); | |
306 | case RX_CONT(v) => | |
307 | if (key == null) { | |
308 | throw new ConfigSyntaxError( | |
309 | path, lno, "no config value to continue"); | |
310 | } | |
311 | b += '\n'; b ++= v; | |
312 | case RX_ASSGN(k, v) => | |
313 | if (sect == null) { | |
314 | throw new ConfigSyntaxError( | |
315 | path, lno, "no active section to update"); | |
316 | } | |
317 | flush(); | |
318 | key = k; b ++= v; | |
319 | case _ => | |
320 | throw new ConfigSyntaxError(path, lno, "incomprehensible line"); | |
321 | } | |
322 | flush(); | |
323 | } | |
324 | this | |
325 | } | |
ad64fbfa | 326 | |
8eabb4ff MW |
327 | def parse(path: File): this.type = { |
328 | if (!path.isDirectory) parseFile(path); | |
329 | else for { | |
330 | f <- path.listFiles sortBy { _.getName }; | |
331 | name = f.getName; | |
332 | if name.length > 0; | |
333 | tail = name(name.length - 1); | |
334 | if tail != '#' && tail != '~' | |
335 | } parseFile(f); | |
336 | this | |
337 | } | |
338 | def parse(path: String): this.type = parse(new File(path)); | |
339 | ||
340 | def analyse() { | |
341 | println(";; resolving all..."); | |
342 | for ((_, sect) <- sectmap) { | |
343 | println(s";; resolving in section `${sect.name}'..."); | |
344 | for (key <- sect.items) { | |
345 | println(s";; resolving in key `$key'..."); | |
346 | val mm = RX_RESOLVE.findAllIn(sect.get(key, false)); | |
347 | for (host <- mm) { resolver.prepare(mm.group(2)); } | |
348 | } | |
349 | } | |
350 | resolver.finish(); | |
351 | ||
352 | def dumpsect(sect: Section) { | |
353 | for (k <- sect.items.filterNot(_.startsWith("@")).sorted) | |
354 | println(s";; `$k' -> `${sect.get(k)}'") | |
355 | } | |
356 | for (sect <- sectmap.values.toSeq sortBy { _.name }) | |
357 | if (sect.name.startsWith("@")) (); | |
358 | else if (sect.name.startsWith("$")) { | |
359 | println(s";; special section `${sect.name}'"); | |
360 | dumpsect(sect); | |
361 | } else { | |
362 | println(s";; peer section `${sect.name}'"); | |
363 | dumpsect(sect); | |
364 | } | |
365 | } | |
366 | } | |
367 | ||
ad64fbfa MW |
368 | /*----- That's all, folks -------------------------------------------------*/ |
369 | ||
8eabb4ff | 370 | } |