-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathSSHTunnel.h
More file actions
226 lines (197 loc) · 7.71 KB
/
SSHTunnel.h
File metadata and controls
226 lines (197 loc) · 7.71 KB
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
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import Foundation
import NIOCore
import NIOConcurrencyHelpers
import NIOPosix
import NIOSSH
import KeychainAccess
import mft
// Custom error type for SSHTunnelManager
enum SSHTunnelError: Error {
case missingCredentials
case connectionFailed(Error)
case portForwardingFailed(Error)
case localProxyFailed(Error)
case reconnectFailed(Error)
case invalidServerConfiguration
case tunnelAlreadyConnected
case tunnelNotConnected
}
class SSHTunnelManager {
private let group: MultiThreadedEventLoopGroup
private var channel: Channel?
private var localPort: Int
private var remoteHost: String
private var remotePort: Int
private var server: ServerEntity
private var isConnected: Bool = false
private var reconnectAttempts: Int = 0
private let maxReconnectAttempts: Int = 5
private let reconnectDelay: TimeAmount = .seconds(5)
private let connectionCheckInterval: TimeAmount = .seconds(10)
private var connectionCheckTask: RepeatedTask?
init(server: ServerEntity, localPort: Int, remoteHost: String, remotePort: Int) throws {
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.server = server
self.localPort = localPort
self.remoteHost = remoteHost
self.remotePort = remotePort
guard let _ = server.sftpHost,
let _ = server.sftpPort,
let _ = server.sftpUser else {
throw SSHTunnelError.invalidServerConfiguration
}
}
deinit {
stop()
try? group.syncShutdownGracefully()
}
func start() throws {
guard !isConnected else {
throw SSHTunnelError.tunnelAlreadyConnected
}
try connect()
startConnectionCheck()
}
private func connect() throws {
let promise = group.next().makePromise(of: Channel.self)
guard let password = Keychain(service: "srgim.throttle2", accessGroup: "group.com.srgim.Throttle-2")["sftpPassword" + (server.name ?? "")] else {
throw SSHTunnelError.missingCredentials
}
let clientConfiguration = SSHClientConfiguration(
userAuthDelegate: PasswordAuthenticationDelegate(username: server.sftpUser!, password: password),
hostKeyValidator: AcceptAnyHostKeyValidator()
)
let bootstrap = ClientBootstrap(group: group)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.channelInitializer { channel in
channel.pipeline.addHandlers([
NIOSSHHandler(
role: .client(clientConfiguration),
allocator: channel.allocator,
inboundChildChannelInitializer: { channel in
channel.pipeline.addHandler(HTTPProxyHandler(remoteHost: self.remoteHost, remotePort: self.remotePort))
}
)
])
}
bootstrap.connect(host: server.sftpHost!, port: Int(server.sftpPort)).cascade(to: promise)
do {
channel = try promise.futureResult.wait()
isConnected = true
reconnectAttempts = 0
print("SSH Tunnel connected to \(server.sftpHost!):\(server.sftpPort)")
// Start listening on the local port
startLocalProxy()
} catch {
throw SSHTunnelError.connectionFailed(error)
}
}
private func startLocalProxy() {
let localBootstrap = ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelInitializer { channel in
channel.pipeline.addHandler(LocalHTTPProxyHandler(tunnelChannel: self.channel!))
}
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
.childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
do {
let localChannel = try localBootstrap.bind(host: "127.0.0.1", port: localPort).wait()
print("Local HTTP proxy listening on port \(localPort)")
// Keep the local proxy running
try localChannel.closeFuture.wait()
} catch {
print("Error starting local proxy: \(error)")
}
}
func stop() {
isConnected = false
connectionCheckTask?.cancel()
connectionCheckTask = nil
channel?.close(mode: .all, promise: nil)
channel = nil
print("SSH Tunnel stopped")
}
private func startConnectionCheck() {
connectionCheckTask = group.next().scheduleRepeatedTask(initialDelay: connectionCheckInterval, delay: connectionCheckInterval) { _ in
self.checkConnection()
}
}
private func checkConnection() {
guard isConnected else {
print("Connection is down, attempting to reconnect...")
reconnect()
return
}
// Add more robust connection checking here if needed
if let channel = channel, !channel.isActive {
print("Connection is down, attempting to reconnect...")
reconnect()
}
}
private func reconnect() {
guard reconnectAttempts < maxReconnectAttempts else {
print("Max reconnect attempts reached. Giving up.")
stop()
return
}
reconnectAttempts += 1
print("Attempting to reconnect (attempt \(reconnectAttempts) of \(maxReconnectAttempts))...")
group.next().scheduleTask(in: reconnectDelay) {
do {
try self.connect()
} catch {
print("Reconnect failed: \(error)")
self.reconnect() // Try again
}
}
}
}
// Example HTTP Proxy Handler (Conceptual)
class HTTPProxyHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
private let remoteHost: String
private let remotePort: Int
init(remoteHost: String, remotePort: Int) {
self.remoteHost = remoteHost
self.remotePort = remotePort
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
// Forward the HTTP request over the SSH tunnel
var buffer = self.unwrapInboundIn(data)
print("HTTPProxyHandler: Forwarding data to \(remoteHost):\(remotePort)")
// ... (Send the buffer over the SSH tunnel) ...
}
}
// Example Local HTTP Proxy Handler (Conceptual)
class LocalHTTPProxyHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
private let tunnelChannel: Channel
init(tunnelChannel: Channel) {
self.tunnelChannel = tunnelChannel
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
// Receive the HTTP request from the local client
var buffer = self.unwrapInboundIn(data)
print("LocalHTTPProxyHandler: Received data from local client")
// Forward the buffer to the tunnel channel
tunnelChannel.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil)
}
}
// Example Usage
// Replace with your actual server details
//let serverHost = "your_server_ip"
//let serverPort = 22
//let serverUsername = "your_username"
//let serverPassword = "your_password"
//let localPort = 8080
//let remoteHost = "localhost"
//let remotePort = 8080
//
//let tunnelManager = try SSHTunnelManager(localPort: localPort, remoteHost: remoteHost, remotePort: remotePort, serverHost: serverHost, serverPort: serverPort, serverUsername: serverUsername, serverPassword: serverPassword)
//
//try tunnelManager.start()
//
//// Keep the program running
//RunLoop.main.run()