Skip to content

Commit

Permalink
SSHTunneledBaseClient parses HTTP headers
Browse files Browse the repository at this point in the history
  • Loading branch information
GreenAppers committed Nov 24, 2019
1 parent 6749af4 commit 428acf0
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 33 deletions.
71 changes: 48 additions & 23 deletions lib/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class SSHClient extends SSHTransport with SSHAgentForwarding {
FingerprintCallback acceptHostFingerprint;
Uint8ListFunction getPassword;
IdentityFunction loadIdentity;
VoidCallback success;
List<VoidCallback> success = <VoidCallback>[];

// State
int loginPrompts = 0, passwordPrompts = 0, userauthFail = 0;
Expand All @@ -55,7 +55,7 @@ class SSHClient extends SSHTransport with SSHAgentForwarding {
StringCallback print,
StringCallback debugPrint,
StringCallback tracePrint,
this.success,
VoidCallback success,
this.acceptHostFingerprint,
this.loadIdentity,
this.getPassword,
Expand All @@ -75,6 +75,9 @@ class SSHClient extends SSHTransport with SSHAgentForwarding {
socket: socketInput,
random: random,
secureRandom: secureRandom) {
if (success != null) {
this.success.add(success);
}
if (socket == null) {
if (debugPrint != null) {
debugPrint('Connecting to $hostport');
Expand Down Expand Up @@ -360,7 +363,9 @@ class SSHClient extends SSHTransport with SSHAgentForwarding {
}
writeCipher(MSG_CHANNEL_OPEN.create(
'session', sessionChannel.localId, initialWindowSize, maxPacketSize));
if (success != null) success();
for (VoidCallback successCallback in success) {
successCallback();
}
}

void handleMSG_USERAUTH_INFO_REQUEST(MSG_USERAUTH_INFO_REQUEST msg) {
Expand Down Expand Up @@ -589,15 +594,19 @@ class SSHClient extends SSHTransport with SSHAgentForwarding {

/// Implement same [SocketInterface] as actual [Socket] but over [SSHClient] tunnel.
class SSHTunneledSocketImpl extends SocketInterface {
bool clientOwner;
SSHClient client;
Identity identity;
Channel channel;
String tunnelToHost;
int tunnelToPort;
Function connected, connectError, onError, onDone, onMessage;

SSHTunneledSocketImpl.fromClient(this.client) : clientOwner = false;

SSHTunneledSocketImpl(Uri url, String login, String key, String password,
{StringCallback print, StringCallback debugPrint}) {
{StringCallback print, StringCallback debugPrint})
: clientOwner = true {
identity = key == null ? null : parsePem(key);
client = SSHClient(
socketInput: SocketImpl(),
Expand All @@ -610,20 +619,11 @@ class SSHTunneledSocketImpl extends SocketInterface {
if (onDone != null) onDone(null);
},
startShell: false,
success: () {
channel = client.openTcpChannel('127.0.0.1', 1234, tunnelToHost,
tunnelToPort, (_, Uint8List m) => onMessage(m), () {
if (connected != null) connected(client.socket);
connected = connectError = null;
});
},
success: openTunnel,
print: print,
debugPrint: debugPrint);
}

@override
void close() => client.disconnect('close');

@override
void handleError(Function errorHandler) => onError = errorHandler;

Expand All @@ -633,22 +633,47 @@ class SSHTunneledSocketImpl extends SocketInterface {
@override
void listen(Function messageHandler) => onMessage = messageHandler;

@override
void send(String text) => sendRaw(Uint8List.fromList(text.codeUnits));

@override
void sendRaw(Uint8List raw) => client.sendToChannel(channel, raw);

@override
void close() {
if (clientOwner) {
client.disconnect('close');
} else if (channel != null) {
client.closeChannel(channel);
}
}

@override
void connect(Uri address, Function connectHandler, Function errorHandler,
{int timeoutSeconds = 15, bool ignoreBadCert = false}) {
tunnelToHost = address.host;
tunnelToPort = address.port;
connected = connectHandler;
connectError = errorHandler;
client.socket.connect(client.hostport, client.onConnected, (error) {
client.disconnect('connect error');
if (connectError != null) connectError(error);
});
if (clientOwner) {
client.socket.connect(client.hostport, client.onConnected, (error) {
client.disconnect('connect error');
if (connectError != null) connectError(error);
});
} else {
if (client.sessionChannel == null) {
client.success.add(openTunnel);
} else {
openTunnel();
}
}
}

@override
void send(String text) => sendRaw(Uint8List.fromList(text.codeUnits));

@override
void sendRaw(Uint8List raw) => client.sendToChannel(channel, raw);
void openTunnel() {
channel = client.openTcpChannel('127.0.0.1', 1234, tunnelToHost,
tunnelToPort, (_, Uint8List m) => onMessage(m), () {
if (connected != null) connected();
connected = connectError = null;
});
}
}
91 changes: 82 additions & 9 deletions lib/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:typed_data';

import 'package:http/http.dart' as http;

import 'package:dartssh/client.dart';
import 'package:dartssh/serializable.dart';
import 'package:dartssh/transport.dart';

typedef HttpClientFactory = http.Client Function();

/// Asynchronous HTTP request
class HttpRequest {
String url, method, data;
Expand Down Expand Up @@ -46,15 +51,20 @@ class TestHttpClient extends HttpClient {

/// package:http based implementation of [HttpClient].
class HttpClientImpl extends HttpClient {
HttpClientImpl({StringCallback debugPrint, StringFilter userAgent})
: super(debugPrint);
HttpClientFactory clientFactory;
HttpClientImpl(
{this.clientFactory, StringCallback debugPrint, StringFilter userAgent})
: super(debugPrint) {
clientFactory ??= () => UserAgentBaseClient(
userAgent == null ? null : userAgent('HttpClientImpl'), http.Client());
}

@override
Future<HttpResponse> request(String url, {String method, String data}) async {
numOutstanding++;
if (debugPrint != null) debugPrint('HTTP Request: $url');

http.Client client = http.Client();
http.Client client = clientFactory();
var uriResponse;
switch (method) {
case 'POST':
Expand Down Expand Up @@ -88,14 +98,77 @@ class HttpClientImpl extends HttpClient {
}
}

class UserAgentClient extends http.BaseClient {
class UserAgentBaseClient extends http.BaseClient {
final String userAgent;
final http.Client _inner;

UserAgentClient(this.userAgent, this._inner);
final http.Client inner;
UserAgentBaseClient(this.userAgent, this.inner);

Future<http.StreamedResponse> send(http.BaseRequest request) {
request.headers['user-agent'] = userAgent;
return _inner.send(request);
if (userAgent != null) {
request.headers['user-agent'] = userAgent;
}
return inner.send(request);
}
}

class SSHTunneledBaseClient extends http.BaseClient {
final String userAgent;
final SSHClient client;
SSHTunneledBaseClient(this.client, {this.userAgent});

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
if (userAgent != null) {
request.headers['user-agent'] = userAgent;
}

Completer<String> connectCompleter = Completer<String>();
SSHTunneledSocketImpl socket = SSHTunneledSocketImpl.fromClient(client);
socket.connect(request.url, () => connectCompleter.complete(null),
(error) => connectCompleter.complete('$error'));
String connectError = await connectCompleter.future;
if (connectError != null) throw FormatException(connectError);

String headers;
Completer<String> readHeadersCompleter = Completer<String>();
QueueBuffer buffer = QueueBuffer(Uint8List(0));
socket.handleDone(() => readHeadersCompleter.complete('done'));
socket.handleError((error) => readHeadersCompleter.complete('$error'));
socket.listen((Uint8List m) {
buffer.add(m);
if (headers == null) {
int headersEnd = searchUint8List(
buffer.data, Uint8List.fromList('\r\n\r\n'.codeUnits));
if (headersEnd != -1) {
headers = utf8.decode(viewUint8List(buffer.data, 0, headersEnd));
buffer.flush(headersEnd + 4);
readHeadersCompleter.complete(null);
}
}
});

Uint8List body;
if (request.method == 'POST') {
body = await request.finalize().toBytes();
request.headers['Content-Length'] = '${body.length}';
}
socket.send('${request.method} /${request.url.path} HTTP/1.1\r\n' +
request.headers.entries
.map((header) => '${header.key}: ${header.value}')
.join('\r\n') +
'\r\n\r\n');
if (request.method == 'POST') socket.sendRaw(body);

await readHeadersCompleter.future;
print('got headers $headers');

return http.StreamedResponse(
Stream.empty(),
200,
contentLength: 0,
request: request,
headers: {'Content-Length': '0'},
reasonPhrase: 'OK',
);
}
}
26 changes: 26 additions & 0 deletions lib/http_html.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2019 dartssh developers
// Use of this source code is governed by a MIT-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:html' as html;

import 'package:dartssh/http.dart';
import 'package:dartssh/transport.dart';

/// dart:html based alternative [HttpClient] implementation.
class HttpClientImpl extends HttpClient {
static const String type = 'html';
HttpClientImpl({StringCallback debugPrint, StringFilter userAgent})
: super(debugPrint);

@override
Future<HttpResponse> request(String url, {String method, String data}) {
numOutstanding++;
Completer<HttpResponse> completer = Completer<HttpResponse>();
html.HttpRequest.request(url, method: method).then((r) {
numOutstanding--;
completer.complete(HttpResponse(r.status, r.responseText));
});
return completer.future;
}
}
54 changes: 54 additions & 0 deletions lib/http_io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2019 dartssh developers
// Use of this source code is governed by a MIT-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;

import 'package:dartssh/http.dart';
import 'package:dartssh/transport.dart';

/// dart:io based alternative [HttpClient] implementation.
class HttpClientImpl extends HttpClient {
static const String type = 'io';
io.HttpClient client = io.HttpClient();

HttpClientImpl({StringCallback debugPrint, StringFilter userAgent})
: super(debugPrint) {
if (userAgent != null) client.userAgent = userAgent(client.userAgent);
}

@override
Future<HttpResponse> request(String url, {String method, String data}) async {
numOutstanding++;

Uri uri = Uri.parse(url);
var request;
switch (method) {
case 'POST':
request = await client.postUrl(uri);
break;

default:
request = await client.getUrl(uri);
break;
}

if (debugPrint != null) debugPrint('HTTP Request: ${request.uri}');
var response = await request.close();
HttpResponse ret = HttpResponse(response.statusCode);
await for (var contents in response.transform(Utf8Decoder())) {
if (ret.text == null) {
ret.text = contents;
} else {
ret.text += contents;
}
}

if (debugPrint != null) {
debugPrint('HTTP Response=${ret.status}: ${ret.text}');
}
numOutstanding--;
return ret;
}
}
13 changes: 13 additions & 0 deletions lib/serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ Uint8List appendUint8List(Uint8List x, Uint8List y) =>
Uint8List viewUint8List(Uint8List x, [int offset = 0, int length]) =>
Uint8List.view(x.buffer, x.offsetInBytes + offset, length ?? x.length);

/// Returns the position of the first match of [needle] in [haystack] or -1.
int searchUint8List(Uint8List haystack, Uint8List needle) {
if (needle.isEmpty) return -1;
for (int i = 0; i < haystack.length - needle.length + 1; i++) {
int j = 0;
while (j < needle.length && haystack[i + j] == needle[j]) {
j++;
}
if (j == needle.length) return i;
}
return -1;
}

/// Returns true if [x] and [y] are equivalent.
bool equalUint8List(Uint8List x, Uint8List y) {
if (x.length != y.length) return false;
Expand Down
2 changes: 1 addition & 1 deletion lib/transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ abstract class SSHTransport with SSHDiffieHellman {
/// When the connection has been established, both sides MUST send an identification string.
/// https://tools.ietf.org/html/rfc4253#section-4.2
void handleConnected() {
if (debugPrint != null) debugPrint('handleConnected');
if (debugPrint != null) debugPrint('SSHTransport.handleConnected');
if (state != SSHTransportState.INIT) throw FormatException('$state');
socket.send(verC + '\r\n');
if (client) sendKeyExchangeInit(false);
Expand Down
1 change: 1 addition & 0 deletions lib/websocket_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:html' as html;
import 'dart:typed_data';

Expand Down

0 comments on commit 428acf0

Please sign in to comment.