Skip to content

Commit 75cdab6

Browse files
committed
refactor PortalLiberator
1 parent d58f65d commit 75cdab6

31 files changed

Lines changed: 293 additions & 248 deletions

Readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,6 @@ This will help me add support for it.
128128
Portal support lives under `liberator/src/main/kotlin/de/binarynoise/liberator/portals`. To add a portal:
129129

130130
- Copy the `_Template` class and rename it.
131-
- Implement `PortalLiberator` with `canSolve(locationUrl)` and `solve(...)` methods.
131+
- Implement `PortalLiberator` with `canSolve(...)` and `solve(...)` methods.
132132
- Optionally annotate with `@SSID("ssid1", "ssid2", ...)`.
133133
- Submit a Pull Request.

app/src/main/kotlin/de/binarynoise/captiveportalautologin/ConnectivityChangeListenerService.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ class ConnectivityChangeListenerService : Service() {
274274

275275
@WorkerThread
276276
fun tryLiberate() {
277-
val network = networkStateLock.write {
277+
val (network, ssid) = networkStateLock.write {
278278
val state = networkState
279279
if (state == null) {
280280
log("no network")
@@ -297,7 +297,7 @@ class ConnectivityChangeListenerService : Service() {
297297
return
298298
}
299299
networkState = state.copy(liberating = true)
300-
state.network
300+
state.network to state.ssid
301301
}
302302

303303
val t = Toast.makeText(applicationContext, "Trying to liberate", Toast.LENGTH_SHORT)
@@ -311,6 +311,7 @@ class ConnectivityChangeListenerService : Service() {
311311
{ okhttpClient -> okhttpClient.socketFactory(network.socketFactory) },
312312
portalTestUrl,
313313
userAgent,
314+
ssid,
314315
).liberate()
315316

316317
t.cancel()

liberator/src/main/kotlin/de/binarynoise/liberator/Liberator.kt

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import de.binarynoise.util.okhttp.requestUrl
1010
import de.binarynoise.util.okhttp.resolveOrThrow
1111
import okhttp3.Cookie
1212
import okhttp3.FormBody
13-
import okhttp3.HttpUrl
1413
import okhttp3.Interceptor
1514
import okhttp3.MultipartBody
1615
import okhttp3.OkHttpClient
@@ -22,6 +21,7 @@ class Liberator(
2221
private val clientInit: (OkHttpClient.Builder) -> Unit,
2322
val portalTestUrl: String,
2423
private val userAgent: String,
24+
private val ssid: String?,
2525
) {
2626

2727
private val cookies: MutableSet<Cookie> = mutableSetOf()
@@ -152,30 +152,32 @@ class Liberator(
152152
}
153153
}
154154

155-
private fun recurse(response: Response, depth: Int): LiberationResult {
155+
private fun recurse(responseWithRedirect: Response, depth: Int): LiberationResult {
156156
try {
157-
val location = response.getLocation()
158-
if (location.isNullOrBlank()) return LiberationResult.UnknownPortal(response.requestUrl.toString())
159-
160-
val locationUrl: HttpUrl = response.requestUrl.resolveOrThrow(location)
157+
val location = responseWithRedirect.getLocation()
158+
if (location.isNullOrBlank()) return LiberationResult.UnknownPortal(responseWithRedirect.requestUrl.toString())
161159

160+
val response = client.get(responseWithRedirect.requestUrl.resolveOrThrow(location), null)
162161

163162
val solver: PortalLiberator? = allPortalLiberators.firstOrNull { solver ->
164-
solver.canSolve(locationUrl, response)
163+
solver.canSolve(response) && (!solver.ssidMustMatch() || (ssid != null && solver.ssidMatches(ssid)))
165164
}
166165

167-
if (solver == null) {
168-
log("unknown captive portal: ${response.getLocation()}")
166+
if (solver == null || (!PortalLiberatorConfig.experimental && solver.isExperimental())) {
167+
log("unknown captive portal: ${responseWithRedirect.getLocation()}")
169168

170169
// follow redirects and try again
171170
check(depth < 10) { "too many redirects" }
172-
return recurse(client.get(locationUrl, null), depth + 1)
171+
return recurse(response, depth + 1)
173172
}
174173

175-
solver.solve(locationUrl, client, response, cookies)
176-
return LiberationResult.Success(locationUrl.toString())
174+
log("solver ${solver::class.simpleName} found for $response.requestUrl")
175+
solver.solve(client, response, cookies)
176+
log("solver ${solver::class.simpleName} finished processing $response.requestUrl")
177+
178+
return LiberationResult.Success(response.requestUrl.toString())
177179
} catch (e: Exception) {
178-
return LiberationResult.Error(response.requestUrl.toString(), e, e.message.orEmpty())
180+
return LiberationResult.Error(responseWithRedirect.requestUrl.toString(), e, e.message.orEmpty())
179181
}
180182
}
181183

liberator/src/main/kotlin/de/binarynoise/liberator/PortalLiberator.kt

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,94 @@ package de.binarynoise.liberator
22

33
import de.binarynoise.logger.Logger.log
44
import okhttp3.Cookie
5-
import okhttp3.HttpUrl
65
import okhttp3.OkHttpClient
76
import okhttp3.Response
87

98
object PortalLiberatorConfig {
9+
/**
10+
* Enables additional liberators that are considered experimental.
11+
*/
1012
var experimental: Boolean = false
1113
set(value) {
1214
field = value
1315
log("PortalLiberatorConfig.debug = $value")
1416
}
1517
}
1618

17-
interface UnIndexedPortalLiberator {
19+
/**
20+
* Marker interface for portal liberators.
21+
*
22+
* Classes implementing this interface will be automatically discovered and included
23+
* in the portal liberation process.
24+
*/
25+
interface PortalLiberator {
1826
/**
19-
* Determines if this portal liberator can handle the given captive portal location.
27+
* Determines if this liberator can handle the given captive portal.
2028
*
21-
* @param locationUrl The HTTP URL from the Location header of the captive portal redirect response.
22-
* This is typically the URL where the captive portal login page is hosted.
23-
* @return true if this liberator knows this captive portal and can solve it, false otherwise.
29+
* This method is called during portal detection to determine if this liberator
30+
* has the capability to authenticate with the specific captive portal system.
31+
*
32+
* The [response] is the HTTP response loaded from `response.requestUrl` containing the portal content.
33+
*
34+
* @return true if this liberator can solve the portal, false otherwise.
2435
*/
25-
fun canSolve(locationUrl: HttpUrl, response: Response): Boolean
36+
fun canSolve(response: Response): Boolean
2637

2738
/**
2839
* Attempts to solve the captive portal by performing the necessary authentication actions.
2940
*
30-
* @param locationUrl The HTTP URL from the Location header that this liberator has identified as solvable.
31-
* This is the same URL passed to canSolve() and represents the portal entry point.
32-
* @param client The OkHttpClient instance configured to make HTTP requests to the portal.
33-
* @param response The HTTP response that contains the redirect to this captive portal.
34-
* @param cookies The current set of cookies collected during the portal detection process.
41+
* This method is called after [canSolve] has returned true, indicating that this liberator
42+
* can handle the detected portal. The implementation should perform all necessary HTTP
43+
* requests and form submissions to authenticate the user with the captive portal.
44+
*
45+
* The [client] is the OkHttpClient instance to use for making HTTP requests to the portal.
46+
* The [response] is the HTTP response that contains the portal this PortalLiberator wants to solve.
47+
* The [cookies] is a read-only view of the current set of cookies.
3548
*/
36-
fun solve(locationUrl: HttpUrl, client: OkHttpClient, response: Response, cookies: Set<Cookie>)
49+
fun solve(client: OkHttpClient, response: Response, cookies: Set<Cookie>)
3750
}
3851

39-
interface PortalLiberator : UnIndexedPortalLiberator
52+
fun PortalLiberator.isExperimental(): Boolean = this::class.java.annotations.any { it is Experimental }
53+
fun PortalLiberator.ssidMustMatch(): Boolean =
54+
this::class.java.annotations.filterIsInstance<SSID>().any { it.mustMatch }
4055

56+
fun PortalLiberator.ssidMatches(ssid: String): Boolean = this::class.java.annotations.filterIsInstance<SSID>()
57+
.flatMap { it.ssid.asIterable() }
58+
.partition { it.startsWith("/") && it.endsWith("/") }
59+
.let { (regex, fixed) -> ssid in fixed || regex.any { it.toRegex().matches(ssid) } }
60+
61+
/**
62+
* Annotation to specify which Wi-Fi networks (SSIDs) a portal liberator is designed for.
63+
*
64+
* This annotation is either informational ([mustMatch] is false, default)
65+
* or used to restrict portal liberator selection
66+
* to specific networks when [mustMatch] is true.
67+
*
68+
* The SSID matching supports both exact string matches and regex patterns.
69+
* Values surrounded by forward slashes (e.g., "/pattern/")
70+
* are treated as regex patterns, while other values are treated as exact matches.
71+
*/
4172
@Target(AnnotationTarget.CLASS)
42-
annotation class SSID(vararg val ssid: String)
73+
annotation class SSID(vararg val ssid: String, val mustMatch: Boolean = false)
4374

75+
/**
76+
* Annotation to mark portal liberators as verified and tested.
77+
*
78+
* This annotation indicates that the liberator has been confirmed
79+
* to work correctly with the target captive portals.
80+
*/
4481
@Target(AnnotationTarget.CLASS)
4582
annotation class Verified
83+
84+
/**
85+
* Annotation to mark portal liberators as experimental.
86+
*
87+
* This annotation indicates that the liberator is under development and is
88+
* not yet considered production ready. Experimental liberators are not
89+
* automatically enabled and must be explicitly enabled by setting
90+
* [PortalLiberatorConfig.experimental] to true.
91+
*
92+
* @see PortalLiberatorConfig.experimental
93+
*/
94+
@Target(AnnotationTarget.CLASS)
95+
annotation class Experimental

liberator/src/main/kotlin/de/binarynoise/liberator/portals/AenaES.kt

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
21
package de.binarynoise.liberator.portals
32

43
import java.util.*
54
import kotlin.time.Clock
5+
import de.binarynoise.liberator.Experimental
66
import de.binarynoise.liberator.PortalLiberator
7-
import de.binarynoise.liberator.PortalLiberatorConfig
87
import de.binarynoise.liberator.SSID
98
import de.binarynoise.util.okhttp.firstPathSegment
109
import de.binarynoise.util.okhttp.get
@@ -13,7 +12,6 @@ import de.binarynoise.util.okhttp.postJson
1312
import de.binarynoise.util.okhttp.readText
1413
import de.binarynoise.util.okhttp.requestUrl
1514
import okhttp3.Cookie
16-
import okhttp3.HttpUrl
1715
import okhttp3.HttpUrl.Companion.toHttpUrl
1816
import okhttp3.OkHttpClient
1917
import okhttp3.Response
@@ -22,27 +20,25 @@ import org.json.JSONObject
2220
// Spain Airports
2321
@Suppress("SpellCheckingInspection", "GrazieInspection", "LocalVariableName", "RedundantSuppression")
2422
@SSID("AIRPORT FREE WIFI AENA")
23+
@Experimental
2524
object AenaES : PortalLiberator {
26-
override fun canSolve(locationUrl: HttpUrl, response: Response): Boolean {
27-
return PortalLiberatorConfig.experimental && "freewifi.aena.es" == locationUrl.host
25+
override fun canSolve(response: Response): Boolean {
26+
return "freewifi.aena.es" == response.requestUrl.host
2827
}
2928

30-
override fun solve(locationUrl: HttpUrl, client: OkHttpClient, response: Response, cookies: Set<Cookie>) {
29+
override fun solve(client: OkHttpClient, response: Response, cookies: Set<Cookie>) {
3130
val freeWifiBase = "https://freewifi.aena.es/".toHttpUrl()
3231
val loginBase = "https://login.aena.es/".toHttpUrl()
33-
34-
// val response1 = client.get(locationUrl, null)
35-
// val location1 = response1.getLocation() ?: error("no location1")
32+
3633
// https://freewifi.aena.es/473fad84-1203-4d8d-8abf-6abb463db3ef?cmd=login&mac=_&ip=_&essid=%20&apname=_&apgroup=&url=_
37-
val location1 = locationUrl.toString()
34+
val pageUrl = response.requestUrl.toString()
3835

39-
val response2 = client.get(locationUrl, null) // get cookies
40-
val uuid = response2.requestUrl.firstPathSegment
41-
val gigyaApiKey: String = response2.readText().let { text ->
36+
val uuid = response.requestUrl.firstPathSegment
37+
val gigyaApiKey: String = response.readText().let { text ->
4238
val pattern = "gigyaApiKey\\s*=\\s*[\"']([^\"']+)[\"']'".toRegex()
4339
pattern.find(text)?.groupValues?.get(1) ?: error("no gigyaApiKey")
4440
}
45-
val mac = response2.requestUrl.queryParameter("mac") ?: error("no mac")
41+
val mac = response.requestUrl.queryParameter("mac") ?: error("no mac")
4642

4743
// https://freewifi.aena.es/api/portal/473fad84-1203-4d8d-8abf-6abb463db3ef
4844

@@ -79,7 +75,7 @@ object AenaES : PortalLiberator {
7975
val response6 = client.postForm(
8076
null, "https://login.aena.es/accounts.isAvailableLoginID", mapOf(
8177
"format" to "json",
82-
"pageUrl" to location1,
78+
"pageUrl" to pageUrl,
8379
"sdk" to "js_latest",
8480
"sdkBuild" to "0",
8581
"loginID" to loginID,
@@ -110,7 +106,7 @@ object AenaES : PortalLiberator {
110106
"skd" to "latest",
111107
"sdkBuild" to "0",
112108
"authMode" to "cookie",
113-
"pageUrl" to location1,
109+
"pageUrl" to pageUrl,
114110
),
115111
)
116112
val json5 = JSONObject(response5.readText())
@@ -144,7 +140,7 @@ object AenaES : PortalLiberator {
144140
loginBase, "/accounts.setAccountInfo", mapOf(
145141
"format" to "json",
146142
"source" to "showScreenSet",
147-
"pageURL" to location1,
143+
"pageURL" to pageUrl,
148144
"regToken" to regToken,
149145
"profile" to JSONObject(
150146
mapOf<String, Any>(
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
package de.binarynoise.liberator.portals
22

3+
import de.binarynoise.liberator.Experimental
34
import de.binarynoise.liberator.PortalLiberator
4-
import de.binarynoise.liberator.PortalLiberatorConfig
55
import de.binarynoise.util.okhttp.checkSuccess
66
import de.binarynoise.util.okhttp.followRedirects
77
import de.binarynoise.util.okhttp.postForm
8+
import de.binarynoise.util.okhttp.requestUrl
89
import okhttp3.Cookie
9-
import okhttp3.HttpUrl
1010
import okhttp3.OkHttpClient
1111
import okhttp3.Response
1212

1313
@Suppress("SpellCheckingInspection", "GrazieInspection", "LocalVariableName", "RedundantSuppression")
14+
@Experimental
1415
object BinarynoisePortalProxy : PortalLiberator {
15-
override fun canSolve(locationUrl: HttpUrl, response: Response): Boolean {
16-
return PortalLiberatorConfig.experimental && locationUrl.host == "binarynoise.de" && locationUrl.port == 8000
16+
override fun canSolve(response: Response): Boolean {
17+
return response.requestUrl.host == "binarynoise.de" && response.requestUrl.port == 8000
1718
}
1819

19-
override fun solve(locationUrl: HttpUrl, client: OkHttpClient, response: Response, cookies: Set<Cookie>) {
20-
client.postForm(locationUrl, "/login", emptyMap()).followRedirects(client).checkSuccess()
20+
override fun solve(client: OkHttpClient, response: Response, cookies: Set<Cookie>) {
21+
client.postForm(response.requestUrl, "/login", emptyMap()).followRedirects(client).checkSuccess()
2122
}
2223
}

liberator/src/main/kotlin/de/binarynoise/liberator/portals/BlockHouse.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,28 @@ import de.binarynoise.liberator.PortalLiberator
55
import de.binarynoise.liberator.SSID
66
import de.binarynoise.rhino.RhinoParser
77
import de.binarynoise.util.okhttp.checkSuccess
8-
import de.binarynoise.util.okhttp.get
98
import de.binarynoise.util.okhttp.parseHtml
109
import de.binarynoise.util.okhttp.postForm
1110
import de.binarynoise.util.okhttp.requestUrl
1211
import okhttp3.Cookie
13-
import okhttp3.HttpUrl
1412
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
1513
import okhttp3.OkHttpClient
1614
import okhttp3.Response
1715

1816
@Suppress("SpellCheckingInspection", "GrazieInspection", "LocalVariableName", "RedundantSuppression")
1917
@SSID("BLOCK HOUSE WIFI")
2018
object BlockHouse : PortalLiberator {
21-
override fun canSolve(locationUrl: HttpUrl, response: Response): Boolean {
22-
return locationUrl.host == "wlan.block-house.de"
19+
override fun canSolve(response: Response): Boolean {
20+
return response.requestUrl.host == "wlan.block-house.de"
2321
}
2422

25-
override fun solve(locationUrl: HttpUrl, client: OkHttpClient, response: Response, cookies: Set<Cookie>) {
26-
val response1 = client.get(locationUrl, null)
27-
response1.checkSuccess()
23+
override fun solve(client: OkHttpClient, response: Response, cookies: Set<Cookie>) {
24+
response.checkSuccess()
2825

29-
val hs_server = response1.requestUrl.queryParameter("hs_server") ?: error("no hs_server")
30-
val Qv = response1.requestUrl.queryParameter("Qv") ?: error("no Qv")
26+
val hs_server = response.requestUrl.queryParameter("hs_server") ?: error("no hs_server")
27+
val Qv = response.requestUrl.queryParameter("Qv") ?: error("no Qv")
3128

32-
val scriptNode = response1.parseHtml()
29+
val scriptNode = response.parseHtml()
3330
.getElementsByTag("script")
3431
.find { listOf("postToUrl", "port", "hs_server").all { str -> it.data().contains(str) } }
3532
?: error("no script found")

0 commit comments

Comments
 (0)