@@ -5,6 +5,7 @@ import { createConnection } from 'net';
5
5
import express from 'express' ;
6
6
import WebSocket from 'ws' ;
7
7
import fs from 'fs' ;
8
+ import crypto from 'crypto' ;
8
9
9
10
import anylogger from 'anylogger' ;
10
11
@@ -23,16 +24,44 @@ const send = (ws, msg) => {
23
24
}
24
25
} ;
25
26
27
+ // Taken from https://github.com/marcsAtSkyhunter/Capper/blob/9d20b92119f91da5201a10a0834416bd449c4706/caplib.js#L80
28
+ export function unique ( ) {
29
+ const chars =
30
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' ;
31
+ let ans = '' ;
32
+ const buf = crypto . randomBytes ( 25 ) ;
33
+ for ( let i = 0 ; i < buf . length ; i ++ ) {
34
+ const index = buf [ i ] % chars . length ;
35
+ ans += chars [ index ] ;
36
+ }
37
+ // while (ans.length < 30) {
38
+ // var nextI = Math.floor(Math.random()*10000) % chars.length;
39
+ // ans += chars[nextI];
40
+ // }
41
+ return ans ;
42
+ }
43
+
26
44
export async function makeHTTPListener ( basedir , port , host , rawInboundCommand ) {
45
+ // Ensure we're protected with a unique webkey for this basedir.
46
+ fs . chmodSync ( basedir , 0o700 ) ;
47
+ const privateWebkeyFile = path . join ( basedir , 'private-webkey.txt' ) ;
48
+ if ( ! fs . existsSync ( privateWebkeyFile ) ) {
49
+ // Create the unique string for this basedir.
50
+ fs . writeFileSync ( privateWebkeyFile , unique ( ) , { mode : 0o600 } ) ;
51
+ }
52
+
27
53
// Enrich the inbound command with some metadata.
28
54
const inboundCommand = (
29
55
body ,
30
56
{ channelID, dispatcher, url, headers : { origin } = { } } = { } ,
31
57
id = undefined ,
32
58
) => {
59
+ // Strip away the query params, as the webkey is there.
60
+ const qmark = url . indexOf ( '?' ) ;
61
+ const shortUrl = qmark < 0 ? url : url . slice ( 0 , qmark ) ;
33
62
const obj = {
34
63
...body ,
35
- meta : { channelID, dispatcher, origin, url, date : Date . now ( ) } ,
64
+ meta : { channelID, dispatcher, origin, url : shortUrl , date : Date . now ( ) } ,
36
65
} ;
37
66
return rawInboundCommand ( obj ) . catch ( err => {
38
67
const idpfx = id ? `${ id } ` : '' ;
@@ -75,15 +104,30 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
75
104
log ( `Serving static files from ${ htmldir } ` ) ;
76
105
app . use ( express . static ( htmldir ) ) ;
77
106
78
- const validateOrigin = req => {
107
+ const validateOriginAndWebkey = req => {
79
108
const { origin } = req . headers ;
80
109
const id = `${ req . socket . remoteAddress } :${ req . socket . remotePort } :` ;
81
110
82
111
if ( ! req . url . startsWith ( '/private/' ) ) {
83
- // Allow any origin that's not marked private.
112
+ // Allow any origin that's not marked private, without a webkey .
84
113
return true ;
85
114
}
86
115
116
+ // Validate the private webkey.
117
+ const privateWebkey = fs . readFileSync ( privateWebkeyFile , 'utf-8' ) ;
118
+ const reqWebkey = new URL ( `http://localhost${ req . url } ` ) . searchParams . get (
119
+ 'webkey' ,
120
+ ) ;
121
+ if ( reqWebkey !== privateWebkey ) {
122
+ log . error (
123
+ id ,
124
+ `Invalid webkey ${ JSON . stringify (
125
+ reqWebkey ,
126
+ ) } ; try running "agoric open"`,
127
+ ) ;
128
+ return false ;
129
+ }
130
+
87
131
if ( ! origin ) {
88
132
log . error ( id , `Missing origin header` ) ;
89
133
return false ;
@@ -93,7 +137,7 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
93
137
hostname . match ( / ^ ( l o c a l h o s t | 1 2 7 \. 0 \. 0 \. 1 ) $ / ) ;
94
138
95
139
if ( [ 'chrome-extension:' , 'moz-extension:' ] . includes ( url . protocol ) ) {
96
- // Extensions such as metamask can access the wallet.
140
+ // Extensions such as metamask are local and can access the wallet.
97
141
return true ;
98
142
}
99
143
@@ -109,10 +153,17 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
109
153
return true ;
110
154
} ;
111
155
156
+ // Allow people to see where this installation is.
157
+ app . get ( '/ag-solo-basedir' , ( req , res ) => {
158
+ res . contentType ( 'text/plain' ) ;
159
+ res . write ( basedir ) ;
160
+ res . end ( ) ;
161
+ } ) ;
162
+
112
163
// accept POST messages to arbitrary endpoints
113
164
app . post ( '*' , ( req , res ) => {
114
- if ( ! validateOrigin ( req ) ) {
115
- res . json ( { ok : false , rej : 'Unauthorized Origin ' } ) ;
165
+ if ( ! validateOriginAndWebkey ( req ) ) {
166
+ res . json ( { ok : false , rej : 'Unauthorized' } ) ;
116
167
return ;
117
168
}
118
169
@@ -130,7 +181,7 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
130
181
// GETs (which should return index.html) and WebSocket requests.
131
182
const wss = new WebSocket . Server ( { noServer : true } ) ;
132
183
server . on ( 'upgrade' , ( req , socket , head ) => {
133
- if ( ! validateOrigin ( req ) ) {
184
+ if ( ! validateOriginAndWebkey ( req ) ) {
134
185
socket . destroy ( ) ;
135
186
return ;
136
187
}
0 commit comments