summaryrefslogtreecommitdiff
path: root/adb-proxy/src/com/google/services/firebase/directaccess/client/device/remote/service/adb/forwardingdaemon/ReverseService.kt
blob: f91fff83feb270e6fc61bdfce97f504dd2eedbf6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * 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.google.services.firebase.directaccess.client.device.remote.service.adb.forwardingdaemon

import com.android.adblib.AdbOutputChannel
import com.android.adblib.AdbSession
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

/**
 * A local implementation of the "reverse:" service on an Android device.
 *
 * The "reverse:" service on an Android device allows the user to create port forwards from the
 * device to the host. However, its current implementation works by sending OPEN requests from the
 * device to the host, which means that any reverse forwards opened from the device will land on the
 * **first** host that exists. For the forwarding daemon, this means that it would land on the lab
 * machine, which is both a feature gap and a security issue.
 *
 * The ReverseService should know about all instances of [ReverseForwardStream] which are currently
 * active. This allows for the implementation of `adb reverse --list`, `adb reverse --remove-all`,
 * and `adb reverse --remove <remote>`.
 */
internal class ReverseService(
  private val deviceId: String,
  private val scope: CoroutineScope,
  outputChannel: AdbOutputChannel,
  private val adbSession: AdbSession
) {
  private val openReverses = ConcurrentHashMap<String, ReverseForwardStream>()
  private val openReversesLock = Mutex()
  private val responseWriter = ResponseWriter(outputChannel)

  /** Handle a command to the "reverse:" service. */
  suspend fun handleReverse(command: String, streamId: Int) {
    val subcommand = command.substringAfter("reverse:").substringBefore('\u0000')
    logger.info("Handling '$command'")
    when (subcommand.substringBefore(":")) {
      "forward" -> handleForward(subcommand, streamId)
      "killforward" -> handleKillForward(streamId, subcommand.substringAfter("killforward:"))
      "killforward-all" -> handleKillForwardAll(streamId)
      "list-forward" -> handleListForward(streamId)
      else -> responseWriter.writeFailResponse(streamId, command)
    }
  }

  private suspend fun handleForward(subcommand: String, streamId: Int): Stream? {
    val arguments = subcommand.substringAfter("forward:").substringAfter("norebind:").split(';')
    // TODO: validate arguments
    val devicePort = arguments[0]
    val localPort = arguments[1]
    openReversesLock.withLock {
      val existingStream = openReverses[devicePort]
      if (existingStream != null) {
        if (subcommand.substringAfter("forward:").substringBefore(":") == "norebind") {
          responseWriter.writeFailResponse(streamId, "'$devicePort' already bound")
          return null
        }
        existingStream.rebind(localPort)
        responseWriter.writeOkayResponse(streamId)
        return null
      }
      val stream =
        ReverseForwardStream(
          devicePort,
          localPort,
          streamId,
          deviceId,
          adbSession,
          responseWriter,
          scope,
        )
      openReverses[devicePort] = stream
      scope.launch {
        // TODO(247652380): error handling
        stream.run()
      }
      return stream
    }
  }

  private suspend fun handleKillForward(streamId: Int, devicePort: String) {
    killForward(devicePort)
    responseWriter.writeOkayResponse(streamId)
  }

  private suspend fun handleKillForwardAll(streamId: Int) {
    killAll()
    responseWriter.writeOkayResponse(streamId)
  }

  private suspend fun handleListForward(streamId: Int) {
    // Build the list of all forward connections
    val stringBuilder = StringBuilder()
    openReversesLock.withLock {
      for (value in openReverses.elements()) {
        // Format from
        // https://cs.android.com/android/platform/superproject/+/3a52886262ae22477a7d8ffb12adba64daf6aafa:packages/modules/adb/adb_listeners.cpp;l=136;drc=6951984bbefb96423970b82005ae381065e36704
        stringBuilder.append("(reverse) ${value.devicePort} ${value.localPort}\n")
      }
    }
    responseWriter.writeOkayResponse(streamId, stringBuilder.toString())
  }

  /**
   * Kill all open reverse forward connections.
   *
   * Note that any currently open sockets will remain open. However, the server sockets on the
   * device will be closed, so no new connections will be made.
   */
  suspend fun killAll() {
    openReversesLock.withLock { openReverses.forEach { (key, _) -> killForwardUnsafe(key) } }
  }

  private suspend fun killForward(key: String) {
    logger.info("Handling killforward for $key")
    openReversesLock.withLock { killForwardUnsafe(key) }
  }

  /**
   * This function is unsafe to call directly. Instead, call to this function should be wrapped with
   * a lock on [openReversesLock].
   *
   * For example, see [killForward], [killAll]
   */
  private suspend fun killForwardUnsafe(key: String) {
    assert(openReversesLock.isLocked)
    openReverses.remove(key)?.kill()
  }

  companion object {
    private val logger = Logger.getLogger(ReverseService::class.qualifiedName)
  }
}