Skip to content

Commit c1a1c73

Browse files
authored
Merge pull request #2808 from uProxy/trevj-air-save-servers
save contacts to HTML5 localStorage
2 parents 42d2bd0 + 4af5a20 commit c1a1c73

File tree

9 files changed

+122
-53
lines changed

9 files changed

+122
-53
lines changed

Gruntfile.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ module.exports = function(grunt) {
252252
// compatibility with the modified libxwalkcore.so that provides obfuscated WebRTC.
253253
ccaBuildAndroid: {
254254
cwd: '<%= androidDevPath %>',
255-
command: '<%= ccaJsPath %> build android --debug --webview=crosswalk@org.xwalk:xwalk_core_library_beta:22.52.561.2'
255+
// Pipe 'no' for the first time cca.js asks whether to send usage stats (grunt build_android_lite).
256+
command: 'echo no | <%= ccaJsPath %> build android --debug --webview=crosswalk@org.xwalk:xwalk_core_library_beta:22.52.561.2'
256257
},
257258
ccaReleaseAndroid: {
258259
cwd: '<%= androidDistPath %>',

src/cca/model/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ export type AccessCode = string;
1313

1414
export interface ServerRepository {
1515
addServer(code: AccessCode): Promise<Server>
16+
// Fetches the list of servers known to this repository.
17+
getServers(): Promise<Server[]>;
1618
}

src/cca/scripts/background.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ import { MakeCoreConnector } from './cordova_core_connector';
1414
import { GetGlobalTun2SocksVpnDevice } from './tun2socks_vpn_device';
1515
import * as vpn_device from '../model/vpn_device';
1616
import * as intents from './intents';
17+
import * as uproxy_core_api from '../../interfaces/uproxy_core_api';
18+
19+
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
20+
function getLocalStorage(): Storage {
21+
try {
22+
const storage = window['localStorage'];
23+
const x = '__storage_test__';
24+
storage.setItem(x, x);
25+
storage.removeItem(x);
26+
return localStorage;
27+
} catch(e) {
28+
throw new Error('localStorage unavailable');
29+
}
30+
}
1731

1832
// We save this reference to allow inspection of the context state from the browser debuggin tools.
1933
(window as any).context = this;
@@ -22,15 +36,20 @@ import * as intents from './intents';
2236
// TODO(fortuna): I believe we need to somehow wait for the core to be ready.
2337
let core = MakeCoreConnector();
2438

25-
// Create UproxyServerRepository.
39+
// Log into the cloud social network and create UproxyServerRepository.
2640
let serversPromise = GetGlobalTun2SocksVpnDevice().then((vpnDevice) => {
2741
console.debug('Device supports VPN');
2842
return vpnDevice;
2943
}).catch((error) => {
3044
console.error(error);
3145
return new vpn_device.NoOpVpnDevice();
3246
}).then((vpnDevice) => {
33-
return new UproxyServerRepository(core, vpnDevice);
47+
return core.login({
48+
network: 'Cloud',
49+
loginType: uproxy_core_api.LoginType.INITIAL,
50+
}).then(() => {
51+
return new UproxyServerRepository(getLocalStorage(), core, vpnDevice);
52+
});
3453
});
3554

3655
// Create UI.

src/cca/scripts/uproxy_server.ts

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import uparams = require('uparams');
2+
import * as cloud_social_provider from '../../lib/cloud/social/provider';
23
import * as jsurl from 'jsurl';
34
import * as social from '../../interfaces/social';
45
import * as uproxy_core_api from '../../interfaces/uproxy_core_api';
@@ -9,26 +10,6 @@ import { SocksProxy } from '../model/socks_proxy_server';
910
import { VpnDevice } from '../model/vpn_device';
1011
import { CloudSocksProxy } from './cloud_socks_proxy_server';
1112

12-
function parseInviteUrl(inviteUrl: string): social.InviteTokenData {
13-
let params = uparams(inviteUrl);
14-
if (!params || !params['networkName']) {
15-
throw new Error(`networkName not found: ${inviteUrl}`);
16-
}
17-
var permission: any;
18-
if (params['permission']) {
19-
permission = jsurl.parse(params['permission']);
20-
}
21-
return {
22-
v: parseInt(params['v'], 10),
23-
networkData: JSON.parse(jsurl.parse(params['networkData'])),
24-
networkName: params['networkName'],
25-
userName: params['userName'],
26-
permission: permission,
27-
userId: params['userId'], // undefined if no permission
28-
instanceId: params['instanceId'], // undefined if no permission
29-
}
30-
}
31-
3213
// A local Socks server that provides access to a remote uProxy Cloud server via RTC.
3314
export class UproxyServer implements Server {
3415
private instancePath: social.InstancePath;
@@ -56,25 +37,76 @@ export class UproxyServer implements Server {
5637
}
5738
}
5839

40+
// Name by which servers are saved to storage.
41+
const SERVERS_STORAGE_KEY = 'servers';
42+
43+
// Type of the object placed, in serialised form, in storage.
44+
type SavedServers = { [id: string]: SavedServer };
45+
46+
// A server as saved to storage.
47+
interface SavedServer {
48+
cloudTokens?: cloud_social_provider.Invite;
49+
}
50+
51+
// Maintains a persisted set of servers and liases with the core.
5952
export class UproxyServerRepository implements ServerRepository {
60-
constructor(private core: CoreConnector, private vpnDevice: VpnDevice) {
61-
this.core.login({
62-
network: 'Cloud',
63-
loginType: uproxy_core_api.LoginType.INITIAL,
64-
});
53+
constructor(
54+
private storage: Storage,
55+
// Must already be logged into social networks.
56+
private core: CoreConnector,
57+
private vpnDevice: VpnDevice) { }
58+
59+
public getServers() {
60+
const servers = this.loadServers();
61+
return Promise.all(Object.keys(servers).map((host) => {
62+
return this.notifyCoreOfServer(servers[host].cloudTokens);
63+
}));
6564
}
6665

67-
public addServer(code: AccessCode): Promise<Server> {
68-
let token = parseInviteUrl(code);
69-
if (!token) {
70-
return Promise.reject(`Failed to parse access code: ${code}`);
66+
private loadServers(): SavedServers {
67+
return JSON.parse(this.storage.getItem(SERVERS_STORAGE_KEY)) || {};
68+
}
69+
70+
// Saves a server to storage, merging it with any already found there.
71+
// Returns true if the server was not already in storage.
72+
private saveServer(cloudTokens: cloud_social_provider.Invite) {
73+
const savedServers = this.loadServers();
74+
savedServers[cloudTokens.host] = {
75+
cloudTokens: cloudTokens
76+
};
77+
this.storage.setItem(SERVERS_STORAGE_KEY, JSON.stringify(savedServers));
78+
}
79+
80+
public addServer(accessCode: AccessCode) {
81+
// This is inspired by ui.ts but note that uProxy Air only
82+
// supports v2 access codes which have just three fields:
83+
// - v
84+
// - networkName
85+
// - networkData
86+
// TODO: accept only cloud access codes
87+
const params: social.InviteTokenData = uparams(accessCode);
88+
if (!(params || params.v ||
89+
params.networkName || params.networkData)) {
90+
return Promise.reject(new Error('could not decode URL'));
7191
}
72-
// TODO: Do I need to wait for core.login()?
92+
93+
const cloudTokens: cloud_social_provider.Invite = JSON.parse(
94+
jsurl.parse(<string>params.networkData));
95+
this.saveServer(cloudTokens);
96+
// TODO: only notify the core when connecting, and delete it afterwards
97+
return this.notifyCoreOfServer(cloudTokens);
98+
}
99+
100+
private notifyCoreOfServer(cloudTokens: cloud_social_provider.Invite) {
73101
return this.core.acceptInvitation({
74-
network: {name: 'Cloud'},
75-
tokenObj: token
102+
network: {
103+
name: 'Cloud'
104+
},
105+
tokenObj: {
106+
networkData: cloudTokens,
107+
}
76108
}).then(() => {
77-
let proxy = new CloudSocksProxy(this.core, (token.networkData as any).host);
109+
let proxy = new CloudSocksProxy(this.core, cloudTokens.host);
78110
return new UproxyServer(proxy, this.vpnDevice, proxy.getRemoteIpAddress());
79111
});
80112
}

src/cca/ui_components/server_list.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export class ServerListPage {
5555
private addButton: HTMLButtonElement;
5656
private entryList: HTMLDivElement;
5757

58+
// Servers currently shown, indexed by hostname.
59+
// Used to prevent listing servers more than once.
60+
private activeServerIds = new Set<String>();
61+
5862
// Parameters:
5963
// - root: Where to attach the ServerListPage to
6064
// - servers: the repository of the servers we can connect to.
@@ -68,6 +72,12 @@ export class ServerListPage {
6872
console.debug('Pressed Add Button');
6973
this.pressAddServer();
7074
});
75+
76+
servers.getServers().then((restoredServers) => {
77+
restoredServers.forEach((server) => {
78+
this.addServer(server);
79+
});
80+
});
7181
}
7282

7383
public enterAccessCode(code: string) {
@@ -77,12 +87,18 @@ export class ServerListPage {
7787

7888
public pressAddServer(): Promise<ServerEntryComponent> {
7989
return this.servers.addServer(this.addTokenText.value).then((server) => {
80-
let entryElement = this.root.ownerDocument.createElement('div');
81-
let serverEntry = new ServerEntryComponent(entryElement, server);
82-
this.entryList.appendChild(entryElement);
83-
return serverEntry;
84-
}).catch((error) => {
85-
console.error(error);
90+
this.addServer(server);
8691
});
8792
}
93+
94+
private addServer(server: Server) {
95+
if (this.activeServerIds.has(server.getIpAddress())) {
96+
return;
97+
}
98+
99+
this.activeServerIds.add(server.getIpAddress());
100+
const entryElement = this.root.ownerDocument.createElement('div');
101+
this.entryList.appendChild(entryElement);
102+
return new ServerEntryComponent(entryElement, server);
103+
}
88104
}

src/generic_core/social.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,10 @@ export function initializeNetworks() :void {
8282
*/
8383
export function getNetwork(networkName :string, userId :string) :social.Network {
8484
if (!(networkName in networks)) {
85-
log.warn('Network does not exist', networkName);
86-
return null;
85+
throw new Error('unknown network ${networkName}');
8786
}
88-
8987
if (!(userId in networks[networkName])) {
90-
log.info('Not logged in to network', {
91-
userId: userId,
92-
network: networkName
93-
});
94-
return null;
88+
throw new Error('${userId} is not logged into network ${networkName}');
9589
}
9690
return networks[networkName][userId];
9791
}

src/generic_core/uproxy_core.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,12 @@ export class uProxyCore implements uproxy_core_api.CoreApi {
368368
// Assumes the user is only signed in once to any given network.
369369
networkUserId = Object.keys(social_network.networks[networkName])[0];
370370
}
371-
var network = social_network.getNetwork(networkName, networkUserId);
372-
return network.acceptInvitation(data.tokenObj, data.userId);
371+
try {
372+
return social_network.getNetwork(networkName, networkUserId).acceptInvitation(
373+
data.tokenObj, data.userId);
374+
} catch (e) {
375+
return Promise.reject(e);
376+
}
373377
}
374378

375379
public getInviteUrl = (data :uproxy_core_api.CreateInviteArgs): Promise<string> => {

src/lib/cloud/social/provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const DEFAULT_MESSAGE_VERSION = 4;
3737

3838
// Credentials for accessing a cloud instance.
3939
// The serialised, base64 form is distributed amongst users.
40-
interface Invite {
40+
export interface Invite {
4141
// Hostname or IP of the cloud instance.
4242
// This is the host on which sshd is running, so it should
4343
// be directly accessible from the client.

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"es5",
1212
"es2015.promise",
1313
"es2015.iterable",
14+
"es2015.collection",
1415
"scripthost"
1516
]
1617
},

0 commit comments

Comments
 (0)