Skip to content

Commit

Permalink
Initial version of the inline powerbox
Browse files Browse the repository at this point in the history
It's still a bit rough around the edges, but the basic functionality
works.
  • Loading branch information
jparyani committed Jan 20, 2016
1 parent c4cf509 commit 5b682ae
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 120 deletions.
14 changes: 14 additions & 0 deletions shell/client/grainview.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ GrainView = class GrainView {
this.revealIdentity();
}

this.enableInlinePowerbox = new ReactiveVar(false);

// We manage our Blaze view directly in order to get more control over when iframes get
// re-rendered. E.g. if we were to instead use a template with {{#each grains}} iterating over
// the list of open grains, all grains might get re-rendered whenever a grain is removed from the
Expand Down Expand Up @@ -617,6 +619,18 @@ GrainView = class GrainView {
this._generatedApiToken = newApiToken;
this._dep.changed();
}

startInlinePowerbox(inlinePowerboxState) {
this.inlinePowerboxState = inlinePowerboxState;
if (inlinePowerboxState.isForeground) {
this.enableInlinePowerbox.set(true);
} else {
state.source.postMessage({
rpcId: inlinePowerboxState.rpcId,
error: "Cannot start inline powerbox when app is not in foreground",
}, inlinePowerboxState.origin);
}
}
};

const onceConditionIsTrue = (condition, continuation) => {
Expand Down
1 change: 1 addition & 0 deletions shell/client/shell.html
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ <h4>Notice from Admin</h4>
{{/if}}
</div>
{{/with}}
<input type="text" class="inline-powerbox">
</template>

<template name="invalidToken">
Expand Down
5 changes: 5 additions & 0 deletions shell/client/shell.scss
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,8 @@ body>.popup.admin-alert>.frame>p {
font-size: 30px;
}
}

.inline-powerbox {
// Hack to make it effectively invisible, but not be treated as un-focusable by browsers
margin: -10000px;
}
34 changes: 34 additions & 0 deletions shell/server/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,38 @@ Meteor.methods({

Notifications.update({userId: Meteor.userId()}, {$set: {isUnread: false}}, {multi: true});
},
offerExternalWebSession(grainId, identityId, url) {
check(grainId, String);
check(identityId, String);
check(url, String);

const db = this.connection.sandstormDb;

// Check that the the identityId matches and has permission to view this grain
if (!db.userHasIdentity(Meteor.userId(), identityId)) {
throw new Meteor.Error(403, "Logged in user doesn't own the supplied identity.");
}
const requirement = {
permissionsHeld: {
grainId: grainId,
identityId: identityId,
permissions: [], // We only want to check for the implicit view permission
},
};
if (!checkRequirements([requirement])) {
throw new Meteor.Error(403, "This identity doesn't have view permissions to the grain.");
}
const requirements = []; // We don't actually want the user's permission check as a requirement.
const grainOwner = {grain: {
grainId: grainId,
introducerIdentity: identityId,
saveLabel: url + " websession",
}};
const sturdyRef = waitPromise(saveFrontendRef(
{externalWebSession: {url: url}}, grainOwner, requirements)).sturdyRef;

return sturdyRef.toString();
}
});

saveFrontendRef = (frontendRef, owner, requirements) => {
Expand Down Expand Up @@ -320,6 +352,8 @@ restoreInternal = (tokenId, ownerPattern, requirements, parentToken) => {
return {cap: makeIpNetwork(tokenId)};
} else if (token.frontendRef.ipInterface) {
return {cap: makeIpInterface(tokenId)};
} else if (token.frontendRef.externalWebSession) {
return {cap: makeExternalWebSession(token.frontendRef.externalWebSession.url)};
} else {
throw new Meteor.Error(500, 'Unknown frontend token type.');
}
Expand Down
266 changes: 147 additions & 119 deletions shell/server/drivers/external-ui-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const Capnp = Npm.require('capnp');
const Url = Npm.require('url');
const Http = Npm.require('http');
const Https = Npm.require('https');
const Dns = Npm.require('dns');
const ApiSession = Capnp.importSystem('sandstorm/api-session.capnp').ApiSession;
const WebSession = Capnp.importSystem('sandstorm/web-session.capnp').WebSession;

WrappedUiView = class WrappedUiView {
constructor(token, proxy) {
Expand Down Expand Up @@ -83,7 +85,7 @@ ExternalUiView = class ExternalUiView {
};
}

return {session: new Capnp.Capability(new ExternalWebSession(this.url, this.grainId, options), ApiSession)};
return {session: new Capnp.Capability(new ExternalWebSession(this.url, options), ApiSession)};
}
};

Expand Down Expand Up @@ -125,13 +127,17 @@ const responseCodes = {
505: {type: 'serverError'},
};

makeExternalWebSession = function (url, options) {
return new Capnp.Capability(new ExternalWebSession(url, options), WebSession);
}

ExternalWebSession = class ExternalWebSession {
constructor(url, grainId, options) {
constructor(url, options) {
const parsedUrl = Url.parse(url);
this.host = parsedUrl.hostname;
this.port = parsedUrl.port;
this.protocol = parsedUrl.protocol;
this.grainId = grainId;
this.path = parsedUrl.path;
this.options = options || {};
}

Expand Down Expand Up @@ -165,129 +171,151 @@ ExternalWebSession = class ExternalWebSession {
const _this = this;
const session = _this;
return new Promise((resolve, reject) => {
const options = _.clone(session.options);
options.headers = options.headers || {};
options.path = path;
options.method = method;
if (contentType) {
options.headers['content-type'] = contentType;
}

// set accept header
if ('accept' in context) {
options.headers.accept = context.accept.map((acceptedType) => {
return acceptedType.mimeType + '; ' + acceptedType.qValue;
}).join(', ');
} else if (!('accept' in options.headers)) {
options.headers.accept = '*/*';
}

// set cookies
if (context.cookies && context.cookies.length > 0) {
options.headers.cookies = options.headers.cookies || '';
context.cookies.forEach((keyVal) => {
options.headers.cookies += keyVal.key + '=' + keyVal.val + ',';
});
options.headers.cookies = options.headers.cookies.slice(0, -1);
}

options.host = session.host;
options.port = session.port;

let requestMethod = Http.request;
if (session.protocol === 'https:') {
requestMethod = Https.request;
}

req = requestMethod(options, (resp) => {
const buffers = [];
const statusInfo = responseCodes[resp.statusCode];

const rpcResponse = {};

switch (statusInfo.type) {
case 'content':
resp.on('data', (buf) => {
buffers.push(buf);
});

resp.on('end', () => {
const content = {};
rpcResponse.content = content;

content.statusCode = statusInfo.code;
if ('content-encoding' in resp.headers) content.encoding = resp.headers['content-encoding'];
if ('content-language' in resp.headers) content.language = resp.headers['content-language'];
if ('content-type' in resp.headers) content.language = resp.headers['content-type'];
if ('content-disposition' in resp.headers) {
const disposition = resp.headers['content-disposition'];
const parts = disposition.split(';');
if (parts[0].toLowerCase().trim() === 'attachment') {
parts.forEach((part) => {
const splitPart = part.split('=');
if (splitPart[0].toLowerCase().trim() === 'filename') {
content.disposition = {download: splitPart[1].trim()};
}
});
Dns.lookup(_this.host, 4, (err, address) => { // TODO(someday): handle ipv6
if (err) {
reject(err);
return;
}
if (address.lastIndexOf("10.", 0) === 0 ||
address.lastIndexOf("127.", 0) === 0 ||
address.lastIndexOf("192.168.", 0) === 0) {
// Block the most commonly used private ip ranges as a security measure.
reject("Domain resolved to an invalid IP: " + address);
return;
}
const options = _.clone(session.options);
options.headers = options.headers || {};
if (_this.path) {
options.path = _this.path;
if (_this.path[_this.path.length - 1] !== "/") {
options.path += "/";
}
options.path += path;
} else {
options.path = path;
}
options.path = (_this.path || "") + path;
options.method = method;
if (contentType) {
options.headers['content-type'] = contentType;
}

// set accept header
if ('accept' in context) {
options.headers.accept = context.accept.map((acceptedType) => {
return acceptedType.mimeType + '; ' + acceptedType.qValue;
}).join(', ');
} else if (!('accept' in options.headers)) {
options.headers.accept = '*/*';
}

// set cookies
if (context.cookies && context.cookies.length > 0) {
options.headers.cookies = options.headers.cookies || '';
context.cookies.forEach((keyVal) => {
options.headers.cookies += keyVal.key + '=' + keyVal.val + ',';
});
options.headers.cookies = options.headers.cookies.slice(0, -1);
}

options.host = session.host;
options.port = session.port;

let requestMethod = Http.request;
if (session.protocol === 'https:') {
requestMethod = Https.request;
}

req = requestMethod(options, (resp) => {
const buffers = [];
const statusInfo = responseCodes[resp.statusCode];

const rpcResponse = {};

switch (statusInfo.type) {
case 'content':
resp.on('data', (buf) => {
buffers.push(buf);
});

resp.on('end', () => {
const content = {};
rpcResponse.content = content;

content.statusCode = statusInfo.code;
if ('content-encoding' in resp.headers) content.encoding = resp.headers['content-encoding'];
if ('content-language' in resp.headers) content.language = resp.headers['content-language'];
if ('content-type' in resp.headers) content.language = resp.headers['content-type'];
if ('content-disposition' in resp.headers) {
const disposition = resp.headers['content-disposition'];
const parts = disposition.split(';');
if (parts[0].toLowerCase().trim() === 'attachment') {
parts.forEach((part) => {
const splitPart = part.split('=');
if (splitPart[0].toLowerCase().trim() === 'filename') {
content.disposition = {download: splitPart[1].trim()};
}
});
}
}
}

content.body = {};
content.body.bytes = Buffer.concat(buffers);
content.body = {};
content.body.bytes = Buffer.concat(buffers);

resolve(rpcResponse);
});
break;
case 'noContent':
const noContent = {};
rpcResponse.noContent = noContent;
noContent.setShouldResetForm = statusInfo.shouldResetForm;
resolve(rpcResponse);
});
break;
case 'noContent':
const noContent = {};
rpcResponse.noContent = noContent;
noContent.setShouldResetForm = statusInfo.shouldResetForm;
resolve(rpcResponse);
break;
case 'redirect':
const redirect = {};
rpcResponse.redirect = redirect;
redirect.isPermanent = statusInfo.isPermanent;
redirect.switchToGet = statusInfo.switchToGet;
if ('location' in resp.headers) redirect.location = resp.headers.location;
resolve(rpcResponse);
break;
case 'clientError':
const clientError = {};
rpcResponse.clientError = clientError;
clientError.statusCode = statusInfo.clientErrorCode;
clientError.descriptionHtml = statusInfo.descriptionHtml;
resolve(rpcResponse);
break;
case 'serverError':
const serverError = {};
rpcResponse.serverError = serverError;
clientError.descriptionHtml = statusInfo.descriptionHtml;
resolve(rpcResponse);
break;
default: // ???
err = new Error('Invalid status code ' + resp.statusCode + ' received in response.');
reject(err);
break;
}
});
break;
case 'redirect':
const redirect = {};
rpcResponse.redirect = redirect;
redirect.isPermanent = statusInfo.isPermanent;
redirect.switchToGet = statusInfo.switchToGet;
if ('location' in resp.headers) redirect.location = resp.headers.location;
resolve(rpcResponse);
break;
case 'clientError':
const clientError = {};
rpcResponse.clientError = clientError;
clientError.statusCode = statusInfo.clientErrorCode;
clientError.descriptionHtml = statusInfo.descriptionHtml;
resolve(rpcResponse);
break;
case 'serverError':
const serverError = {};
rpcResponse.serverError = serverError;
clientError.descriptionHtml = statusInfo.descriptionHtml;
resolve(rpcResponse);
break;
default: // ???
err = new Error('Invalid status code ' + resp.statusCode + ' received in response.');
reject(err);
break;
}
});

req.on('error', (e) => {
reject(e);
});
req.on('error', (e) => {
reject(e);
});

req.setTimeout(15000, () => {
req.abort();
err = new Error('Request timed out.');
err.kjType = 'overloaded';
reject(err);
});
req.setTimeout(15000, () => {
req.abort();
err = new Error('Request timed out.');
err.kjType = 'overloaded';
reject(err);
});

if (content) {
req.end(content);
} else {
req.end();
}
if (content) {
req.end(content);
} else {
req.end();
}
});
});
}
};
Loading

0 comments on commit 5b682ae

Please sign in to comment.