3 * The database of known peers
5 * (c) 2018 Straylight/Edgeware
8 /*----- Licensing notice --------------------------------------------------*
10 * This file is part of the Trivial IP Encryption (TrIPE) Android app.
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.
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
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/>.
26 package uk.org.distorted.tripe; package object peers {
28 /*----- Imports -----------------------------------------------------------*/
30 import java.io.{BufferedReader, File, FileReader, Reader};
31 import java.net.{InetAddress, Inet4Address, Inet6Address,
32 UnknownHostException};
34 import scala.collection.mutable.{HashMap, HashSet};
35 import scala.concurrent.Channel;
36 import scala.util.control.Breaks;
37 import scala.util.matching.Regex;
39 /*----- Handy regular expressions -----------------------------------------*/
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) ^
44 ([^\s:=] (?: [^:=]* [^\s:=])?)
46 (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
47 (?: \s+ (?: [;\#].*)? )? $""".r;
48 private final val RX_CONT = """(?x) ^ \s+
49 (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
50 (?: \s+ (?: [;\#].*)? )? $""".r;
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
55 /*----- Name resolution ---------------------------------------------------*/
57 private object BulkResolver {
58 private val BREAK = new Breaks;
61 private class BulkResolver(val nthreads: Int = 8) {
62 import BulkResolver.BREAK.{breakable, break};
64 class Host(val name: String) {
65 var a4, a6: Seq[InetAddress] = Seq.empty;
67 def addaddr(a: InetAddress) { a match {
68 case _: Inet4Address => a4 +:= a;
69 case _: Inet6Address => a6 +:= a;
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;
82 if (!any) { wanta4 = true; wanta6 = true; }
85 (all, b.result) match {
87 case (false, aa@(Nil | Seq(_))) => aa
88 case (false, Seq(a, _*)) => Seq(a)
93 val ch = new Channel[Host];
94 val map = HashMap[String, Host]();
97 val workers = Array.tabulate(nthreads) { i =>
98 thread(s"resolver worker #$i") {
100 val host = ch.read; if (host == null) exit;
101 println(s";; ${Thread.currentThread.getName} resolving `${host.name}'");
103 for (a <- InetAddress.getAllByName(host.name)) host.addaddr(a);
104 } catch { case e: UnknownHostException => () }
106 println(s";; ${Thread.currentThread.getName} done'");
111 def prepare(name: String) {
112 println(s";; prepare host `$name'");
114 if (!(map contains name)) {
115 val host = new Host(name);
125 for (t <- workers) t.join();
128 def resolve(name: String, flags: String): Seq[InetAddress] =
129 map(name).get(flags);
132 /*----- The peer configuration --------------------------------------------*/
134 def fmtpath(path: Seq[String]) =
135 path.reverse map { i => s"`$i'" } mkString " -> ";
137 class ConfigSyntaxError(val file: File, val lno: Int, val msg: String)
139 override def getMessage(): String = s"$file:$lno: $msg";
142 class MissingConfigSection(val sect: String) extends Exception {
143 override def getMessage(): String =
144 s"missing configuration section `$sect'";
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'";
152 else msg + s" (wanted while expanding ${fmtpath(path)})"
156 class AmbiguousConfig(val key: String,
157 val v0: String, val p0: Seq[String],
158 val v1: String, val p1: Seq[String])
160 override def getMessage(): String =
161 s"ambiguous answer resolving key `$key': " +
162 s"path ${fmtpath(p0)} yields `$v0', but ${fmtpath(p1)} yields `$v1'";
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'";
170 class NoHostAddresses(val sect: String, val key: String, val host: String)
172 override def getMessage(): String =
173 s"no addresses found for `$host' (key `$key' in section `$sect')";
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;
182 class Config { conf =>
184 class Section private(val name: String) {
185 private val itemmap = HashMap[String, String]();
186 private[this] val cache = HashMap[String, ConfigCacheEntry]();
188 override def toString: String = s"${getClass.getName}($name)";
190 def parents: Seq[Section] =
191 (itemmap.get("@inherit")
192 map { pp => (RX_PARENT.findAllIn(pp) map { conf.section _ }).toList }
195 private def get_internal(key: String, path: Seq[String] = Nil):
196 Option[(String, Seq[String])] = {
197 val incpath = name +: path;
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));
205 for (v <- itemmap.get(key)) {
206 cache(key) = Found(v, Seq(name));
207 return Some((v, incpath));
210 cache(key) = StillLooking;
212 ((None: Option[(String, Seq[String])]) /: parents) { (st, parent) =>
213 parent.get_internal(key, incpath) match {
215 case newst@Some((v, p)) => st match {
217 case Some((vv, _)) if v == vv => st
218 case Some((vv, pp)) =>
219 throw new AmbiguousConfig(key, v, p, vv, pp)
223 case None => cache(key) = NotFound; None
225 cache(key) = Found(v, p dropRight path.length);
230 def get(key: String, resolve: Boolean = true,
231 path: Seq[String] = Nil): String = {
233 case "name" => itemmap.getOrElse("name", name)
234 case _ => get_internal(key).
235 getOrElse(throw new MissingConfigItem(name, key, path))._1
237 expand(key, v0, resolve, path)
240 private def expand(key: String, value: String, resolve: Boolean,
241 path: Seq[String]): String = {
242 val v1 = RX_REF.replaceAllIn(value, { m =>
243 Regex.quoteReplacement(get(m.group(1), resolve, path))
245 val v2 = if (!resolve) v1
246 else RX_RESOLVE.replaceAllIn(v1, { m =>
247 resolver.resolve(m.group(2), m.group(1)) match {
249 throw new NoHostAddresses(name, key, m.group(2));
251 Regex.quoteReplacement((addrs map { _.getHostAddress }).
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);
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; }
276 private[this] val sectmap = new HashMap[String, Section];
277 def sections: Iterator[Section] = sectmap.values.iterator;
278 def section(name: String): Section =
279 sectmap.getOrElse(name, throw new MissingConfigSection(name));
281 private[this] val resolver = new BulkResolver;
283 private[this] def parseFile(path: File): this.type = {
284 println(s";; parse ${path.getPath}");
285 withCleaner { clean =>
286 val in = new FileReader(path); clean { in.close(); }
289 val b = new StringBuilder;
290 var key: String = null;
291 var sect: Section = null;
294 sect.itemmap(key) = b.result;
295 println(s";; in `${sect.name}', set `$key' to `${b.result}'");
300 for (line <- lines(in)) line match {
303 case RX_GRPHDR(grp) =>
305 sect = sectmap.getOrElseUpdate(grp, new Section(grp));
308 throw new ConfigSyntaxError(
309 path, lno, "no config value to continue");
312 case RX_ASSGN(k, v) =>
314 throw new ConfigSyntaxError(
315 path, lno, "no active section to update");
320 throw new ConfigSyntaxError(path, lno, "incomprehensible line");
327 def parse(path: File): this.type = {
328 if (!path.isDirectory) parseFile(path);
330 f <- path.listFiles sortBy { _.getName };
333 tail = name(name.length - 1);
334 if tail != '#' && tail != '~'
338 def parse(path: String): this.type = parse(new File(path));
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)); }
352 def dumpsect(sect: Section) {
353 for (k <- sect.items.filterNot(_.startsWith("@")).sorted)
354 println(s";; `$k' -> `${sect.get(k)}'")
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}'");
362 println(s";; peer section `${sect.name}'");
368 /*----- That's all, folks -------------------------------------------------*/