blob: 9ae28454abe3f276698423be7beb47b5c83ad6d9 [file] [log] [blame]
#!mojo mojo:dart_content_handler
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:core';
import 'dart:typed_data';
import 'package:mojo/application.dart';
import 'package:mojo/bindings.dart';
import 'package:mojo/core.dart';
import 'package:mojo/mojo/network_error.mojom.dart';
import 'package:mojo_services/mojo/files/file.mojom.dart' as files;
import 'package:mojo_services/mojo/files/types.mojom.dart' as files;
import 'package:mojo_services/mojo/net_address.mojom.dart';
import 'package:mojo_services/mojo/network_service.mojom.dart';
import 'package:mojo_services/mojo/tcp_bound_socket.mojom.dart';
import 'package:mojo_services/mojo/tcp_connected_socket.mojom.dart';
import 'package:mojo_services/mojo/terminal/terminal_client.mojom.dart';
void ignoreFuture(Future f) {
f.catchError((e) {});
}
NetAddress makeIPv4NetAddress(List<int> addr, int port) {
var rv = new NetAddress();
rv.family = NetAddressFamily.ipv4;
rv.ipv4 = new NetAddressIPv4();
rv.ipv4.addr = new List<int>.from(addr);
rv.ipv4.port = port;
return rv;
}
void fputs(files.File f, String s) {
ignoreFuture(f.write((s + '\n').codeUnits, 0, files.Whence.fromCurrent));
}
// Connects the terminal |File| and the socket.
// TODO(vtl):
// * Error handling: both connection/socket errors and terminal errors.
// * Relatedly, we should listen for _socketSender's peer being closed (also
// _socket, I guess).
// * Handle the socket send pipe being full (currently, we assume it's never
// full).
class Connector {
final Application _application;
files.FileProxy _terminal;
TcpConnectedSocketProxy _socket;
MojoDataPipeProducer _socketSender;
MojoDataPipeConsumer _socketReceiver;
MojoEventSubscription _socketReceiverEventSubscription;
final ByteData _readBuffer;
final ByteData _writeBuffer;
// TODO(vtl): Don't just hard-code buffer sizes.
Connector(this._application, this._terminal)
: _readBuffer = new ByteData(16 * 1024),
_writeBuffer = new ByteData(16 * 1024);
Future connect(NetAddress remote_address) async {
try {
var networkService = new NetworkServiceProxy.unbound();
_application.connectToService('mojo:network_service', networkService);
NetAddress local_address = makeIPv4NetAddress([0, 0, 0, 0], 0);
var boundSocket = new TcpBoundSocketProxy.unbound();
await networkService.createTcpBoundSocket(local_address, boundSocket);
await networkService.close();
var sendDataPipe = new MojoDataPipe();
_socketSender = sendDataPipe.producer;
var receiveDataPipe = new MojoDataPipe();
_socketReceiver = receiveDataPipe.consumer;
_socket = new TcpConnectedSocketProxy.unbound();
await boundSocket.connect(remote_address, sendDataPipe.consumer,
receiveDataPipe.producer, _socket);
await boundSocket.close();
// Set up reading from the terminal.
_startReadingFromTerminal();
// Set up reading from the socket.
_socketReceiverEventSubscription =
new MojoEventSubscription(_socketReceiver.handle);
_socketReceiverEventSubscription.subscribe(_onSocketReceiverEvent);
} catch (e) {
_shutDown();
}
}
void _startReadingFromTerminal() {
// TODO(vtl): Do we have to do something on error?
_terminal
.read(_writeBuffer.lengthInBytes, 0, files.Whence.fromCurrent)
.then(_onReadFromTerminal)
.catchError((e) {
_shutDown();
});
}
void _onReadFromTerminal(files.FileReadResponseParams p) {
if (p.error != files.Error.ok) {
// TODO(vtl): Do terminal errors.
return;
}
// TODO(vtl): Verify that |bytesRead.length| is within the expected range.
for (var i = 0, j = 0; i < p.bytesRead.length; i++, j++) {
// TODO(vtl): Temporary hack: Translate \r to \n, since we don't have
// built-in support for that.
if (p.bytesRead[i] == 13) {
_writeBuffer.setUint8(i, 10);
} else {
_writeBuffer.setUint8(i, p.bytesRead[i]);
}
}
// TODO(vtl): Handle the send data pipe being full (or closed).
_socketSender
.write(new ByteData.view(_writeBuffer.buffer, 0, p.bytesRead.length));
_startReadingFromTerminal();
}
void _onSocketReceiverEvent(int mojoSignals) {
var shouldShutDown = false;
if (MojoHandleSignals.isReadable(mojoSignals)) {
var numBytesRead = _socketReceiver.read(_readBuffer);
if (_socketReceiver.status == MojoResult.kOk) {
assert(numBytesRead > 0);
_terminal
.write(_readBuffer.buffer.asUint8List(0, numBytesRead), 0,
files.Whence.fromCurrent)
.catchError((e) {
_shutDown();
});
_socketReceiverEventSubscription.enableReadEvents();
} else {
shouldShutDown = true;
}
} else if (MojoHandleSignals.isPeerClosed(mojoSignals)) {
shouldShutDown = true;
} else {
String signals = MojoHandleSignals.string(mojoSignals);
throw 'Unexpected handle event: $signals';
}
if (shouldShutDown) {
_shutDown();
}
}
void _shutDown() {
if (_socketReceiverEventSubscription != null) {
ignoreFuture(_socketReceiverEventSubscription.close());
_socketReceiverEventSubscription = null;
}
if (_socketSender != null) {
if (_socketSender.handle.isValid) _socketSender.handle.close();
_socketSender = null;
}
if (_socketReceiver != null) {
if (_socketReceiver.handle.isValid) _socketReceiver.handle.close();
_socketReceiver = null;
}
if (_terminal != null) {
ignoreFuture(_terminal.close());
_terminal = null;
}
}
}
class TerminalClientImpl implements TerminalClient {
TerminalClientStub _stub;
Application _application;
String _resolvedUrl;
TerminalClientImpl(
this._application, this._resolvedUrl, MojoMessagePipeEndpoint endpoint) {
_stub = new TerminalClientStub.fromEndpoint(endpoint, this);
}
@override
void connectToTerminal(files.FileProxy terminal) {
var url = Uri.parse(_resolvedUrl);
NetAddress remote_address;
try {
remote_address = _getNetAddressFromUrl(url);
} catch (e) {
fputs(
terminal,
'HALP: Add a query: ?host=<host>&port=<port>\n'
'(<host> must be "localhost" or n1.n2.n3.n4)\n\n'
'Got query parameters:\n' +
url.queryParameters.toString());
ignoreFuture(terminal.close());
return;
}
// TODO(vtl): Currently, we only do IPv4, so this should work.
fputs(
terminal,
'Connecting to: ' +
remote_address.ipv4.addr.join('.') +
':' +
remote_address.ipv4.port.toString() +
'...');
var connector = new Connector(_application, terminal);
// TODO(vtl): Do we have to do something on error?
connector.connect(remote_address).catchError((e) {});
}
// Note: May throw all sorts of things.
static NetAddress _getNetAddressFromUrl(Uri url) {
var params = url.queryParameters;
var host = params['host'];
return makeIPv4NetAddress(
(host == 'localhost') ? [127, 0, 0, 1] : Uri.parseIPv4Address(host),
int.parse(params['port']));
}
}
class NetcatApplication extends Application {
NetcatApplication.fromHandle(MojoHandle handle) : super.fromHandle(handle);
@override
void acceptConnection(String requestorUrl, String resolvedUrl,
ApplicationConnection connection) {
connection.provideService(TerminalClient.serviceName,
(endpoint) => new TerminalClientImpl(this, resolvedUrl, endpoint));
}
}
main(List args, Object handleToken) {
MojoHandle appHandle = new MojoHandle(handleToken);
new NetcatApplication.fromHandle(appHandle)
..onError = ((Object e) {
MojoHandle.reportLeakedHandles();
});
}