From 4a5ad0a33dce11e75103b335a780324a5fec7e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Sat, 6 Jan 2018 16:07:19 +0100 Subject: [PATCH 1/8] implemented parser for (repository) provisioning --- .../java/org/fdroid/fdroid/Provisioner.java | 242 ++++++++++++++++++ .../java/org/fdroid/fdroid/UpdateService.java | 2 + .../org/fdroid/fdroid/ProvisionerTest.java | 105 ++++++++ .../resources/demo_credentials_user1.fdrp | Bin 0 -> 259 bytes .../resources/demo_credentials_user2.fdrp | Bin 0 -> 220 bytes 5 files changed, 349 insertions(+) create mode 100644 app/src/main/java/org/fdroid/fdroid/Provisioner.java create mode 100644 app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java create mode 100644 app/src/test/resources/demo_credentials_user1.fdrp create mode 100644 app/src/test/resources/demo_credentials_user2.fdrp diff --git a/app/src/main/java/org/fdroid/fdroid/Provisioner.java b/app/src/main/java/org/fdroid/fdroid/Provisioner.java new file mode 100644 index 000000000..03ce936cb --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/Provisioner.java @@ -0,0 +1,242 @@ +package org.fdroid.fdroid; + +import android.os.Environment; +import android.util.Base64; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * @author Michael Pöhn (michael.poehn@fsfe.org) + */ +public class Provisioner { + + public static final String TAG = "Provisioner"; + + private static final String DEFAULT_PROVISION_DIR = Environment.getExternalStorageDirectory().getPath(); + + protected Provisioner() { + } + + /** + * search for provision files and process them + */ + public void scanAndProcess() { + + List files = findProvisionFiles(); + List plaintexts = extractProvisionsPlaintext(files); + files.clear(); + + List provisions = parseProvisions(plaintexts); + plaintexts.clear(); + + // TODO: do something useful with provisions, like prompting users + for (Provision provision : provisions) { + if (provision.getRepositoryProvision() != null) { + RepositoryProvision repo = provision.getRepositoryProvision(); + Utils.debugLog(TAG, "repository:" + + " " + repo.getName() + + " " + repo.getUrl() + + " " + repo.getUsername()); + } + } + } + + /** + * @return List of + */ + public List findProvisionFiles() { + return findProvisionFilesInDir(new File(DEFAULT_PROVISION_DIR)); + } + + protected List findProvisionFilesInDir(File file) { + File[] files = file.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if (name != null && name.endsWith(".fdrp")) { + return true; + } + return false; + } + }); + return files != null ? Arrays.asList(files) : null; + } + + String rot13(String text) { + StringBuilder sb = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) { + sb.append(c + 13); + } else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) { + sb.append(c - 13); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + protected String deobfuscate(String obfuscated) { + try { + return new String(Base64.decode(rot13(obfuscated), Base64.DEFAULT), "UTF-8"); + } catch (UnsupportedEncodingException e) { + // encoding is defined to be utf8, continue gracefully if this magically fails. + return ""; + } + } + + protected List extractProvisionsPlaintext(List files) { + List result = new ArrayList<>(); + for (File file : files) { + ProvisionPlaintext plain = new ProvisionPlaintext(); + plain.setProvisionPath(file.getAbsolutePath()); + ZipInputStream in = null; + try { + in = new ZipInputStream(new FileInputStream(file)); + ZipEntry zipEntry = null; + while ((zipEntry = in.getNextEntry()) != null) { + String name = zipEntry.getName(); + if ("repo_provision.json".equals(name)) { + if (plain.getRepositoryProvision() != null) { + throw new IOException("provision malformed: contains more than one repo provision file."); + } + plain.setRepositoryProvision(IOUtils.toString(in, Charset.forName("UTF-8"))); + } else if ("repo_provision.ojson".equals(name)) { + if (plain.getRepositoryProvision() != null) { + throw new IOException("provision malformed: contains more than one repo provision file."); + } + plain.setRepositoryProvision(deobfuscate(IOUtils.toString(in, Charset.forName("UTF-8")))); + } + } + } catch (FileNotFoundException e) { + Utils.debugLog(TAG, String.format("finding provision '%s' failed", file.getPath()), e); + continue; + } catch (IOException e) { + Utils.debugLog(TAG, String.format("reading provision '%s' failed", file.getPath()), e); + continue; + } finally { + IOUtils.closeQuietly(in); + } + + result.add(plain); + } + return result; + } + + public List parseProvisions(List provisionPlaintexts) { + + List provisions = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + + for (ProvisionPlaintext provisionPlaintext : provisionPlaintexts) { + Provision provision = new Provision(); + provision.setProvisonPath(provisionPlaintext.getProvisionPath()); + try { + provision.setRepositoryProvision( + mapper.readValue(provisionPlaintext.getRepositoryProvision(), RepositoryProvision.class)); + } catch (IOException e) { + Utils.debugLog(TAG, "could not parse repository provision", e); + } + provisions.add(provision); + } + + return provisions; + } + + public static class ProvisionPlaintext { + private String provisionPath; + private String repositoryProvision; + + public String getProvisionPath() { + return provisionPath; + } + + public void setProvisionPath(String provisionPath) { + this.provisionPath = provisionPath; + } + + public String getRepositoryProvision() { + return repositoryProvision; + } + + public void setRepositoryProvision(String repositoryProvision) { + this.repositoryProvision = repositoryProvision; + } + } + + public static class Provision { + private String provisonPath; + private RepositoryProvision repositoryProvision; + + public String getProvisonPath() { + return provisonPath; + } + + public void setProvisonPath(String provisonPath) { + this.provisonPath = provisonPath; + } + + public RepositoryProvision getRepositoryProvision() { + return repositoryProvision; + } + + public void setRepositoryProvision(RepositoryProvision repositoryProvision) { + this.repositoryProvision = repositoryProvision; + } + } + + public static class RepositoryProvision { + + private String name; + private String url; + private String username; + private String password; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index e6a02303f..f037132a7 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -467,6 +467,8 @@ public class UpdateService extends IntentService { long time = System.currentTimeMillis() - startTime; Log.i(TAG, "Updating repo(s) complete, took " + time / 1000 + " seconds to complete."); + + // TODO provi: this looks like a good spot for adding automated repository provisioning } private void notifyContentProviders() { diff --git a/app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java b/app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java new file mode 100644 index 000000000..368ab6112 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java @@ -0,0 +1,105 @@ +package org.fdroid.fdroid; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * @author Michael Poehn (michael.poehn@fsfe.org) + */ +@Config(constants = BuildConfig.class, sdk = 24) +@RunWith(RobolectricTestRunner.class) +@SuppressWarnings("LineLength") +public class ProvisionerTest { + + @Test + public void provisionLookup() throws IOException { + // wired hack for getting resource dir path ... + String resourceDir = getResourceFile( + "demo_credentials_user1.fdrp").getParent(); + + Provisioner p = new Provisioner(); + List files = p.findProvisionFilesInDir(new File(resourceDir)); + + List expectedFilenames = Arrays.asList( + "demo_credentials_user1.fdrp", + "demo_credentials_user2.fdrp"); + + Assert.assertEquals(2, files.size()); + for (File f : files) { + Assert.assertTrue("unexpected file name " + f.getName(), expectedFilenames.contains(f.getName())); + } + } + + @Test + public void rot13() { + Provisioner p = new Provisioner(); + String result = p.rot13("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890{}\"':="); + Assert.assertEquals("nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM1234567890{}\"':=", result); + } + + @Test + public void deobfuscate() { + Provisioner p = new Provisioner(); + String result = p.deobfuscate("rlWVMKWuL2kcqUImVwbaGz90nTyhMlOyozE1pzImVTW1qPOwnTShM2HhWljXVPNtVPq3nTIhWmbtJlWuLz91qPN1ZQNtDv5QYvWqsD=="); + Assert.assertEquals("{\"Heraclitus\":'Nothing endures but change.',\n 'when': [\"about 500 B.C.\"]}", result); + } + + @Test + public void extractProvisionsPlaintextUnobfuscated() throws IOException { + Provisioner p = new Provisioner(); + List files = Arrays.asList(getResourceFile("demo_credentials_user2.fdrp")); + List result = p.extractProvisionsPlaintext(files); + + Assert.assertEquals(result.size(), 1); + Assert.assertEquals("{\"username\": \"user2\", \"name\": \"test repo a\", \"password\": \"other secret\", \"url\": \"https://example.com/repo\"}", result.get(0).getRepositoryProvision()); + Assert.assertTrue(String.valueOf(result.get(0).getProvisionPath()).endsWith("demo_credentials_user2.fdrp")); + } + + @Test + public void extractProvisionsPlaintextObfuscated() throws IOException { + Provisioner p = new Provisioner(); + List files = Arrays.asList(getResourceFile("demo_credentials_user1.fdrp")); + List result = p.extractProvisionsPlaintext(files); + + Assert.assertEquals(result.size(), 1); + Assert.assertEquals("{\"username\": \"user1\", \"password\": \"secret1\", \"name\": \"test repo a\", \"url\": \"https://example.com/repo\"}", result.get(0).getRepositoryProvision()); + Assert.assertTrue(String.valueOf(result.get(0).getProvisionPath()).endsWith("demo_credentials_user1.fdrp")); + } + + @Test + public void parseProvisions() { + + List plaintexts = Arrays.asList(new Provisioner.ProvisionPlaintext(), new Provisioner.ProvisionPlaintext()); + plaintexts.get(0).setProvisionPath("/some/dir/abc.fdrp"); + plaintexts.get(0).setRepositoryProvision("{\"username\": \"user1\", \"password\": \"secret1\", \"name\": \"test repo a\", \"url\": \"https://example.com/repo\"}"); + plaintexts.get(1).setProvisionPath("/some/dir/def.fdrp"); + plaintexts.get(1).setRepositoryProvision("{\"username\": \"user2\", \"name\": \"test repo a\", \"password\": \"other secret\", \"url\": \"https://example.com/repo\"}"); + + Provisioner p = new Provisioner(); + List result = p.parseProvisions(plaintexts); + + Assert.assertEquals("/some/dir/abc.fdrp", result.get(0).getProvisonPath()); + Assert.assertEquals("test repo a", result.get(0).getRepositoryProvision().getName()); + Assert.assertEquals("https://example.com/repo", result.get(0).getRepositoryProvision().getUrl()); + Assert.assertEquals("user1", result.get(0).getRepositoryProvision().getUsername()); + Assert.assertEquals("secret1", result.get(0).getRepositoryProvision().getPassword()); + + Assert.assertEquals("/some/dir/def.fdrp", result.get(1).getProvisonPath()); + Assert.assertEquals("test repo a", result.get(1).getRepositoryProvision().getName()); + Assert.assertEquals("https://example.com/repo", result.get(1).getRepositoryProvision().getUrl()); + Assert.assertEquals("user2", result.get(1).getRepositoryProvision().getUsername()); + Assert.assertEquals("other secret", result.get(1).getRepositoryProvision().getPassword()); + } + + private File getResourceFile(String resourceFileName) { + return new File(getClass().getClassLoader().getResource(resourceFileName).getPath()); + } +} diff --git a/app/src/test/resources/demo_credentials_user1.fdrp b/app/src/test/resources/demo_credentials_user1.fdrp new file mode 100644 index 0000000000000000000000000000000000000000..6864a8ab38d604fef0158fc76e226d4b89782822 GIT binary patch literal 259 zcmWIWW@Zs#U|`??Vnv3t+ZfhX0$CkE3=%C$Ey#~AD9SI(EY8f&)635)&d*crI>>iO zL4dV><)kY&lBJ!O>~PwmcX4sMlz@}rCf^@bo%imY*|Xhzb}Og+>8G4B+gJRrd2Vt2 zZQmyT&QiHA>sckL`HTKL70K)5_C0Q7vkIK$;t_RqnT}uju9vTt%q*RyL3lMj&(o(yPJh0jqme A>Hq)$ literal 0 HcmV?d00001 diff --git a/app/src/test/resources/demo_credentials_user2.fdrp b/app/src/test/resources/demo_credentials_user2.fdrp new file mode 100644 index 0000000000000000000000000000000000000000..d0122cdba33c81d8fe790b42d134f294e5a8bc95 GIT binary patch literal 220 zcmWIWW@Zs#U|`??Vnqh?hmWg5fUImF76#&?)Pnr@f};Gg%;L=aJiV;q{5;b$j$91} zJT4bkx2!qZbo$=1D~)c=v Date: Mon, 15 Jan 2018 00:04:49 +0100 Subject: [PATCH 2/8] added parsing for repository signing key property to provisioning files --- .../java/org/fdroid/fdroid/Provisioner.java | 22 +++++++++++++++--- .../org/fdroid/fdroid/ProvisionerTest.java | 21 ++++++++++++----- .../resources/demo_credentials_user1.fdrp | Bin 259 -> 332 bytes .../resources/demo_credentials_user2.fdrp | Bin 220 -> 261 bytes 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/Provisioner.java b/app/src/main/java/org/fdroid/fdroid/Provisioner.java index 03ce936cb..9a5c6ee24 100644 --- a/app/src/main/java/org/fdroid/fdroid/Provisioner.java +++ b/app/src/main/java/org/fdroid/fdroid/Provisioner.java @@ -1,5 +1,8 @@ package org.fdroid.fdroid; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.os.Environment; import android.util.Base64; @@ -35,7 +38,7 @@ public class Provisioner { /** * search for provision files and process them */ - public void scanAndProcess() { + public void scanAndProcess(Context context) { List files = findProvisionFiles(); List plaintexts = extractProvisionsPlaintext(files); @@ -52,6 +55,10 @@ public class Provisioner { + " " + repo.getName() + " " + repo.getUrl() + " " + repo.getUsername()); + + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(repo.getUrl())); + context.startActivity(i); } } } @@ -81,9 +88,9 @@ public class Provisioner { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) { - sb.append(c + 13); + sb.append((char)(c + 13)); } else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) { - sb.append(c - 13); + sb.append((char)(c - 13)); } else { sb.append(c); } @@ -204,6 +211,7 @@ public class Provisioner { private String name; private String url; + private String sigfp; private String username; private String password; @@ -223,6 +231,14 @@ public class Provisioner { this.url = url; } + public String getSigfp() { + return sigfp; + } + + public void setSigfp(String sigfp) { + this.sigfp = sigfp; + } + public String getUsername() { return username; } diff --git a/app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java b/app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java index 368ab6112..14882e15f 100644 --- a/app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java +++ b/app/src/test/java/org/fdroid/fdroid/ProvisionerTest.java @@ -1,6 +1,8 @@ package org.fdroid.fdroid; +import org.fdroid.fdroid.shadows.ShadowLog; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -19,6 +21,11 @@ import java.util.List; @SuppressWarnings("LineLength") public class ProvisionerTest { + @Before + public void setUp() { + ShadowLog.stream = System.out; + } + @Test public void provisionLookup() throws IOException { // wired hack for getting resource dir path ... @@ -59,7 +66,7 @@ public class ProvisionerTest { List result = p.extractProvisionsPlaintext(files); Assert.assertEquals(result.size(), 1); - Assert.assertEquals("{\"username\": \"user2\", \"name\": \"test repo a\", \"password\": \"other secret\", \"url\": \"https://example.com/repo\"}", result.get(0).getRepositoryProvision()); + Assert.assertEquals("{\"username\": \"user2\", \"password\": \"other secret\", \"name\": \"Example Repo\", \"url\": \"https://example.com/fdroid/repo\", \"sigfp\": \"1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff\"}", result.get(0).getRepositoryProvision()); Assert.assertTrue(String.valueOf(result.get(0).getProvisionPath()).endsWith("demo_credentials_user2.fdrp")); } @@ -70,7 +77,7 @@ public class ProvisionerTest { List result = p.extractProvisionsPlaintext(files); Assert.assertEquals(result.size(), 1); - Assert.assertEquals("{\"username\": \"user1\", \"password\": \"secret1\", \"name\": \"test repo a\", \"url\": \"https://example.com/repo\"}", result.get(0).getRepositoryProvision()); + Assert.assertEquals("{\"sigfp\": \"1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff\", \"name\": \"Example Repo\", \"password\": \"secret1\", \"url\": \"https://example.com/fdroid/repo\", \"username\": \"user1\"}", result.get(0).getRepositoryProvision()); Assert.assertTrue(String.valueOf(result.get(0).getProvisionPath()).endsWith("demo_credentials_user1.fdrp")); } @@ -79,22 +86,24 @@ public class ProvisionerTest { List plaintexts = Arrays.asList(new Provisioner.ProvisionPlaintext(), new Provisioner.ProvisionPlaintext()); plaintexts.get(0).setProvisionPath("/some/dir/abc.fdrp"); - plaintexts.get(0).setRepositoryProvision("{\"username\": \"user1\", \"password\": \"secret1\", \"name\": \"test repo a\", \"url\": \"https://example.com/repo\"}"); + plaintexts.get(0).setRepositoryProvision("{\"username\": \"user1\", \"password\": \"secret1\", \"name\": \"test repo a\", \"url\": \"https://example.com/fdroid/repo\", \"sigfp\": \"1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff\"}"); plaintexts.get(1).setProvisionPath("/some/dir/def.fdrp"); - plaintexts.get(1).setRepositoryProvision("{\"username\": \"user2\", \"name\": \"test repo a\", \"password\": \"other secret\", \"url\": \"https://example.com/repo\"}"); + plaintexts.get(1).setRepositoryProvision("{\"username\": \"user2\", \"name\": \"test repo a\", \"password\": \"other secret\", \"url\": \"https://example.com/fdroid/repo\", \"sigfp\": \"1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff\"}"); Provisioner p = new Provisioner(); List result = p.parseProvisions(plaintexts); Assert.assertEquals("/some/dir/abc.fdrp", result.get(0).getProvisonPath()); Assert.assertEquals("test repo a", result.get(0).getRepositoryProvision().getName()); - Assert.assertEquals("https://example.com/repo", result.get(0).getRepositoryProvision().getUrl()); + Assert.assertEquals("https://example.com/fdroid/repo", result.get(0).getRepositoryProvision().getUrl()); + Assert.assertEquals("1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff", result.get(0).getRepositoryProvision().getSigfp()); Assert.assertEquals("user1", result.get(0).getRepositoryProvision().getUsername()); Assert.assertEquals("secret1", result.get(0).getRepositoryProvision().getPassword()); Assert.assertEquals("/some/dir/def.fdrp", result.get(1).getProvisonPath()); Assert.assertEquals("test repo a", result.get(1).getRepositoryProvision().getName()); - Assert.assertEquals("https://example.com/repo", result.get(1).getRepositoryProvision().getUrl()); + Assert.assertEquals("https://example.com/fdroid/repo", result.get(1).getRepositoryProvision().getUrl()); + Assert.assertEquals("1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff", result.get(1).getRepositoryProvision().getSigfp()); Assert.assertEquals("user2", result.get(1).getRepositoryProvision().getUsername()); Assert.assertEquals("other secret", result.get(1).getRepositoryProvision().getPassword()); } diff --git a/app/src/test/resources/demo_credentials_user1.fdrp b/app/src/test/resources/demo_credentials_user1.fdrp index 6864a8ab38d604fef0158fc76e226d4b89782822..94b27d803fa0220b0d20f4cfd208df7079bb9965 100644 GIT binary patch literal 332 zcmWIWW@Zs#U|`??Vnqh!4e}<3fUG}23=%C$Ey#~AD9SI(EY8f&)635)&d>ApJ?qzO zz)+{UB&qD?!m|?5nVPOSo9{hKt9I@F zloS+qR@>+VzvLP}{|!o(Usw*@WH=&!zN4KfG%;k4Bxl`L7H{`SU+gt+O>g}6+~cF= zMbSgO{1drWe>|ypeaEJ4Vb|Zd^}q4RHp}@pHIiB3f&<&b`{8`bj7EBUil=WY^w!Ov zJuhe5o{f?_?>hW1<68eYXTi!jC31&T-_ET2e!KbNpWpX(yuWijasPj|0B=Sn5wQQT b`x@wRB!Ddp0=!w-KuQ>a&u>uu@pqQWeOb>c zQO#fU->FDmC%5l$Bb!y=EEkWctIKr!(s#XlwPa>t?UnevOQEfeWnwAk0#`{^>~_0$ fV=0ID-k19VycwB97=*F7Z89&TCEIGC3mF&yibF^2 diff --git a/app/src/test/resources/demo_credentials_user2.fdrp b/app/src/test/resources/demo_credentials_user2.fdrp index d0122cdba33c81d8fe790b42d134f294e5a8bc95..acf364dd170807a8bc8e9328d3d1fce4762bb70f 100644 GIT binary patch delta 197 zcmcb^*vcdw;LXe;!oa}50mO<7Df3Jp*8*7wfLM6q2C;h6Q_*~f6hv4qtQFYTn^F6H zzw`?}0ljUWf<^kizCG3D9}4%W8GpXAS>S%I%H37|d0)I<=D1EfvTxc-o+Dl-45#k; zd;ad>V>T0CX)eEY-sWnRhN^cqPwn%q-WOTUXMSm`_m|99y#8KH>c&AizJeCvIS-kn hHx#h4%W*KhxDIriI3tq?0~Y5^oM*wd4(LV(1_0M!NizTd literal 220 zcmWIWW@Zs#U|`??Vnqh?hmWg5fUImF76#&?)Pnr@f};Gg%;L=aJiV;q{5;b$j$91} zJT4bkx2!qZbo$=1D~)c=v Date: Mon, 22 Jan 2018 22:25:25 +0100 Subject: [PATCH 3/8] added authentication parsing support to add-repo dialog --- .../org/fdroid/fdroid/data/NewRepoConfig.java | 24 +++++++++++ .../fdroid/views/ManageReposActivity.java | 43 +++++++++++++++---- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java index 51a642be6..5b45396a3 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java +++ b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java @@ -23,6 +23,8 @@ public class NewRepoConfig { private String uriString; private String host; private int port = -1; + private String username; + private String password; private String fingerprint; private String bssid; private String ssid; @@ -91,6 +93,18 @@ public class NewRepoConfig { boolean isFdroidScheme = TextUtils.equals("fdroidrepo", scheme) || TextUtils.equals("fdroidrepos", scheme); + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + String[] userInfoTokens = userInfo.split(":"); + if (userInfoTokens != null && userInfoTokens.length >= 2){ + username = userInfoTokens[0]; + password = userInfoTokens[1]; + for (int i = 2; i < userInfoTokens.length; i++) { + password += ":" + userInfoTokens[i]; + } + } + } + fingerprint = uri.getQueryParameter("fingerprint"); bssid = uri.getQueryParameter("bssid"); ssid = uri.getQueryParameter("ssid"); @@ -133,6 +147,14 @@ public class NewRepoConfig { return host; } + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + public String getFingerprint() { return fingerprint; } @@ -157,9 +179,11 @@ public class NewRepoConfig { public static String sanitizeRepoUri(Uri uri) { String scheme = uri.getScheme(); String host = uri.getHost(); + String userInfo = uri.getUserInfo(); return uri.toString() .replaceAll("\\?.*$", "") // remove the whole query .replaceAll("/*$", "") // remove all trailing slashes + .replace(userInfo + "@", "") // remove user authentication .replace(host, host.toLowerCase(Locale.ENGLISH)) .replace(scheme, scheme.toLowerCase(Locale.ENGLISH)) .replace("fdroidrepo", "http") // proper repo address diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 1f83b82d3..13d5d4969 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -162,6 +162,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana ClipboardCompat clipboard = ClipboardCompat.create(this); String text = clipboard.getText(); String fingerprint = null; + String username = null; + String password = null; if (!TextUtils.isEmpty(text)) { try { new URL(text); @@ -171,6 +173,19 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana if (TextUtils.isEmpty(fingerprint)) { fingerprint = uri.getQueryParameter("FINGERPRINT"); } + + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + String[] userInfoTokens = userInfo.split(":"); + if (userInfoTokens.length >= 2) { + username = userInfoTokens[0]; + password = userInfoTokens[1]; + for (int i=2; i Date: Tue, 23 Jan 2018 12:00:25 +0100 Subject: [PATCH 4/8] trigger provisions on app start --- app/src/main/java/org/fdroid/fdroid/FDroidApp.java | 3 +++ app/src/main/java/org/fdroid/fdroid/UpdateService.java | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index e08216491..bbf9d5c4e 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -410,6 +410,9 @@ public class FDroidApp extends Application { } grantUriPermission(packageName, InstallHistoryService.LOG_URI, modeFlags); } + + // find and process provisions if any. + Provisioner.scanAndProcess(getApplicationContext()); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index f037132a7..e6a02303f 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -467,8 +467,6 @@ public class UpdateService extends IntentService { long time = System.currentTimeMillis() - startTime; Log.i(TAG, "Updating repo(s) complete, took " + time / 1000 + " seconds to complete."); - - // TODO provi: this looks like a good spot for adding automated repository provisioning } private void notifyContentProviders() { From 0f64fd30c6fa4939a06e53987bfc1bcf34e42309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Wed, 24 Jan 2018 15:08:33 +0100 Subject: [PATCH 5/8] fix lint for NewRepoConfig --- app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java index 5b45396a3..c1242f345 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java +++ b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java @@ -96,7 +96,7 @@ public class NewRepoConfig { String userInfo = uri.getUserInfo(); if (userInfo != null) { String[] userInfoTokens = userInfo.split(":"); - if (userInfoTokens != null && userInfoTokens.length >= 2){ + if (userInfoTokens != null && userInfoTokens.length >= 2) { username = userInfoTokens[0]; password = userInfoTokens[1]; for (int i = 2; i < userInfoTokens.length; i++) { From bb1292586bef01b30af343e590710c413d739ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Wed, 24 Jan 2018 15:09:15 +0100 Subject: [PATCH 6/8] fix lint for ManageRepoActivity --- .../java/org/fdroid/fdroid/views/ManageReposActivity.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 13d5d4969..4674b4802 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -180,7 +180,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana if (userInfoTokens.length >= 2) { username = userInfoTokens[0]; password = userInfoTokens[1]; - for (int i=2; i Date: Wed, 24 Jan 2018 16:20:38 +0100 Subject: [PATCH 7/8] implemented provisioning routine --- .../java/org/fdroid/fdroid/Provisioner.java | 196 +++++++++++------- 1 file changed, 124 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/Provisioner.java b/app/src/main/java/org/fdroid/fdroid/Provisioner.java index 9a5c6ee24..7fd443c78 100644 --- a/app/src/main/java/org/fdroid/fdroid/Provisioner.java +++ b/app/src/main/java/org/fdroid/fdroid/Provisioner.java @@ -3,12 +3,14 @@ package org.fdroid.fdroid; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Environment; import android.util.Base64; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.views.ManageReposActivity; import java.io.File; import java.io.FileInputStream; @@ -26,11 +28,18 @@ import java.util.zip.ZipInputStream; /** * @author Michael Pöhn (michael.poehn@fsfe.org) */ +@SuppressWarnings("LineLength") public class Provisioner { public static final String TAG = "Provisioner"; - private static final String DEFAULT_PROVISION_DIR = Environment.getExternalStorageDirectory().getPath(); + /** + * This is the name of the subfolder in the file directory of this app + * where {@link Provisioner} looks for new provisions. + * + * eg. in the Emulator (API level 24): /data/user/0/org.fdroid.fdroid.debug/files/provisions + */ + private static final String NEW_PROVISIONS_DIR = "provisions"; protected Provisioner() { } @@ -38,49 +47,88 @@ public class Provisioner { /** * search for provision files and process them */ - public void scanAndProcess(Context context) { + public static void scanAndProcess(Context context) { - List files = findProvisionFiles(); - List plaintexts = extractProvisionsPlaintext(files); - files.clear(); + File provisionDir = new File(context.getExternalFilesDir(null).getAbsolutePath() + File.separator + NEW_PROVISIONS_DIR); - List provisions = parseProvisions(plaintexts); - plaintexts.clear(); + if (!provisionDir.isDirectory()) { + Utils.debugLog(TAG, "Provisions dir does not exists: '" + provisionDir.getAbsolutePath() + "' moving on ..."); + } else if (provisionDir.list().length == 0) { + Utils.debugLog(TAG, "Provisions dir is empty: '" + provisionDir.getAbsolutePath() + "' moving on ..."); + } else { - // TODO: do something useful with provisions, like prompting users - for (Provision provision : provisions) { - if (provision.getRepositoryProvision() != null) { - RepositoryProvision repo = provision.getRepositoryProvision(); - Utils.debugLog(TAG, "repository:" - + " " + repo.getName() - + " " + repo.getUrl() - + " " + repo.getUsername()); + Provisioner p = new Provisioner(); + List files = p.findProvisionFiles(context); + List plaintexts = p.extractProvisionsPlaintext(files); + List provisions = p.parseProvisions(plaintexts); - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse(repo.getUrl())); - context.startActivity(i); + if (provisions == null || provisions.size() == 0) { + Utils.debugLog(TAG, "Provision dir does not contain any provisions: '" + provisionDir.getAbsolutePath() + "' moving on ..."); + } else { + int cleanupCounter = 0; + for (Provision provision : provisions) { + if (provision.getRepositoryProvision() != null) { + RepositoryProvision repo = provision.getRepositoryProvision(); + + // TODO uniqx: check if repo is already enable + Repo storedRepo = RepoProvider.Helper.findByAddress(context, repo.getUrl()); + if (storedRepo != null) { + Utils.debugLog(TAG, "Provision contains a repo which is already added: '" + provision.getProvisonPath() + "' ignoring ..."); + } else { + // Note: only the last started activity will visible to users. + // All other prompting attempts will be lost. + Uri origUrl = Uri.parse(repo.getUrl()); + Uri.Builder data = new Uri.Builder(); + data.scheme(origUrl.getScheme()); + data.encodedAuthority(Uri.encode(repo.getUsername()) + ":" + Uri.encode(repo.getPassword()) + "@" + Uri.encode(origUrl.getAuthority())); + data.path(origUrl.getPath()); + data.appendQueryParameter("fingerprint", repo.getSigfp()); + Intent i = new Intent(context, ManageReposActivity.class); + i.setData(data.build()); + context.startActivity(i); + Utils.debugLog(TAG, "Provision processed: '" + provision.getProvisonPath() + "' prompted user ..."); + } + + } + + // remove provision file + try { + new File(provision.getProvisonPath()).delete(); + cleanupCounter++; + } catch (SecurityException e) { + // ignore this exception + Utils.debugLog(TAG, "Removing provision not possible: " + e.getMessage() + " ()"); + } + } + Utils.debugLog(TAG, "Provisions done, removed " + cleanupCounter + " provision(s)."); } } } - /** - * @return List of - */ - public List findProvisionFiles() { - return findProvisionFilesInDir(new File(DEFAULT_PROVISION_DIR)); + public List findProvisionFiles(Context context) { + String provisionDirPath = context.getExternalFilesDir(null).getAbsolutePath() + File.separator + NEW_PROVISIONS_DIR; + return findProvisionFilesInDir(new File(provisionDirPath)); } protected List findProvisionFilesInDir(File file) { - File[] files = file.listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - if (name != null && name.endsWith(".fdrp")) { - return true; + if (file == null || !file.isDirectory()) { + return new ArrayList<>(); + } + try { + File[] files = file.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if (name != null && name.endsWith(".fdrp")) { + return true; + } + return false; } - return false; - } - }); - return files != null ? Arrays.asList(files) : null; + }); + return files != null ? Arrays.asList(files) : null; + } catch (Exception e) { + Utils.debugLog(TAG, "can not search for provisions, can not access: " + file.getAbsolutePath(), e); + return new ArrayList<>(); + } } String rot13(String text) { @@ -88,9 +136,9 @@ public class Provisioner { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) { - sb.append((char)(c + 13)); + sb.append((char) (c + 13)); } else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) { - sb.append((char)(c - 13)); + sb.append((char) (c - 13)); } else { sb.append(c); } @@ -109,38 +157,40 @@ public class Provisioner { protected List extractProvisionsPlaintext(List files) { List result = new ArrayList<>(); - for (File file : files) { - ProvisionPlaintext plain = new ProvisionPlaintext(); - plain.setProvisionPath(file.getAbsolutePath()); - ZipInputStream in = null; - try { - in = new ZipInputStream(new FileInputStream(file)); - ZipEntry zipEntry = null; - while ((zipEntry = in.getNextEntry()) != null) { - String name = zipEntry.getName(); - if ("repo_provision.json".equals(name)) { - if (plain.getRepositoryProvision() != null) { - throw new IOException("provision malformed: contains more than one repo provision file."); + if (files != null) { + for (File file : files) { + ProvisionPlaintext plain = new ProvisionPlaintext(); + plain.setProvisionPath(file.getAbsolutePath()); + ZipInputStream in = null; + try { + in = new ZipInputStream(new FileInputStream(file)); + ZipEntry zipEntry = null; + while ((zipEntry = in.getNextEntry()) != null) { + String name = zipEntry.getName(); + if ("repo_provision.json".equals(name)) { + if (plain.getRepositoryProvision() != null) { + throw new IOException("provision malformed: contains more than one repo provision file."); + } + plain.setRepositoryProvision(IOUtils.toString(in, Charset.forName("UTF-8"))); + } else if ("repo_provision.ojson".equals(name)) { + if (plain.getRepositoryProvision() != null) { + throw new IOException("provision malformed: contains more than one repo provision file."); + } + plain.setRepositoryProvision(deobfuscate(IOUtils.toString(in, Charset.forName("UTF-8")))); } - plain.setRepositoryProvision(IOUtils.toString(in, Charset.forName("UTF-8"))); - } else if ("repo_provision.ojson".equals(name)) { - if (plain.getRepositoryProvision() != null) { - throw new IOException("provision malformed: contains more than one repo provision file."); - } - plain.setRepositoryProvision(deobfuscate(IOUtils.toString(in, Charset.forName("UTF-8")))); } + } catch (FileNotFoundException e) { + Utils.debugLog(TAG, String.format("finding provision '%s' failed", file.getPath()), e); + continue; + } catch (IOException e) { + Utils.debugLog(TAG, String.format("reading provision '%s' failed", file.getPath()), e); + continue; + } finally { + IOUtils.closeQuietly(in); } - } catch (FileNotFoundException e) { - Utils.debugLog(TAG, String.format("finding provision '%s' failed", file.getPath()), e); - continue; - } catch (IOException e) { - Utils.debugLog(TAG, String.format("reading provision '%s' failed", file.getPath()), e); - continue; - } finally { - IOUtils.closeQuietly(in); - } - result.add(plain); + result.add(plain); + } } return result; } @@ -150,16 +200,18 @@ public class Provisioner { List provisions = new ArrayList<>(); ObjectMapper mapper = new ObjectMapper(); - for (ProvisionPlaintext provisionPlaintext : provisionPlaintexts) { - Provision provision = new Provision(); - provision.setProvisonPath(provisionPlaintext.getProvisionPath()); - try { - provision.setRepositoryProvision( - mapper.readValue(provisionPlaintext.getRepositoryProvision(), RepositoryProvision.class)); - } catch (IOException e) { - Utils.debugLog(TAG, "could not parse repository provision", e); + if (provisionPlaintexts != null) { + for (ProvisionPlaintext provisionPlaintext : provisionPlaintexts) { + Provision provision = new Provision(); + provision.setProvisonPath(provisionPlaintext.getProvisionPath()); + try { + provision.setRepositoryProvision( + mapper.readValue(provisionPlaintext.getRepositoryProvision(), RepositoryProvision.class)); + provisions.add(provision); + } catch (IOException e) { + Utils.debugLog(TAG, "could not parse repository provision", e); + } } - provisions.add(provision); } return provisions; From c60c606dcfb0b4d3b0673e84cfa20b375759eaea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Fri, 26 Jan 2018 17:56:06 +0100 Subject: [PATCH 8/8] remove outdated comments; use File concatenation instead of separators --- app/src/main/java/org/fdroid/fdroid/Provisioner.java | 3 +-- .../main/java/org/fdroid/fdroid/views/ManageReposActivity.java | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/Provisioner.java b/app/src/main/java/org/fdroid/fdroid/Provisioner.java index 7fd443c78..e0265ac39 100644 --- a/app/src/main/java/org/fdroid/fdroid/Provisioner.java +++ b/app/src/main/java/org/fdroid/fdroid/Provisioner.java @@ -49,7 +49,7 @@ public class Provisioner { */ public static void scanAndProcess(Context context) { - File provisionDir = new File(context.getExternalFilesDir(null).getAbsolutePath() + File.separator + NEW_PROVISIONS_DIR); + File provisionDir = new File(context.getExternalFilesDir(null).getAbsolutePath(), NEW_PROVISIONS_DIR); if (!provisionDir.isDirectory()) { Utils.debugLog(TAG, "Provisions dir does not exists: '" + provisionDir.getAbsolutePath() + "' moving on ..."); @@ -70,7 +70,6 @@ public class Provisioner { if (provision.getRepositoryProvision() != null) { RepositoryProvision repo = provision.getRepositoryProvision(); - // TODO uniqx: check if repo is already enable Repo storedRepo = RepoProvider.Helper.findByAddress(context, repo.getUrl()); if (storedRepo != null) { Utils.debugLog(TAG, "Provision contains a repo which is already added: '" + provision.getProvisonPath() + "' ignoring ..."); diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 4674b4802..bc470667f 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -298,8 +298,6 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana case EXISTS_DISABLED: case EXISTS_UPGRADABLE_TO_SIGNED: case EXISTS_FINGERPRINT_MATCH: - // TODO uniqx: - //updateAndEnableExistingRepo(url, fp, username, password); updateAndEnableExistingRepo(url, fp); finishedAddingRepo(); break;