NewServerDialog.kt 12.4 KB
Newer Older
1
2
3
4
5
6
7
package com.frostnerd.smokescreen.dialog

import android.content.Context
import android.content.DialogInterface
import android.os.Vibrator
import android.text.Editable
import android.text.TextWatcher
8
9
10
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
11
import android.widget.EditText
12
import com.frostnerd.dnstunnelproxy.Decision
Daniel Wolf's avatar
Daniel Wolf committed
13
14
import com.frostnerd.dnstunnelproxy.DnsServerConfiguration
import com.frostnerd.dnstunnelproxy.DnsServerInformation
15
import com.frostnerd.encrypteddnstunnelproxy.*
16
import com.frostnerd.encrypteddnstunnelproxy.tls.AbstractTLSDnsHandle
Daniel Wolf's avatar
Daniel Wolf committed
17
18
import com.frostnerd.encrypteddnstunnelproxy.tls.TLS
import com.frostnerd.encrypteddnstunnelproxy.tls.TLSUpstreamAddress
19
20
21
import com.frostnerd.lifecyclemanagement.BaseDialog
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.getPreferences
22
import com.frostnerd.smokescreen.log
23
24
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
25
import kotlinx.android.synthetic.main.dialog_new_server.*
26
import kotlinx.android.synthetic.main.dialog_new_server.view.*
27

Daniel Wolf's avatar
Daniel Wolf committed
28
29
/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
30
 *
Daniel Wolf's avatar
Daniel Wolf committed
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 * 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.
45
46
47
 */
class NewServerDialog(
    context: Context,
Daniel Wolf's avatar
Daniel Wolf committed
48
    title: String? = null,
49
    var dnsOverHttps: Boolean,
Daniel Wolf's avatar
Daniel Wolf committed
50
    onServerAdded: (serverInfo: DnsServerInformation<*>) -> Unit
51
) : BaseDialog(context, context.getPreferences().theme.dialogStyle) {
52
    private var validationRegex = NewServerDialog.SERVER_URL_REGEX
53
54
55

    companion object {
        val SERVER_URL_REGEX =
Daniel Wolf's avatar
Daniel Wolf committed
56
57
58
59
60
            Regex(
                "^\\s*(?:https://)?([a-z0-9][a-z0-9-.]*[a-z0-9])(?::[1-9][0-9]{0,4})?(/[a-z0-9-.]+)*(/)?\\s*$",
                RegexOption.IGNORE_CASE
            )
        val TLS_REGEX = Regex("^\\s*([a-z0-9][a-z0-9-.]*[a-z0-9])(?::[1-9][0-9]{0,4})?\\s*$", RegexOption.IGNORE_CASE)
61
62
63
64
    }

    init {
        val view = layoutInflater.inflate(R.layout.dialog_new_server, null, false)
65
        setHintAndTitle(view,dnsOverHttps, title)
66
67
68
        setView(view)

        setButton(
69
            DialogInterface.BUTTON_NEUTRAL, context.getString(android.R.string.cancel)
70
71
72
        ) { _, _ -> }

        setButton(
73
            DialogInterface.BUTTON_POSITIVE, context.getString(android.R.string.ok)
74
75
        ) { _, _ -> }

76
        setOnShowListener {
77
78
            addUrlTextWatcher(primaryServerWrap, primaryServer, false)
            addUrlTextWatcher(secondaryServerWrap, secondaryServer, true)
79
80
81
82
83
84
85
86
            serverName.addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {
                    serverNameWrap.error = if (s.isNullOrBlank()) context.getString(R.string.error_invalid_servername)
                    else null
                }

                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
                }
87

88
89
90
91
                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                }
            })
            serverNameWrap.error = context.getString(R.string.error_invalid_servername)
92
93
94
            getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
                if (inputsValid()) {
                    val name = serverName.text.toString()
95
                    var primary = primaryServer.text.toString().trim()
Daniel Wolf's avatar
Daniel Wolf committed
96
97
                    var secondary =
                        if (secondaryServer.text.isNullOrBlank()) null else secondaryServer.text.toString().trim()
98

99
                    if (primary.startsWith("https")) primary = primary.replace("https://", "")
Daniel Wolf's avatar
Daniel Wolf committed
100
101
                    if (secondary != null && secondary.startsWith("https")) secondary =
                        secondary.replace("https://", "")
102
                    invokeCallback(name, primary, secondary, onServerAdded)
103
104
105
106
107
108
                    dismiss()
                } else {
                    val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
                    vibrator.vibrate(250)
                }
            }
109
110
111
112
113
114
115
            val spinnerAdapter = ArrayAdapter<String>(
                context, android.R.layout.simple_spinner_item,
                arrayListOf(
                    context.getString(R.string.dialog_serverconfiguration_https),
                    context.getString(R.string.dialog_serverconfiguration_tls)
                )
            )
116
            spinnerAdapter.setDropDownViewResource(R.layout.item_tasker_action_spinner_dropdown_item)
117
            serverType.adapter = spinnerAdapter
118
            serverType.setSelection(if (dnsOverHttps) 0 else 1)
119
120
            serverType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
                override fun onNothingSelected(parent: AdapterView<*>?) {}
121
                override fun onItemSelected(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
122
123
                    validationRegex = if (position == 0) NewServerDialog.SERVER_URL_REGEX else NewServerDialog.TLS_REGEX
                    dnsOverHttps = position == 0
124
                    setHintAndTitle(view, dnsOverHttps, title)
125
126
127
128
                    primaryServer.text = primaryServer.text
                    secondaryServer.text = secondaryServer.text
                }
            }
129
130
131
        }
    }

132
133
134
135
    private fun setHintAndTitle(view:View, dnsOverHttps: Boolean, titleOverride:String?) {
        if (dnsOverHttps) {
            if(titleOverride == null) setTitle(R.string.dialog_newserver_title_https)
            view.primaryServer.setHint(R.string.dialog_newserver_primaryserver_hint)
136
137
138
            view.secondaryServer.apply {
                if(isFocused || error != null) setHint(R.string.dialog_newserver_secondaryserver_hint)
            }
139
140
141
        }else {
            if(titleOverride == null) setTitle(R.string.dialog_newserver_title_tls)
            view.primaryServer.setHint(R.string.dialog_newserver_primaryserver_hint_dot)
142
143
144
            view.secondaryServer.apply {
                if(isFocused || error != null) setHint(R.string.dialog_newserver_secondaryserver_hint_dot)
            }
145
146
147
148
        }
        if(titleOverride != null) setTitle(titleOverride)
    }

Daniel Wolf's avatar
Daniel Wolf committed
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
    private fun invokeCallback(
        name: String,
        primary: String,
        secondary: String?,
        onServerAdded: (DnsServerInformation<*>) -> Unit
    ) {
        if (dnsOverHttps) {
            val requestType = mapOf(RequestType.WIREFORMAT_POST to ResponseType.WIREFORMAT)
            val serverInfo = mutableListOf<HttpsDnsServerConfiguration>()
            serverInfo.add(
                HttpsDnsServerConfiguration(
                    address = createHttpsUpstreamAddress(primary),
                    requestTypes = requestType,
                    experimental = false
                )
            )
            if (!secondary.isNullOrBlank()) serverInfo.add(
                HttpsDnsServerConfiguration(
                    address = createHttpsUpstreamAddress(
                        secondary
                    ), requestTypes = requestType, experimental = false
                )
            )
            onServerAdded.invoke(
                HttpsDnsServerInformation(
                    name,
                    HttpsDnsServerSpecification(
                        Decision.UNKNOWN,
                        Decision.UNKNOWN,
                        Decision.UNKNOWN,
                        Decision.UNKNOWN
                    ),
                    serverInfo,
                    emptyList()
                )
            )
        } else {
            val serverInfo = mutableListOf<DnsServerConfiguration<TLSUpstreamAddress>>()
            serverInfo.add(
                DnsServerConfiguration(
                    address = createTlsUpstreamAddress(primary),
                    experimental = false,
                    supportedProtocols = listOf(TLS),
                    preferredProtocol = TLS
                )
194
            )
Daniel Wolf's avatar
Daniel Wolf committed
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
            if (!secondary.isNullOrBlank()) serverInfo.add(
                DnsServerConfiguration(
                    address = createTlsUpstreamAddress(
                        secondary
                    ), experimental = false, supportedProtocols = listOf(TLS), preferredProtocol = TLS
                )
            )
            onServerAdded.invoke(
                DnsServerInformation(
                    name,
                    HttpsDnsServerSpecification(
                        Decision.UNKNOWN,
                        Decision.UNKNOWN,
                        Decision.UNKNOWN,
                        Decision.UNKNOWN
                    ),
                    serverInfo,
                    emptyList()
                )
            )
        }
216
217
    }

Daniel Wolf's avatar
Daniel Wolf committed
218
    private fun createHttpsUpstreamAddress(url: String): HttpsUpstreamAddress {
219
        context.log("Creating HttpsUpstreamAddress for `$url`")
220
        var host = ""
Daniel Wolf's avatar
Daniel Wolf committed
221
222
223
        var port: Int? = null
        var path: String? = null
        if (url.contains(":")) {
224
225
            host = url.split(":")[0]
            port = url.split(":")[1].split("/")[0].toInt()
Daniel Wolf's avatar
Daniel Wolf committed
226
            if (port > 65535) port = null
227
        }
Daniel Wolf's avatar
Daniel Wolf committed
228
        if (url.contains("/")) {
229
            path = url.split("/")[1]
Daniel Wolf's avatar
Daniel Wolf committed
230
            if (host == "") host = url.split("/")[0]
231
        }
Daniel Wolf's avatar
Daniel Wolf committed
232
        if (host == "") host = url
233
234
235
236
237

        return AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated { allServer ->
            if(path != null) emptyList()
            else allServer.values.filter {
                it.servers.any { server ->
Daniel Wolf's avatar
Daniel Wolf committed
238
                    server.address.host == host && (port == null || server.address.port == port)
239
240
241
                }
            }
        }.firstOrNull()?.servers?.firstOrNull {
Daniel Wolf's avatar
Daniel Wolf committed
242
            it.address.host == host
243
        }?.address ?: if (port != null && path != null) HttpsUpstreamAddress(host, port, path)
Daniel Wolf's avatar
Daniel Wolf committed
244
245
        else if (port != null) HttpsUpstreamAddress(host, port)
        else if (path != null) HttpsUpstreamAddress(host, urlPath = path)
246
247
248
        else HttpsUpstreamAddress(host)
    }

Daniel Wolf's avatar
Daniel Wolf committed
249
250
    private fun createTlsUpstreamAddress(host: String): TLSUpstreamAddress {
        context.log("Creating TLSUpstreamAddress for `$host`")
251
        val parsedHost:String
Daniel Wolf's avatar
Daniel Wolf committed
252
253
254
255
256
257
        var port: Int? = null
        if (host.contains(":")) {
            parsedHost = host.split(":")[0]
            port = host.split(":")[1].split("/")[0].toInt()
            if (port > 65535) port = null
        } else parsedHost = host
258
259
260
        return AbstractTLSDnsHandle.waitUntilKnownServersArePopulated { allServer ->
            allServer.values.filter {
                it.servers.any { server ->
Daniel Wolf's avatar
Daniel Wolf committed
261
                    server.address.host == parsedHost && (port == null || server.address.port == port)
262
263
264
                }
            }
        }.firstOrNull()?.servers?.firstOrNull {
Daniel Wolf's avatar
Daniel Wolf committed
265
            it.address.host == parsedHost
266
        }?.address ?: if (port != null) TLSUpstreamAddress(parsedHost, port)
Daniel Wolf's avatar
Daniel Wolf committed
267
268
269
        else TLSUpstreamAddress(parsedHost)
    }

270
    fun addUrlTextWatcher(input: TextInputLayout, editText: TextInputEditText, emptyAllowed: Boolean) {
271
272
        editText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable) {
Daniel Wolf's avatar
Daniel Wolf committed
273
274
275
276
                val valid =
                    (emptyAllowed && s.isBlank()) || (dnsOverHttps && SERVER_URL_REGEX.matches(s.toString())) || (!dnsOverHttps && TLS_REGEX.matches(
                        s.toString()
                    ))
277

278
279
280
                input.error = if (valid) {
                    null
                } else context.getString(R.string.error_invalid_url)
281
282
283
284
285
286
287
288
289
290
291
            }

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            }

        })
    }

292
293
294
    fun inputsValid(): Boolean = serverNameWrap.error == null &&
            primaryServerWrap.error == null &&
            secondaryServerWrap.error == null
295
296
297

    override fun destroy() {}
}