From 35c97a777229aea6d815fb91f2fe31d202d1ade7 Mon Sep 17 00:00:00 2001 From: seven332 Date: Fri, 23 Mar 2018 12:10:03 +0800 Subject: [PATCH] Add Hosts --- .../main/java/com/hippo/ehviewer/Hosts.java | 350 ++++++++++++++++++ .../java/com/hippo/ehviewer/HostsTest.java | 127 +++++++ 2 files changed, 477 insertions(+) create mode 100644 app/src/main/java/com/hippo/ehviewer/Hosts.java create mode 100644 app/src/test/java/com/hippo/ehviewer/HostsTest.java diff --git a/app/src/main/java/com/hippo/ehviewer/Hosts.java b/app/src/main/java/com/hippo/ehviewer/Hosts.java new file mode 100644 index 000000000..39b8f29b0 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/Hosts.java @@ -0,0 +1,350 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer; + +/* + * Created by Hippo on 2018/3/21. + */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.support.annotation.Nullable; +import android.util.Pair; +import com.hippo.database.MSQLiteBuilder; +import com.hippo.util.SqlUtils; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +public class Hosts { + + private static final int VERSION_1 = 1; + private static final String TABLE_HOSTS = "HOSTS"; + private static final String COLUMN_HOST = "HOST"; + private static final String COLUMN_IP = "IP"; + + private static final int DB_VERSION = VERSION_1; + + private final SQLiteOpenHelper helper; + private final SQLiteDatabase db; + + public Hosts(Context context, String name) { + helper = new MSQLiteBuilder() + .version(VERSION_1) + .createTable(TABLE_HOSTS) + .insertColumn(TABLE_HOSTS, COLUMN_HOST, String.class) + .insertColumn(TABLE_HOSTS, COLUMN_IP, String.class) + .build(context, name, DB_VERSION); + db = helper.getWritableDatabase(); + } + + /** + * Gets a InetAddress with the host. + */ + @Nullable + public InetAddress get(String host) { + if (!isValidHost(host)) { + return null; + } + + Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[] {host}); + try { + if (cursor.moveToNext()) { + String ip = SqlUtils.getString(cursor, COLUMN_IP, null); + return toInetAddress(host, ip); + } else { + return null; + } + } finally { + cursor.close(); + } + } + + private boolean contains(String host) { + Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[] {host}); + try { + return cursor.moveToNext(); + } finally { + cursor.close(); + } + } + + /** + * Puts the host-ip pair into this hosts. + */ + public boolean put(String host, String ip) { + if (!isValidHost(host) || !isValidIp(ip)) { + return false; + } + + ContentValues values = new ContentValues(); + values.put(COLUMN_HOST, host); + values.put(COLUMN_IP, ip); + + if (contains(host)) { + db.update(TABLE_HOSTS, values, COLUMN_HOST + " = ?", new String[] { host }); + } else { + db.insert(TABLE_HOSTS, null, values); + } + + return true; + } + + /** + * Puts delete the entry with the host. + */ + public void delete(String host) { + db.delete(TABLE_HOSTS, COLUMN_HOST + " = ?", new String[] { host }); + } + + /** + * Get all data from this host. + */ + public List> getAll() { + List> result = new ArrayList<>(); + + Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + ";", null); + try { + while (cursor.moveToNext()) { + String host = SqlUtils.getString(cursor, COLUMN_HOST, null); + String ip = SqlUtils.getString(cursor, COLUMN_IP, null); + + InetAddress inetAddress = toInetAddress(host, ip); + if (inetAddress == null) { + continue; + } + + result.add(new Pair<>(host, ip)); + } + } finally { + cursor.close(); + } + + return result; + } + + @Nullable + private static InetAddress toInetAddress(String host, String ip) { + if (!isValidHost(host)) { + return null; + } + + if (ip == null) { + return null; + } + + byte[] bytes = parseV4(ip); + if (bytes == null) { + bytes = parseV6(ip); + } + if (bytes == null) { + return null; + } + + try { + return InetAddress.getByAddress(host, bytes); + } catch (UnknownHostException e) { + return null; + } + } + + /** + * Returns true if the host is valid. + */ + public static boolean isValidHost(String host) { + if (host == null) { + return false; + } + + if (host.length() > 253) { + return false; + } + + int labelLength = 0; + for (int i = 0, n = host.length(); i < n; i++) { + char ch = host.charAt(i); + + if (ch == '.') { + if (labelLength < 1 || labelLength > 63) { + return false; + } + labelLength = 0; + } else { + labelLength++; + } + + if ((ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-' && ch != '.') { + return false; + } + } + + if (labelLength < 1 || labelLength > 63) { + return false; + } + + return true; + } + + /** + * Returns true if the ip is valid. + */ + public static boolean isValidIp(String ip) { + return ip != null && (parseV4(ip) != null || parseV6(ip) != null); + } + + // org.xbill.DNS.Address.parseV4 + @Nullable + private static byte[] parseV4(String s) { + int numDigits; + int currentOctet; + byte [] values = new byte[4]; + int currentValue; + int length = s.length(); + + currentOctet = 0; + currentValue = 0; + numDigits = 0; + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (c >= '0' && c <= '9') { + /* Can't have more than 3 digits per octet. */ + if (numDigits == 3) + return null; + /* Octets shouldn't start with 0, unless they are 0. */ + if (numDigits > 0 && currentValue == 0) + return null; + numDigits++; + currentValue *= 10; + currentValue += (c - '0'); + /* 255 is the maximum value for an octet. */ + if (currentValue > 255) + return null; + } else if (c == '.') { + /* Can't have more than 3 dots. */ + if (currentOctet == 3) + return null; + /* Two consecutive dots are bad. */ + if (numDigits == 0) + return null; + values[currentOctet++] = (byte) currentValue; + currentValue = 0; + numDigits = 0; + } else + return null; + } + /* Must have 4 octets. */ + if (currentOctet != 3) + return null; + /* The fourth octet can't be empty. */ + if (numDigits == 0) + return null; + values[currentOctet] = (byte) currentValue; + return values; + } + + // org.xbill.DNS.Address.parseV6 + @Nullable + private static byte[] parseV6(String s) { + int range = -1; + byte [] data = new byte[16]; + + String [] tokens = s.split(":", -1); + + int first = 0; + int last = tokens.length - 1; + + if (tokens[0].length() == 0) { + // If the first two tokens are empty, it means the string + // started with ::, which is fine. If only the first is + // empty, the string started with :, which is bad. + if (last - first > 0 && tokens[1].length() == 0) + first++; + else + return null; + } + + if (tokens[last].length() == 0) { + // If the last two tokens are empty, it means the string + // ended with ::, which is fine. If only the last is + // empty, the string ended with :, which is bad. + if (last - first > 0 && tokens[last - 1].length() == 0) + last--; + else + return null; + } + + if (last - first + 1 > 8) + return null; + + int i, j; + for (i = first, j = 0; i <= last; i++) { + if (tokens[i].length() == 0) { + if (range >= 0) + return null; + range = j; + continue; + } + + if (tokens[i].indexOf('.') >= 0) { + // An IPv4 address must be the last component + if (i < last) + return null; + // There can't have been more than 6 components. + if (i > 6) + return null; + byte [] v4addr = parseV4(tokens[i]); + if (v4addr == null) + return null; + for (int k = 0; k < 4; k++) + data[j++] = v4addr[k]; + break; + } + + try { + for (int k = 0; k < tokens[i].length(); k++) { + char c = tokens[i].charAt(k); + if (Character.digit(c, 16) < 0) + return null; + } + int x = Integer.parseInt(tokens[i], 16); + if (x > 0xFFFF || x < 0) + return null; + data[j++] = (byte)(x >>> 8); + data[j++] = (byte)(x & 0xFF); + } + catch (NumberFormatException e) { + return null; + } + } + + if (j < 16 && range < 0) + return null; + + if (range >= 0) { + int empty = 16 - j; + System.arraycopy(data, range, data, range + empty, j - range); + for (i = range; i < range + empty; i++) + data[i] = 0; + } + + return data; + } +} diff --git a/app/src/test/java/com/hippo/ehviewer/HostsTest.java b/app/src/test/java/com/hippo/ehviewer/HostsTest.java new file mode 100644 index 000000000..3d67bae82 --- /dev/null +++ b/app/src/test/java/com/hippo/ehviewer/HostsTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer; + +/* + * Created by Hippo on 2018/3/22. + */ + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.util.Pair; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class HostsTest { + + @Test + public void testIsValidHost() { + assertTrue(Hosts.isValidHost("ni.hao")); + assertFalse(Hosts.isValidHost("ni.ha~o")); + assertFalse(Hosts.isValidHost(".ni.hao")); + assertFalse(Hosts.isValidHost("ni.hao.")); + assertFalse(Hosts.isValidHost("ni..hao.")); + assertFalse(Hosts.isValidHost(".")); + assertFalse(Hosts.isValidHost("")); + assertFalse(Hosts.isValidHost(null)); + } + + @Test + public void testIsValidIp() { + assertTrue(Hosts.isValidIp("127.0.0.1")); + assertTrue(Hosts.isValidIp("0.0.0.0")); + assertFalse(Hosts.isValidIp("324.0.0.1")); + assertFalse(Hosts.isValidIp("127.0.0.")); + assertFalse(Hosts.isValidIp("127.0.0")); + assertFalse(Hosts.isValidIp("-1.0.0")); + assertFalse(Hosts.isValidIp("")); + assertFalse(Hosts.isValidIp(null)); + } + + @Test + public void testGet() { + Hosts hosts = new Hosts(RuntimeEnvironment.application, "hosts.db"); + + assertEquals(null, hosts.get("ni.hao")); + hosts.put("ni.hao", "127.0.0.1"); + assertEquals("ni.hao/127.0.0.1", hosts.get("ni.hao").toString()); + + assertEquals(null, hosts.get(null)); + } + + @Test + public void testPut() { + Hosts hosts = new Hosts(RuntimeEnvironment.application, "hosts.db"); + + assertEquals(null, hosts.get("ni.hao")); + assertEquals(true, hosts.put("ni.hao", "127.0.0.1")); + assertEquals("ni.hao/127.0.0.1", hosts.get("ni.hao").toString()); + assertEquals(true, hosts.put("ni.hao", "127.0.0.2")); + assertEquals("ni.hao/127.0.0.2", hosts.get("ni.hao").toString()); + + assertEquals(false, hosts.put(".wo.hao", "127.0.0.1")); + assertEquals(null, hosts.get(".wo.hao")); + + assertEquals(false, hosts.put("wo.hao", ".127.0.0.1")); + assertEquals(null, hosts.get("wo.hao")); + + assertEquals(false, hosts.put(null, "127.0.0.1")); + assertEquals(false, hosts.put("wo.hao", null)); + assertEquals(false, hosts.put(null, null)); + assertEquals(null, hosts.get("wo.hao")); + } + + @Test + public void testDelete() { + Hosts hosts = new Hosts(RuntimeEnvironment.application, "hosts.db"); + + hosts.put("ni.hao", "127.0.0.1"); + assertEquals("ni.hao/127.0.0.1", hosts.get("ni.hao").toString()); + hosts.put("ni.hao", "127.0.0.2"); + hosts.delete("ni.hao"); + assertEquals(null, hosts.get("ni.hao")); + + hosts.delete(null); + } + + @Test + public void testGetAll() { + Hosts hosts = new Hosts(RuntimeEnvironment.application, "hosts.db"); + + List> all = hosts.getAll(); + assertEquals(0, all.size()); + + hosts.put("ni.hao", "127.0.0.1"); + hosts.put("wo.hao", "127.0.0.2"); + + all = hosts.getAll(); + assertEquals(2, all.size()); + + assertEquals("ni.hao", all.get(0).first); + assertEquals("127.0.0.1", all.get(0).second); + assertEquals("wo.hao", all.get(1).first); + assertEquals("127.0.0.2", all.get(1).second); + } +}