RuleImportService.kt 9.98 KB
Newer Older
1
2
package com.frostnerd.smokescreen.service

3
import android.app.NotificationManager
4
import android.app.PendingIntent
5
import android.app.Service
6
import android.content.Context
7
8
import android.content.Intent
import android.os.IBinder
9
10
import androidx.core.app.NotificationCompat
import com.frostnerd.smokescreen.R
11
12
13
import com.frostnerd.smokescreen.database.entities.DnsRule
import com.frostnerd.smokescreen.database.entities.HostSource
import com.frostnerd.smokescreen.database.getDatabase
14
import com.frostnerd.smokescreen.log
15
import com.frostnerd.smokescreen.sendLocalBroadcast
16
import com.frostnerd.smokescreen.util.Notifications
17
18
19
20
21
22
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import org.minidns.record.Record
Daniel Wolf's avatar
Daniel Wolf committed
23
import java.io.*
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.lang.IllegalStateException
import java.util.regex.Matcher
import java.util.regex.Pattern

/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
 *
 * This program 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.

 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * You can contact the developer at daniel.wolf@frostnerd.com.
 */
class RuleImportService : Service() {
    private var importJob: Job? = null
    private val DNSMASQ_MATCHER = Pattern.compile("^address=/([^/]+)/(?:([0-9.]+)|([0-9a-fA-F:]+))").matcher("")
    private val HOSTS_MATCHER =
        Pattern.compile("^((?:[A-Fa-f0-9:]|[0-9.])+)\\s+([a-zA-Z0-9.]+).*")
            .matcher("")
    private val DOMAINS_MATCHER = Pattern.compile("^([A-Za-z0-9][A-Za-z0-9\\-.]+)").matcher("")
    private val ADBLOCK_MATCHER = Pattern.compile("^\\|\\|(.*)\\^$").matcher("")
54
    private var notification: NotificationCompat.Builder? = null
55
    private var ruleCount:Int = 0
56

57
58
59
60
    companion object {
        const val BROADCAST_IMPORT_DONE = "com.frostnerd.nebulo.RULE_IMPORT_DONE"
    }

61
62
63
64
65
    private val httpClient by lazy {
        OkHttpClient()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
66
        if(intent != null && intent.hasExtra("abort")) {
67
            abortImport()
68
        }
69
        createNotification()
70
71
72
73
        startWork()
        return START_STICKY
    }

74
75
76
77
78
79
80
81
82
83
    private fun abortImport() {
        importJob?.let {
            it.cancel()
            val dnsRuleDao = getDatabase().dnsRuleDao()
            dnsRuleDao.deleteStagedRules()
            dnsRuleDao.commitStaging()
        }
        importJob = null
    }

84
85
86
87
88
89
90
91
92
93
94
95
    private fun createNotification() {
        if (notification == null) {
            notification = NotificationCompat.Builder(this, Notifications.servicePersistentNotificationChannel(this))
            notification!!.setSmallIcon(R.drawable.ic_mainnotification)
            notification!!.setOngoing(true)
            notification!!.setAutoCancel(false)
            notification!!.setSound(null)
            notification!!.setOnlyAlertOnce(true)
            notification!!.setUsesChronometer(true)
            notification!!.setContentTitle(getString(R.string.notification_ruleimport_title))
            notification!!.setContentText(getString(R.string.notification_ruleimport_secondarymessage))
            notification!!.setProgress(100, 0, true)
96
97
98
            val abortPendingAction = PendingIntent.getService(this, 1, Intent(this, RuleImportService::class.java).putExtra("abort", true), PendingIntent.FLAG_CANCEL_CURRENT)
            val abortAction = NotificationCompat.Action(R.drawable.ic_times, getString(R.string.all_abort), abortPendingAction)
            notification!!.addAction(abortAction)
99
100
101
102
        }
        startForeground(3, notification!!.build())
    }

103
104
105
106
107
108
109
110
111
    private fun showSuccessNotification() {
        val successNotification = NotificationCompat.Builder(this, Notifications.getDefaultNotificationChannelId(this))
        successNotification.setSmallIcon(R.drawable.ic_mainnotification)
        successNotification.setAutoCancel(true)
        successNotification.setContentTitle(getString(R.string.notification_ruleimportfinished_title))
        successNotification.setContentText(getString(R.string.notification_ruleimportfinished_message, ruleCount))
        (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(4, successNotification.build())
    }

112
113
114
115
116
117
118
119
120
121
122
123
124
125
    private fun updateNotification(source: HostSource, count:Int, maxCount:Int) {
        if (notification != null) {
            notification?.setContentText(
                getString(
                    R.string.notification_ruleimport_message,
                    source.name,
                    source.source
                )
            )
            notification?.setProgress(maxCount, count, false)
            startForeground(3, notification!!.build())
        }
    }

126
127
    private fun startWork() {
        importJob = GlobalScope.launch {
128
129
            val dnsRuleDao = getDatabase().dnsRuleDao()
            dnsRuleDao.markNonUserRulesForDeletion()
130
            var count = 0
Daniel Wolf's avatar
Daniel Wolf committed
131
132
            val maxCount = getDatabase().hostSourceDao().getEnabledCount()
            getDatabase().hostSourceDao().getAllEnabled().forEach {
133
                log("Importing HostSource $it")
134
135
                if(importJob != null && importJob?.isCancelled == false) {
                    updateNotification(it, count, maxCount.toInt())
136
                    count++
137
                    if (it.isFileSource) {
Daniel Wolf's avatar
Daniel Wolf committed
138
139
140
141
142
143
144
145
                        try {
                            val file = File(it.source)
                            if(file.canRead()) {
                                processLines(it, FileInputStream(file))
                            }
                        } catch (ex:Exception) {
                            ex.printStackTrace()
                        }
146
147
148
149
150
                    } else {
                        val request = Request.Builder().url(it.source)
                        val response = httpClient.newCall(request.build()).execute()
                        if (response.isSuccessful) {
                            processLines(it, response.body()!!.byteStream())
151
152
                        } else {
                            log("Downloading resource of $it failed.")
153
                        }
154
155
156
                    }
                }
            }
157
158
159
160
            if(importJob != null && importJob?.isCancelled == false) {
                dnsRuleDao.deleteMarkedRules()
                dnsRuleDao.commitStaging()
            }
161
            importJob = null
162
            showSuccessNotification()
163
            stopForeground(true)
164
165
166
167
            stopSelf()
        }
    }

168
    private fun processLines(source: HostSource, stream: InputStream) {
169
        val parsers = mutableMapOf(
170
171
172
173
            DNSMASQ_MATCHER to (0 to mutableListOf<Host>()),
            HOSTS_MATCHER to (0 to mutableListOf()),
            DOMAINS_MATCHER to (0 to mutableListOf()),
            ADBLOCK_MATCHER to (0 to mutableListOf())
174
175
176
        )
        BufferedReader(InputStreamReader(stream)).useLines { lines ->
            lines.forEach { line ->
177
                if(importJob != null && importJob?.isCancelled == false) {
178
                    if (parsers.isNotEmpty() && !line.trim().startsWith("#") && !line.trim().startsWith("!") && !line.isBlank()) {
179
180
181
                        val iterator = parsers.iterator()
                        for ((matcher, hosts) in iterator) {
                            if (matcher.reset(line).matches()) {
182
                                hosts.second.addAll(processLine(matcher))
183
184
                                commitLines(source, parsers)
                            } else {
185
186
187
                                log("Matcher $matcher mismatch for line $line")
                                if(hosts.first > 5) iterator.remove()
                                else parsers[matcher] = hosts.copy(hosts.first + 1)
188
                            }
189
190
                        }
                    }
191
192
                } else {
                    return@useLines
193
194
195
196
197
198
                }
            }
        }
        commitLines(source, parsers, true)
    }

199
200
    private fun commitLines(
        source: HostSource,
201
        parsers: Map<Matcher, Pair<Int, MutableList<Host>>>,
202
203
204
        forceCommit: Boolean = false
    ) {
        if (parsers.size == 1) {
205
            val hosts = parsers[parsers.keys.first()]!!.second
206
            if (hosts.size > 5000 || forceCommit) {
207
208
209
                getDatabase().dnsRuleDao().insertAll(hosts.map {
                    DnsRule(it.type, it.host, it.target, source.id)
                })
210
                ruleCount += hosts.size
211
212
213
214
215
                hosts.clear()
            }
        }
    }

216
    private fun processLine(matcher: Matcher): Collection<Host> {
217
        when {
218
            matcher.groupCount() == 1 -> return listOf(Host(matcher.group(1), "0.0.0.0", Record.TYPE.A), Host(matcher.group(1), "::1", Record.TYPE.AAAA))
219
220
221
            matcher == DNSMASQ_MATCHER -> {
                val host = matcher.group(1)
                val target = matcher.group(2)
222
                return listOf(Host(host, target, if (target.contains(":")) Record.TYPE.AAAA else Record.TYPE.A))
223
224
225
226
            }
            matcher == HOSTS_MATCHER -> {
                val target = matcher.group(1)
                val host = matcher.group(2)
227
                return listOf(Host(host, target, if (target.contains(":")) Record.TYPE.AAAA else Record.TYPE.A))
228
229
230
231
232
233
234
            }
        }
        throw IllegalStateException()
    }

    override fun onDestroy() {
        super.onDestroy()
235
        abortImport()
236
        sendLocalBroadcast(Intent(BROADCAST_IMPORT_DONE))
237
238
239
240
241
242
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

243
    private data class Host(val host: String, val target: String, val type: Record.TYPE)
244
}