feat: profile picture downloader

- new events: add view, network request
- fix anonymous story viewing
This commit is contained in:
rhunk 2023-08-25 02:50:41 +02:00
parent 05990a4b72
commit d0668b67d4
17 changed files with 252 additions and 108 deletions

View File

@ -1,6 +1,11 @@
package me.rhunk.snapenhance
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent
import me.rhunk.snapenhance.core.eventbus.events.impl.OnSnapInteractionEvent
import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent
import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent
@ -9,6 +14,8 @@ import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.manager.Manager
import me.rhunk.snapenhance.util.ktx.getObjectField
import me.rhunk.snapenhance.util.ktx.setObjectField
import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper
class EventDispatcher(
@ -57,5 +64,48 @@ class EventDispatcher(
}
}
}
ViewGroup::class.java.getMethod(
"addView",
View::class.java,
Int::class.javaPrimitiveType,
LayoutParams::class.java
).hook(HookStage.BEFORE) { param ->
context.event.post(
AddViewEvent(
parent = param.thisObject(),
view = param.arg(0),
index = param.arg(1),
layoutParams = param.arg(2)
).apply {
adapter = param
}
)?.also { event ->
with(param) {
setArg(0, event.view)
setArg(1, event.index)
setArg(2, event.layoutParams)
}
if (event.canceled) param.setResult(null)
}
}
context.classCache.networkApi.hook("submit", HookStage.BEFORE) { param ->
val request = param.arg<Any>(0)
context.event.post(
NetworkApiRequestEvent(
url = request.getObjectField("mUrl") as String,
callback = param.arg(4),
request = request,
).apply {
adapter = param
}
)?.also { event ->
event.request.setObjectField("mUrl", event.url)
if (event.canceled) param.setResult(null)
}
}
}
}

View File

@ -21,7 +21,7 @@ import me.rhunk.snapenhance.database.DatabaseAccess
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.manager.impl.ActionManager
import me.rhunk.snapenhance.manager.impl.FeatureManager
import me.rhunk.snapenhance.util.download.DownloadServer
import me.rhunk.snapenhance.util.download.HttpServer
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.reflect.KClass
@ -50,7 +50,7 @@ class ModContext {
val mappings = MappingsWrapper()
val actionManager = ActionManager(this)
val database = DatabaseAccess(this)
val downloadServer = DownloadServer()
val httpServer = HttpServer()
val messageSender = MessageSender(this)
val classCache get() = SnapEnhance.classCache
val resources: Resources get() = androidContext.resources

View File

@ -25,13 +25,13 @@ class UserInterfaceTweaks : ConfigContainer() {
"hide_call_buttons"
)
val disableSpotlight = boolean("disable_spotlight")
val startupTab = unique("startup_tab", "ngs_map_icon_container",
val startupTab = unique("startup_tab",
"ngs_map_icon_container",
"ngs_chat_icon_container",
"ngs_camera_icon_container",
"ngs_community_icon_container",
"ngs_spotlight_icon_container",
"ngs_search_icon_container"
)
) { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) }
val storyViewerOverride = unique("story_viewer_override", "DISCOVER_PLAYBACK_SEEKBAR", "VERTICAL_STORY_VIEWER") { addNotices(FeatureNotice.UNSTABLE) }
}

View File

@ -0,0 +1,12 @@
package me.rhunk.snapenhance.core.eventbus.events.impl
import android.view.View
import android.view.ViewGroup
import me.rhunk.snapenhance.core.eventbus.events.AbstractHookEvent
class AddViewEvent(
val parent: ViewGroup,
var view: View,
var index: Int,
var layoutParams: ViewGroup.LayoutParams
) : AbstractHookEvent()

View File

@ -0,0 +1,9 @@
package me.rhunk.snapenhance.core.eventbus.events.impl
import me.rhunk.snapenhance.core.eventbus.events.AbstractHookEvent
class NetworkApiRequestEvent(
val request: Any,
val callback: Any,
var url: String,
) : AbstractHookEvent()

View File

@ -21,17 +21,15 @@ enum class FileType(
UNKNOWN("dat", "application/octet-stream", false, false, false);
companion object {
private val fileSignatures = HashMap<String, FileType>()
init {
fileSignatures["52494646"] = WEBP
fileSignatures["504b0304"] = ZIP
fileSignatures["89504e47"] = PNG
fileSignatures["00000020"] = MP4
fileSignatures["00000018"] = MP4
fileSignatures["0000001c"] = MP4
fileSignatures["ffd8ffe0"] = JPG
}
private val fileSignatures = mapOf(
"52494646" to WEBP,
"504b0304" to ZIP,
"89504e47" to PNG,
"00000020" to MP4,
"00000018" to MP4,
"0000001c" to MP4,
"ffd8ff" to JPG,
)
fun fromString(string: String?): FileType {
return values().firstOrNull { it.fileExtension.equals(string, ignoreCase = true) } ?: UNKNOWN

View File

@ -8,7 +8,8 @@ enum class MediaFilter(
PENDING("pending", true),
CHAT_MEDIA("chat_media"),
STORY("story"),
SPOTLIGHT("spotlight");
SPOTLIGHT("spotlight"),
PROFILE_PICTURE("profile_picture");
fun matches(source: String?): Boolean {
if (source == null) return false

View File

@ -165,7 +165,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
Uri.parse(path).let { uri ->
if (uri.scheme == "file") {
return@let suspendCoroutine<String> { continuation ->
context.downloadServer.ensureServerStarted {
context.httpServer.ensureServerStarted {
val file = Paths.get(uri.path).toFile()
val url = putDownloadableContent(file.inputStream(), file.length())
continuation.resumeWith(Result.success(url))
@ -532,6 +532,18 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
}
}
fun downloadProfilePicture(url: String, author: String) {
provideDownloadManagerClient(
pathSuffix = "Profile Pictures",
mediaIdentifier = url.hashCode().toString(16).replaceFirst("-", ""),
mediaDisplaySource = author,
mediaDisplayType = MediaFilter.PROFILE_PICTURE.key
).downloadSingleMedia(
url,
DownloadMediaType.REMOTE_MEDIA
)
}
/**
* Called when a message is focused in chat
*/

View File

@ -0,0 +1,77 @@
package me.rhunk.snapenhance.features.impl.downloader
import android.annotation.SuppressLint
import android.widget.Button
import android.widget.RelativeLayout
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.util.protobuf.ProtoReader
import java.nio.ByteBuffer
class ProfilePictureDownloader : Feature("ProfilePictureDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
@SuppressLint("SetTextI18n")
override fun asyncOnActivityCreate() {
var friendUsername: String? = null
var backgroundUrl: String? = null
var avatarUrl: String? = null
context.event.subscribe(AddViewEvent::class) { event ->
if (event.view::class.java.name != "com.snap.unifiedpublicprofile.UnifiedPublicProfileView") return@subscribe
event.parent.addView(Button(event.parent.context).apply {
text = "Download"
layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 200, 0, 0)
}
setOnClickListener {
ViewAppearanceHelper.newAlertDialogBuilder(
this@ProfilePictureDownloader.context.mainActivity!!
).apply {
setTitle("Download profile picture")
val choices = mutableMapOf<String, String>()
backgroundUrl?.let { choices["Background"] = it }
avatarUrl?.let { choices["Avatar"] = it }
setItems(choices.keys.toTypedArray()) { _, which ->
runCatching {
this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture(
choices.values.elementAt(which),
friendUsername!!
)
}.onFailure {
Logger.error("Failed to download profile picture", it)
}
}
}.show()
}
})
}
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
if (!event.url.endsWith("/rpc/getPublicProfile")) return@subscribe
Hooker.ephemeralHookObjectMethod(event.callback::class.java, event.callback, "onSucceeded", HookStage.BEFORE) { methodParams ->
val content = methodParams.arg<ByteBuffer>(2).run {
ByteArray(capacity()).also {
get(it)
position(0)
}
}
ProtoReader(content).readPath(1, 1, 2) {
friendUsername = getString(2) ?: return@readPath
readPath(4) {
backgroundUrl = getString(2)
avatarUrl = getString(100)
}
}
}
}
}
}

View File

@ -1,6 +1,6 @@
package me.rhunk.snapenhance.features.impl.privacy
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
@ -20,20 +20,10 @@ class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.
}
}
Hooker.hook(context.classCache.networkApi, "submit", HookStage.BEFORE,
{ disableMetrics }) { param ->
val httpRequest: Any = param.arg(0)
val url = XposedHelpers.getObjectField(httpRequest, "mUrl").toString()
/*if (url.contains("resolve?co=")) {
val index = url.indexOf("co=")
val end = url.lastIndexOf("&")
val co = url.substring(index + 3, end)
val decoded = Base64.getDecoder().decode(co.toByteArray(StandardCharsets.UTF_8))
debug("decoded : " + decoded.toString(Charsets.UTF_8))
debug("content: $co")
}*/
context.event.subscribe(NetworkApiRequestEvent::class, { disableMetrics }) { param ->
val url = param.url
if (url.contains("app-analytics") || url.endsWith("v1/metrics")) {
param.setResult(null)
param.canceled = true
}
}
}

View File

@ -1,20 +1,27 @@
package me.rhunk.snapenhance.features.impl.spying
import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.util.ktx.getObjectField
import me.rhunk.snapenhance.util.ktx.setObjectField
import me.rhunk.snapenhance.util.download.HttpServer
import kotlin.coroutines.suspendCoroutine
class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() {
val anonymousStoryViewProperty by context.config.messaging.anonymousStoryViewing
Hooker.hook(context.classCache.networkApi,"submit", HookStage.BEFORE, { anonymousStoryViewProperty }) {
val httpRequest: Any = it.arg(0)
val url = httpRequest.getObjectField("mUrl") as String
if (url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) {
httpRequest.setObjectField("mUrl", "http://127.0.0.1")
val httpServer = HttpServer()
context.event.subscribe(NetworkApiRequestEvent::class, { anonymousStoryViewProperty }) { event ->
if (!event.url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) return@subscribe
runBlocking {
suspendCoroutine {
httpServer.ensureServerStarted {
event.url = "http://127.0.0.1:${httpServer.port}"
it.resumeWith(Result.success(Unit))
}
}
}
}
}

View File

@ -3,9 +3,9 @@ package me.rhunk.snapenhance.features.impl.ui
import android.annotation.SuppressLint
import android.os.Handler
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
@ -38,21 +38,17 @@ class StartupPageOverride : Feature("StartupPageOverride", loadParams = FeatureL
}
val ngsIconId = context.androidContext.resources.getIdentifier(ngsIconName, "id", Constants.SNAPCHAT_PACKAGE_NAME)
val unhooks = mutableListOf<() -> Unit>()
ViewGroup::class.java.getMethod(
"addView",
View::class.java,
Int::class.javaPrimitiveType,
ViewGroup.LayoutParams::class.java
).hook(HookStage.AFTER) { param ->
if (param.thisObject<ViewGroup>() !is LinearLayout) return@hook
with(param.arg<View>(0)) {
lateinit var unhook: () -> Unit
context.event.subscribe(AddViewEvent::class) { event ->
if (event.parent !is LinearLayout) return@subscribe
with(event.view) {
if (id == ngsIconId) {
ngsIcon = this
unhooks.forEach { it() }
unhook()
}
}
}.also { unhooks.add(it::unhook) }
}.also { unhook = it }
}
}

View File

@ -8,9 +8,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookAdapter
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.hook.hook
@ -25,12 +25,12 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE
}
}
private fun hideStorySection(param: HookAdapter) {
val parent = param.thisObject() as ViewGroup
private fun hideStorySection(event: AddViewEvent) {
val parent = event.parent
parent.visibility = View.GONE
val marginLayoutParams = parent.layoutParams as ViewGroup.MarginLayoutParams
marginLayoutParams.setMargins(-99999, -99999, -99999, -99999)
param.setResult(null)
event.canceled = true
}
@SuppressLint("DiscouragedApi", "InternalInsetResource")
@ -69,33 +69,29 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE
}
}
ViewGroup::class.java.getMethod(
"addView",
View::class.java,
Int::class.javaPrimitiveType,
ViewGroup.LayoutParams::class.java
).hook(HookStage.BEFORE) { param ->
val view: View = param.arg(0)
val viewId = view.id
context.event.subscribe(AddViewEvent::class) { event ->
val viewId = event.view.id
val view = event.view
if (hideStorySections.contains("hide_for_you")) {
if (viewId == getIdentifier("df_large_story", "id") ||
viewId == getIdentifier("df_promoted_story", "id")) {
hideStorySection(param)
return@hook
hideStorySection(event)
return@subscribe
}
if (viewId == getIdentifier("stories_load_progress_layout", "id")) {
param.setResult(null)
event.canceled = true
}
}
if (hideStorySections.contains("hide_friends") && viewId == getIdentifier("friend_card_frame", "id")) {
hideStorySection(param)
hideStorySection(event)
}
//mappings?
if (hideStorySections.contains("hide_friend_suggestions") && view.javaClass.superclass?.name?.endsWith("StackDrawLayout") == true) {
val layoutParams = view.layoutParams as? FrameLayout.LayoutParams ?: return@hook
val layoutParams = view.layoutParams as? FrameLayout.LayoutParams ?: return@subscribe
if (layoutParams.width == -1 &&
layoutParams.height == -2 &&
view.javaClass.let { clazz ->
@ -103,17 +99,17 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE
clazz.constructors.any { it.parameterCount == 1 && it.parameterTypes[0] == Context::class.java }
}
) {
hideStorySection(param)
hideStorySection(event)
}
}
if (hideStorySections.contains("hide_following") && (viewId == getIdentifier("df_small_story", "id"))
) {
hideStorySection(param)
hideStorySection(event)
}
if (blockAds && viewId == getIdentifier("df_promoted_story", "id")) {
hideStorySection(param)
hideStorySection(event)
}
if (isImmersiveCamera) {
@ -145,15 +141,15 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE
view.visibility = View.GONE
}
if (getIdentifier("chat_input_bar_sharing_drawer_button", "id") == viewId && hiddenElements.contains("hide_live_location_share_button")) {
param.setResult(null)
event.canceled = true
}
if (viewId == callButton1 || viewId == callButton2) {
if (!hiddenElements.contains("hide_call_buttons")) return@hook
if (view.visibility == View.GONE) return@hook
if (!hiddenElements.contains("hide_call_buttons")) return@subscribe
if (view.visibility == View.GONE) return@subscribe
}
if (viewId == callButtonsStub) {
if (!hiddenElements.contains("hide_call_buttons")) return@hook
param.setResult(null)
if (!hiddenElements.contains("hide_call_buttons")) return@subscribe
event.canceled = true
}
}
}

View File

@ -111,8 +111,8 @@ object Hooker {
val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet()
hook(clazz, methodName, stage) { param->
if (param.nullableThisObject<Any>() != instance) return@hook
unhooks.forEach { it.unhook() }
hookConsumer(param)
unhooks.forEach{ it.unhook() }
}.also { unhooks.addAll(it) }
}
}

View File

@ -8,6 +8,7 @@ import me.rhunk.snapenhance.features.impl.AutoUpdater
import me.rhunk.snapenhance.features.impl.ConfigurationOverride
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.downloader.ProfilePictureDownloader
import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode
import me.rhunk.snapenhance.features.impl.experiments.AppPasscode
import me.rhunk.snapenhance.features.impl.experiments.DeviceSpooferHook
@ -91,6 +92,7 @@ class FeatureManager(private val context: ModContext) : Manager {
register(StartupPageOverride::class)
register(GooglePlayServicesDialogs::class)
register(NoFriendScoreDelay::class)
register(ProfilePictureDownloader::class)
initializeFeatures()
}

View File

@ -7,11 +7,10 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import java.lang.reflect.Modifier
@SuppressLint("DiscouragedApi")
@ -42,17 +41,9 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
val componentsHolder = context.resources.getIdentifier("components_holder", "id", Constants.SNAPCHAT_PACKAGE_NAME)
val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id", Constants.SNAPCHAT_PACKAGE_NAME)
val addViewMethod = ViewGroup::class.java.getMethod(
"addView",
View::class.java,
Int::class.javaPrimitiveType,
ViewGroup.LayoutParams::class.java
)
Hooker.hook(addViewMethod, HookStage.BEFORE) { param ->
val viewGroup: ViewGroup = param.thisObject()
context.event.subscribe(AddViewEvent::class) { event ->
val originalAddView: (View) -> Unit = {
param.invokeOriginal(arrayOf(it, -1,
event.adapter.invokeOriginal(arrayOf(it, -1,
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
@ -60,19 +51,20 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
)
}
val childView: View = param.arg(0)
operaContextActionMenu.inject(viewGroup, childView)
val viewGroup: ViewGroup = event.parent
val childView: View = event.view
operaContextActionMenu.inject(event.parent, childView)
if (viewGroup.id == componentsHolder && childView.id == feedNewChat) {
settingsGearInjector.inject(viewGroup, childView)
return@hook
if (event.parent.id == componentsHolder && childView.id == feedNewChat) {
settingsGearInjector.inject(event.parent, childView)
return@subscribe
}
//download in chat snaps and notes from the chat action menu
if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer")) {
if (viewGroup.parent == null || viewGroup.parent.parent == null) return@hook
if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe
chatActionMenu.inject(viewGroup)
return@hook
return@subscribe
}
//TODO: inject in group chat menus
@ -101,7 +93,7 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
viewList.reversed().forEach { injectedLayout.addView(it, 0) }
}
param.setArg(0, injectedLayout)
event.view = injectedLayout
}
if (viewGroup is LinearLayout && viewGroup.id == actionSheetItemsContainerLayoutId) {
@ -125,12 +117,12 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
//context.config.writeConfig()
}
})
return@hook
return@subscribe
}
if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@hook
if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@subscribe
//filter by the slot index
if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@hook
if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@subscribe
friendFeedInfoMenu.inject(viewGroup, originalAddView)
}
}

View File

@ -18,10 +18,10 @@ import java.util.StringTokenizer
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random
class DownloadServer(
class HttpServer(
private val timeout: Int = 10000
) {
private val port = Random.nextInt(10000, 65535)
val port = Random.nextInt(10000, 65535)
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private var timeoutJob: Job? = null
@ -30,16 +30,16 @@ class DownloadServer(
private val cachedData = ConcurrentHashMap<String, Pair<InputStream, Long>>()
private var serverSocket: ServerSocket? = null
fun ensureServerStarted(callback: DownloadServer.() -> Unit) {
fun ensureServerStarted(callback: HttpServer.() -> Unit) {
if (serverSocket != null && !serverSocket!!.isClosed) {
callback(this)
return
}
coroutineScope.launch(Dispatchers.IO) {
Logger.debug("starting download server on port $port")
Logger.debug("starting http server on port $port")
serverSocket = ServerSocket(port)
callback(this@DownloadServer)
callback(this@HttpServer)
while (!serverSocket!!.isClosed) {
try {
val socket = serverSocket!!.accept()
@ -48,7 +48,7 @@ class DownloadServer(
handleRequest(socket)
timeoutJob = launch {
delay(timeout.toLong())
Logger.debug("download server closed due to timeout")
Logger.debug("http server closed due to timeout")
runCatching {
socketJob?.cancel()
socket.close()
@ -59,7 +59,7 @@ class DownloadServer(
}
}
} catch (e: SocketException) {
Logger.debug("download server timed out")
Logger.debug("http server timed out")
break;
} catch (e: Throwable) {
Logger.error("failed to handle request", e)
@ -96,6 +96,8 @@ class DownloadServer(
val parse = StringTokenizer(line)
val method = parse.nextToken().uppercase(Locale.getDefault())
var fileRequested = parse.nextToken().lowercase(Locale.getDefault())
Logger.debug("[http-server:${port}] $method $fileRequested")
if (method != "GET") {
with(writer) {
println("HTTP/1.1 501 Not Implemented")