From: Mark Wooding Date: Thu, 28 Jun 2018 11:09:08 +0000 (+0100) Subject: The work! The progress! X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/tripe-android/commitdiff_plain/ad64fbfa301eaddc1a067e78743adcd4c1d1dd8d The work! The progress! --- diff --git a/.gitignore b/.gitignore index e3dc5e6..7778fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /COPYING /auto-version debug.keystore +/local.mk diff --git a/Makefile b/Makefile index 7982820..275a88f 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ CCACHE := $(shell \ ## Where to put the object files. OUTDIR = out -CONFIGDIR = $(OUTDIR)/config +CONFIGDIR = $(OUTDIR)/config-$(hostcpu) ## The Java runtime, for some reason, hardcodes its default for ## `java.io.tmpdir', inviting security problems. If the user has defined a @@ -97,13 +97,13 @@ CFLAGS = -O2 -g -Wall LDFLAGS = -Wl,-z,defs ## Host toolchain. -FLAVOURS += host -ENV.host = -CC.host = gcc -CFLAGS.host = $(CFLAGS) -fPIC -LD.host = $(CC.host) -LDFLAGS.host = $(LDFLAGS) -CONFIG.host = +FLAVOURS += host-$(hostcpu) +ENV.host-$(hostcpu) = +CC.host-$(hostcpu) = gcc +CFLAGS.host-$(hostcpu) = $(CFLAGS) -fPIC +LD.host-$(hostcpu) = $(CC.host-$(hostcpu)) +LDFLAGS.host-$(hostcpu) = $(LDFLAGS) +CONFIG.host-$(hostcpu) = ## Host JNI machinery. $(CONFIGDIR)/jdkdir.mk: @@ -119,7 +119,7 @@ JDKPLAT := $(shell \ (darwin) echo macosx ;; \ (*) echo $(hostos) ;; \ esac) -CFLAGS.host += -I$(JDKDIR)/include -I$(JDKDIR)/include/$(JDKPLAT) +CFLAGS.host-$(hostcpu) += -I$(JDKDIR)/include -I$(JDKDIR)/include/$(JDKPLAT) ## Android SDK location. ANDROID_SDKDIR = /usr/local/android/sdk @@ -386,7 +386,7 @@ $(foreach a,$(APKLIBS), $(eval $(call obj-rule,$a))) CLEANFILES += $(OUTDIR)/obj.*/*.o $(OUTDIR)/obj.*/*.d ## Machinery for linking. -JNIDIR.host = $(OUTDIR) +JNIDIR.host-$(hostcpu) = $(OUTDIR)/lib.host-$(hostcpu) JNIDIR.ndk = $(OUTDIR)/pkg/lib/$1 define apklib-rule @@ -404,7 +404,8 @@ $(foreach f,$(FLAVOURS), \ $(foreach a,$(APKLIBS), \ $(eval $(call apklib-rule,$f,$a)))) -CLEANFILES += $(OUTDIR)/pkg/lib/*/lib*.so $(OUTDIR)/lib*.so +CLEANFILES += $(OUTDIR)/pkg/lib/*/lib*.so +CLEANFILES += $(OUTDIR)/lib.host-$(hostcpu)/lib*.so ###-------------------------------------------------------------------------- ### Android string resource generation. @@ -473,7 +474,8 @@ CLASSES += progress:sys,util CLASSES += keys:progress,tar,sys,util CLASSES += terminal:progress,sys,util CLASSES += R -CLASSES += toy-activity:R +CLASSES += app:R +CLASSES += toy-activity:app,R ## Building class files. $(STAMPDIR)/%.class-stamp: %.java @@ -581,8 +583,9 @@ all:: debug clean::; rm -f $(CLEANFILES) realclean::; rm -f $(REALCLEANFILES) -repl: $(CLASSSTAMPS) $(foreach a,$(APKLIBS),$(OUTDIR)/$a) - $(SCALA) -cp $(CLASSDIR) -Djava.lib.path=$(OUTDIR) -Yno-load-impl-class +repl: $(CLASSSTAMPS) $(foreach a,$(APKLIBS),$(JNIDIR.host-$(hostcpu))/$a) + $(SCALA) -cp $(CLASSDIR) -Yno-load-impl-class \ + -Djava.lib.path=$(JNIDIR.host-$(hostcpu)) \ t:; : $(show) .PHONY: t diff --git a/app.scala b/app.scala new file mode 100644 index 0000000..f82846c --- /dev/null +++ b/app.scala @@ -0,0 +1,141 @@ +/* -*-scala-*- + * + * Setting up the Android environment + * + * (c) 2018 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the Trivial IP Encryption (TrIPE) Android app. + * + * TrIPE is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * TrIPE 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 General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License + * along with TrIPE. If not, see . + */ + +package uk.org.distorted.tripe; package object app { + +/*----- Imports -----------------------------------------------------------*/ + +import java.io.{File, IOException}; + +import scala.collection.mutable.HashMap; + +import android.content.Context; import Context.MODE_WORLD_READABLE; +import android.os.Build; import Build.{CPU_ABI, CPU_ABI2}; +import android.util.Log; + +import sys.FileImplicits._; + +/*----- Regular expressions for parsing the `.installed file --------------*/ + +private final val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r; +private final val RX_KEYVAL = """(?x) ^ \s* + ([-\w]+) + (?:\s+(?!=)|\s*=\s*) + (|\S|\S.*\S) + \s* $""".r; + +/*----- Main code ---------------------------------------------------------*/ + +private final val TAG = "TrIPE"; + +var root: File = null; + +private def install(ctx: Context, inst: HashMap[String, String]) { + + /* First, figure out which ABIs are wanted on this device. Unfortunately, + * the good way of doing this isn't available in our minimum API level, so + * we must use reflection. + */ + val abis = try { + classOf[Build].getField("SUPPORTED_ABIS").get(null). + asInstanceOf[Array[String]] + } catch { case _: NoSuchFieldException => + Array(CPU_ABI, CPU_ABI2) flatMap { + case null | "" => None + case s => Some(s) + } + } + Log.d(TAG, s"abis = ${abis.mkString(", ")}"); + + /* Clear out whatever might be there already. */ + val bindir = root/"bin"; + bindir.rmTree(); + bindir.mkdir_!(); + + /* Now extract each of our binaries using the best available ABI. */ + val assets = ctx.getAssets; + for (abi <- abis) { + val binsrc = s"bin/$abi"; + for (base <- assets.list(binsrc)) { + val outfile = bindir/base; + if (!outfile.exists_!) { + Log.d(TAG, s"install: extract `$base' using abi `$abi'"); + outfile.withOutput { out => + closing(assets.open(s"$binsrc/$base")) { in => + for ((buf, n) <- blocks(in)) out.write(buf, 0, n); + } + } + } + outfile.chmod_!(0x1ed); + } + } + + /* Write out a new install file. */ + val infofile = root/".installed"; + val newinfofile = root/".installed.new"; + newinfofile.withWriter { out => + out.write(s"""### -*-conf-*- + +uuid = ${ctx.getString(R.string.auto_build_uuid)} +"""); + } + newinfofile.rename_!(infofile); +} + +def setup(ctx: Context) { + + /* Make our root directory and remember where it is. */ + root = ctx.getFilesDir; + if (!root.isdir_!) { + throw new IOException("system failed to create `files' " + + "(but didn't tell us)"); + } + + /* Find out which build, if any, corresponds to what's there already. */ + val inst = HashMap[String, String](); + try { root/".installed" withReader { in => + var lno = 1; + for (line <- lines(in)) { + line match { + case RX_COMMENT() => ok; + case RX_KEYVAL(k, v) => inst(k) = v; + case _ => Log.w(TAG, s".installed:$lno: ignored unparseable line"); + } + lno += 1; + } + } } catch { + case e: IOException => + Log.w(TAG, s".installed: I/O error: ${e.getMessage}"); + } + + /* If this doesn't match, then we have some work to do. */ + if (inst.getOrElse("uuid", "") != + ctx.getString(R.string.auto_build_uuid)) + install(ctx, inst); +} + +/*----- That's all, folks -------------------------------------------------*/ + +} diff --git a/jni.c b/jni.c index 9ca3651..3841f35 100644 --- a/jni.c +++ b/jni.c @@ -912,6 +912,21 @@ end: put_cstring(jni, path, pathstr); } +JNIEXPORT void JNIFUNC(chmod)(JNIEnv *jni, jobject cls, + jobject path, jint mode) +{ + const char *pathstr = 0; + + pathstr = get_cstring(jni, path); if (!pathstr) goto end; + if (chmod(pathstr, mode)) { + except_syserror(jni, SYSERR, errno, + "failed st permissions on `%s'", pathstr); + goto end; + } +end: + put_cstring(jni, path, pathstr); +} + JNIEXPORT void JNIFUNC(mkfile)(JNIEnv *jni, jobject cls, jobject path, jint mode) { diff --git a/keys.scala b/keys.scala index bdbede9..9108b38 100644 --- a/keys.scala +++ b/keys.scala @@ -123,7 +123,7 @@ private val DEFAULTS: Seq[(String, Config => String)] = private def parseConfig(file: File): HashMap[String, String] = { /* Build the new configuration in a temporary place. */ - var m = HashMap[String, String](); + val m = HashMap[String, String](); /* Read the config file into our map. */ file.withReader { in => diff --git a/peers.scala b/peers.scala index 76326be..c487a9e 100644 --- a/peers.scala +++ b/peers.scala @@ -1,5 +1,32 @@ +/* -*-scala-*- + * + * The database of known peers + * + * (c) 2018 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the Trivial IP Encryption (TrIPE) Android app. + * + * TrIPE is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * TrIPE 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 General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License + * along with TrIPE. If not, see . + */ + package uk.org.distorted.tripe; package object peers { +/*----- Imports -----------------------------------------------------------*/ + import java.io.{BufferedReader, File, FileReader, Reader}; import java.net.{InetAddress, Inet4Address, Inet6Address, UnknownHostException}; @@ -9,26 +36,31 @@ import scala.concurrent.Channel; import scala.util.control.Breaks; import scala.util.matching.Regex; -val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r; -val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r; -val RX_ASSGN = """(?x) ^ +/*----- Handy regular expressions -----------------------------------------*/ + +private final val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r; +private final val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r; +private final val RX_ASSGN = """(?x) ^ ([^\s:=] (?: [^:=]* [^\s:=])?) \s* [:=] \s* (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*) (?: \s+ (?: [;\#].*)? )? $""".r; -val RX_CONT = """(?x) ^ \s+ +private final val RX_CONT = """(?x) ^ \s+ (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*) (?: \s+ (?: [;\#].*)? )? $""".r; -val RX_REF = """(?x) \$ \( ([^)]+) \)""".r; -val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r; -val RX_PARENT = """(?x) [^\s,]+""".r +private final val RX_REF = """(?x) \$ \( ([^)]+) \)""".r; +private final val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r; +private final val RX_PARENT = """(?x) [^\s,]+""".r + +/*----- Name resolution ---------------------------------------------------*/ -object BulkResolver { - val BREAK = new Breaks; +private object BulkResolver { + private val BREAK = new Breaks; } -class BulkResolver(val nthreads: Int = 8) { - import BulkResolver.BREAK._; +private class BulkResolver(val nthreads: Int = 8) { + import BulkResolver.BREAK.{breakable, break}; + class Host(val name: String) { var a4, a6: Seq[InetAddress] = Seq.empty; @@ -57,6 +89,7 @@ class BulkResolver(val nthreads: Int = 8) { } } } + val ch = new Channel[Host]; val map = HashMap[String, Host](); var preparing = true; @@ -96,6 +129,8 @@ println(s";; prepare host `$name'"); map(name).get(flags); } +/*----- The peer configuration --------------------------------------------*/ + def fmtpath(path: Seq[String]) = path.reverse map { i => s"`$i'" } mkString " -> "; @@ -103,10 +138,12 @@ class ConfigSyntaxError(val file: File, val lno: Int, val msg: String) extends Exception { override def getMessage(): String = s"$file:$lno: $msg"; } + class MissingConfigSection(val sect: String) extends Exception { override def getMessage(): String = s"missing configuration section `$sect'"; } + class MissingConfigItem(val sect: String, val key: String, val path: Seq[(String)]) extends Exception { override def getMessage(): String = { @@ -115,6 +152,7 @@ class MissingConfigItem(val sect: String, val key: String, else msg + s" (wanted while expanding ${fmtpath(path)})" } } + class AmbiguousConfig(val key: String, val v0: String, val p0: Seq[String], val v1: String, val p1: Seq[String]) @@ -128,32 +166,33 @@ class ConfigCycle(val key: String, path: Seq[String]) extends Exception { override def getMessage(): String = s"found a cycle ${fmtpath(path)} looking up key `$key'"; } + class NoHostAddresses(val sect: String, val key: String, val host: String) extends Exception { override def getMessage(): String = s"no addresses found for `$host' (key `$key' in section `$sect')"; } -object Config { - sealed abstract class ConfigCacheEntry; - case object StillLooking extends ConfigCacheEntry; - case object NotFound extends ConfigCacheEntry; - case class Found(value: String, path: Seq[String]) - extends ConfigCacheEntry; -} +private sealed abstract class ConfigCacheEntry; +private case object StillLooking extends ConfigCacheEntry; +private case object NotFound extends ConfigCacheEntry; +private case class Found(value: String, path: Seq[String]) + extends ConfigCacheEntry; class Config { conf => - import Config._; - class Section(val name: String) { - val itemmap = HashMap[String, String](); - val cache = HashMap[String, ConfigCacheEntry](); + + class Section private(val name: String) { + private val itemmap = HashMap[String, String](); + private[this] val cache = HashMap[String, ConfigCacheEntry](); + override def toString: String = s"${getClass.getName}($name)"; + def parents: Seq[Section] = (itemmap.get("@inherit") map { pp => (RX_PARENT.findAllIn(pp) map { conf.section _ }).toList } getOrElse Nil); - def get_internal(key: String, path: Seq[String] = Nil): + private def get_internal(key: String, path: Seq[String] = Nil): Option[(String, Seq[String])] = { val incpath = name +: path; @@ -198,8 +237,8 @@ class Config { conf => expand(key, v0, resolve, path) } - def expand(key: String, value: String, resolve: Boolean, - path: Seq[String]): String = { + private def expand(key: String, value: String, resolve: Boolean, + path: Seq[String]): String = { val v1 = RX_REF.replaceAllIn(value, { m => Regex.quoteReplacement(get(m.group(1), resolve, path)) }); @@ -233,14 +272,15 @@ class Config { conf => b.result } } - val sectmap = new HashMap[String, Section]; + + private[this] val sectmap = new HashMap[String, Section]; def sections: Iterator[Section] = sectmap.values.iterator; def section(name: String): Section = sectmap.getOrElse(name, throw new MissingConfigSection(name)); - val resolver = new BulkResolver; + private[this] val resolver = new BulkResolver; - def parseFile(path: File): this.type = { + private[this] def parseFile(path: File): this.type = { println(s";; parse ${path.getPath}"); withCleaner { clean => val in = new FileReader(path); clean { in.close(); } @@ -283,6 +323,7 @@ println(s";; in `${sect.name}', set `$key' to `${b.result}'"); } this } + def parse(path: File): this.type = { if (!path.isDirectory) parseFile(path); else for { @@ -324,4 +365,6 @@ println(s";; resolving in key `$key'..."); } } +/*----- That's all, folks -------------------------------------------------*/ + } diff --git a/sock.scala b/sock.scala deleted file mode 100644 index 1b52bf1..0000000 --- a/sock.scala +++ /dev/null @@ -1,37 +0,0 @@ -package uk.org.distorted.tripe; - -import java.io.{Closeable, File, InputStream, OutputStream}; -import jni.Constants._; - -class Connection(path: File) extends Closeable { - def this(path: String) { this(new File(path)); } - val conn = jni.connect(path.getPath); - override def close() { jni.close(conn, CF_CLOSEMASK); } - lazy val input = new ConnectionInputStream(this); - lazy val output = new ConnectionOutputStream(this); - override protected def finalize() { super.finalize(); close(); } -} - -class ConnectionInputStream(val conn: Connection) extends InputStream { - override def read(): Int = { - val buf = new Array[Byte](1); - val n = read(buf, 0, 1); - if (n < 0) -1 else buf(0)&0xff; - } - override def read(buf: Array[Byte]): Int = - read(buf, 0, buf.length); - override def read(buf: Array[Byte], start: Int, len: Int) = - jni.recv(conn.conn, buf, start, len); - override def close() { jni.close(conn.conn, CF_CLOSERD); } -} - -class ConnectionOutputStream(val conn: Connection) extends OutputStream { - override def write(b: Int) = { - val buf = Array[Byte](b.toByte); - write(buf, 0, 1); - } - override def write(buf: Array[Byte]) { write(buf, 0, buf.length); } - override def write(buf: Array[Byte], start: Int, len: Int) = - jni.send(conn.conn, buf, start, len); - override def close() { jni.close(conn.conn, CF_CLOSEWR); } -} diff --git a/sys.scala b/sys.scala index c449414..43487b5 100644 --- a/sys.scala +++ b/sys.scala @@ -32,6 +32,7 @@ import scala.collection.mutable.{HashMap, HashSet}; import java.io.{BufferedReader, BufferedWriter, Closeable, File, FileDescriptor, FileInputStream, FileOutputStream, + FileReader, FileWriter, InputStream, InputStreamReader, OutputStream, OutputStreamWriter}; import java.nio.{ByteBuffer, CharBuffer}; @@ -409,6 +410,8 @@ def unlink(path: String) { unlink(path.toCString); } def rmdir(path: String) { rmdir(path.toCString); } @native protected def mkdir(path: CString, mode: Int); def mkdir(path: String, mode: Int) { mkdir(path.toCString, mode); } +@native protected def chmod(path: CString, mode: Int); +def chmod(path: String, mode: Int) { chmod(path.toCString, mode); } @native protected def mkfile(path: CString, mode: Int); def mkfile(path: String, mode: Int) { mkfile(path.toCString, mode); } @native protected def rename(from: CString, to: CString); @@ -608,6 +611,7 @@ object FileImplicits { def rmdir_!() { rmdir(file.getPath); } def mkdir_!(mode: Int) { mkdir(file.getPath, mode); } def mkdir_!() { mkdir_!(0x1ff); } + def chmod_!(mode: Int) { chmod(file.getPath, mode); } def mkfile_!(mode: Int) { mkfile(file.getPath, mode); } def mkfile_!() { mkfile_!(0x1b6); } def rename_!(to: File) { rename(file.getPath, to.getPath); } @@ -681,10 +685,8 @@ object FileImplicits { /* Opening files. Again, I'm surprised this isn't here already. */ def open(): FileInputStream = new FileInputStream(file); def openForOutput(): FileOutputStream = new FileOutputStream(file); - def reader(): BufferedReader = - new BufferedReader(new InputStreamReader(open())); - def writer(): BufferedWriter = - new BufferedWriter(new OutputStreamWriter(openForOutput())); + def reader(): BufferedReader = new BufferedReader(new FileReader(file)); + def writer(): BufferedWriter = new BufferedWriter(new FileWriter(file)); def withInput[T](body: FileInputStream => T): T = { val in = open(); try { body(in) } @@ -694,13 +696,15 @@ object FileImplicits { val out = openForOutput(); try { body(out) } finally { out.close(); } } - def withReader[T](body: BufferedReader => T): T = withInput { in => - body(new BufferedReader(new InputStreamReader(in))) - }; - def withWriter[T](body: BufferedWriter => T): T = withOutput { out => - val w = new BufferedWriter(new OutputStreamWriter(out)); - /* Do this the hard way, so that we flush the `BufferedWriter'. */ - try { body(w) } finally { w.close(); } + def withReader[T](body: BufferedReader => T): T = { + val r = reader(); + try { body(r) } + finally { r.close(); } + } + def withWriter[T](body: BufferedWriter => T): T = { + val w = writer(); + try { body(w) } + finally { w.close(); } } } } diff --git a/toy-activity.scala b/toy-activity.scala index dce975c..c605ca5 100644 --- a/toy-activity.scala +++ b/toy-activity.scala @@ -12,61 +12,6 @@ import android.view.View; import scala.util.control.Breaks; -object Setup { - private final val TAG = "Setup"; - private val BREAK = new Breaks; - import BREAK.{breakable, break}; - - def setup(ctx: Context) { - val bindir = ctx.getDir("bin", MODE_WORLD_READABLE); - val assets = ctx.getAssets; - - val abis = - try { classOf[Build].getField("SUPPORTED_ABIS").get(null).asInstanceOf[Array[String]] } - catch { - case _: NoSuchFieldException => Array(CPU_ABI, CPU_ABI2) flatMap { - case null | "" => None - case s => Some(s) - } - }; - - Log.d(TAG, s"abis = ${abis.mkString(", ")}"); - Log.d(TAG, s"assets: ${assets.list("bin").mkString(", ")}"); - - for (abi <- abis) { - val binsrc = s"bin/$abi"; - for (base <- assets.list(binsrc)) { - val prog = new File(bindir, base); - if (!prog.exists) try { - Log.d(TAG, s"creating $prog..."); - val in = assets.open(s"$binsrc/$base"); - Log.d(TAG, "opened source..."); - val out = new FileOutputStream(prog); - Log.d(TAG, "opened target..."); - val buf = new Array[Byte](4096); - breakable { - while (true) { - val n = in.read(buf); - Log.d(TAG, s"read $n bytes..."); - if (n <= 0) break; - out.write(buf, 0, n); - } - } - in.close(); - out.close(); - Log.d(TAG, "set permissions..."); - if (!prog.setReadable(true, false) || - !prog.setExecutable(true, false)) - throw new IOException("failed to set program permissions"); - } catch { - case exc: IOException => - Log.wtf(TAG, "fuck, failed to create prog", exc); - } - } - } - Log.d(TAG, "all OK"); - } -} object ToyActivity { private final val TAG = "ToyActivity"; @@ -79,7 +24,7 @@ class ToyActivity extends Activity { override protected def onCreate(joy: Bundle) { super.onCreate(joy); - Setup.setup(this); + app.setup(this); setContentView(R.layout.toy); Log.d(TAG, s"created ${this}"); }