tlsd 2.15.0 → 2.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/package.json +1 -1
- package/rpc_static/rpc/index.html +214 -0
- package/scaffold/rpc/index.cjs +4 -9
- package/scaffold/static/index.html +34 -17
- package/tlsd +2 -2
- package/tlsd.js +129 -36
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ It is *not* designed for production websites.
|
|
|
9
9
|
|
|
10
10
|
- SSL/TLS via Greenlock
|
|
11
11
|
- Websockets built in
|
|
12
|
-
|
|
12
|
+
- Standardised Websockets/RPC protocol for front/back end communications
|
|
13
13
|
- Utility command 'tlsd'
|
|
14
14
|
|
|
15
15
|
|
|
@@ -48,16 +48,16 @@ You should see the pages being delivered and a websocket connection being establ
|
|
|
48
48
|
In dev mode, tlsd runs a plain HTTP server without TLS/SSL security.
|
|
49
49
|
This is for local (your computer) development:
|
|
50
50
|
|
|
51
|
-
tlsd root_dir
|
|
51
|
+
tlsd run dev root_dir port verbosity
|
|
52
52
|
|
|
53
53
|
All arguments are required.
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
## tlsd domain
|
|
57
57
|
|
|
58
|
-
Adds a domain to Greenlock so
|
|
58
|
+
Adds a domain to Greenlock and links the site's root so it can be served with HTTPS:
|
|
59
59
|
|
|
60
|
-
tlsd domain example.com
|
|
60
|
+
tlsd domain example.com /absolute/path/to/site_root
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
## tlsd version
|
|
@@ -71,11 +71,15 @@ In HTTPS mode, you must specify the dir containing links/dirs for domains to ser
|
|
|
71
71
|
it will run on HTTP on 80 (redirecting to 443), HTTPS on 443, and magically
|
|
72
72
|
generate certs and serve secure sites like nodes:
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
DOMAINS_ROOT=./my_domains MAINTAINER_EMAIL=foo@bar.com VERBOSITY=4 node tlsd.js
|
|
75
75
|
|
|
76
|
-
All env vars
|
|
76
|
+
All env vars are required.
|
|
77
77
|
|
|
78
|
-
You must also
|
|
78
|
+
You must also register your domain before running in TLS mode, otherwise it won't be recognized. Either run:
|
|
79
|
+
|
|
80
|
+
tlsd domain example.com /absolute/path/to/site_root
|
|
81
|
+
|
|
82
|
+
or use Greenlock directly:
|
|
79
83
|
|
|
80
84
|
npx greenlock add --subject example.com --altnames example.com,www.example.com
|
|
81
85
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚙️</text></svg>">
|
|
7
|
+
<title>RPC Tool</title>
|
|
8
|
+
<style>
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: Arial, sans-serif;
|
|
17
|
+
height: 100vh;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.grid-container {
|
|
22
|
+
display: grid;
|
|
23
|
+
grid-template-columns: 1fr 1fr;
|
|
24
|
+
grid-template-rows: 1fr 1fr;
|
|
25
|
+
height: 100vh;
|
|
26
|
+
gap: 10px;
|
|
27
|
+
padding: 10px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.textarea-group {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
height: 100%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.textarea-group:first-child {
|
|
37
|
+
position: relative;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.send-button {
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 0;
|
|
43
|
+
right: 0;
|
|
44
|
+
background-color: #007bff;
|
|
45
|
+
color: white;
|
|
46
|
+
border: none;
|
|
47
|
+
border-radius: 4px;
|
|
48
|
+
padding: 6px 12px;
|
|
49
|
+
font-size: 12px;
|
|
50
|
+
font-weight: bold;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
transition: background-color 0.2s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.send-button:hover {
|
|
56
|
+
background-color: #0056b3;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.send-button:active {
|
|
60
|
+
background-color: #004085;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.textarea-label {
|
|
64
|
+
font-weight: bold;
|
|
65
|
+
margin-bottom: 5px;
|
|
66
|
+
color: #333;
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.textarea-input {
|
|
71
|
+
flex: 1;
|
|
72
|
+
resize: none;
|
|
73
|
+
border: 2px solid #ddd;
|
|
74
|
+
border-radius: 5px;
|
|
75
|
+
padding: 10px;
|
|
76
|
+
font-family: monospace;
|
|
77
|
+
font-size: 14px;
|
|
78
|
+
line-height: 1.4;
|
|
79
|
+
background-color: #f9f9f9;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.textarea-input:focus {
|
|
83
|
+
outline: none;
|
|
84
|
+
border-color: #007bff;
|
|
85
|
+
background-color: white;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.textarea-input:hover {
|
|
89
|
+
border-color: #007bff;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
92
|
+
<script src="/rpc/rpc.js"></script>
|
|
93
|
+
</head>
|
|
94
|
+
<body>
|
|
95
|
+
<div class="grid-container">
|
|
96
|
+
<div class="textarea-group">
|
|
97
|
+
<label class="textarea-label">Message Data</label>
|
|
98
|
+
<button class="send-button">Send</button>
|
|
99
|
+
<textarea class="textarea-input" id="e_input" ></textarea>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="textarea-group">
|
|
102
|
+
<label class="textarea-label">Preview</label>
|
|
103
|
+
<textarea class="textarea-input" id="e_preview" ></textarea>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="textarea-group">
|
|
106
|
+
<label class="textarea-label">Sent to Server →</label>
|
|
107
|
+
<textarea class="textarea-input" id="e_sent" ></textarea>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="textarea-group">
|
|
110
|
+
<label class="textarea-label">← Received from Server</label>
|
|
111
|
+
<textarea class="textarea-input" id="e_received" ></textarea>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<script>
|
|
116
|
+
|
|
117
|
+
// Get the send button and first textarea
|
|
118
|
+
const sendButton = document.querySelector('.send-button');
|
|
119
|
+
const e_input = document.querySelector('#e_input');
|
|
120
|
+
const e_preview = document.querySelector('#e_preview');
|
|
121
|
+
const e_sent = document.querySelector('#e_sent');
|
|
122
|
+
const e_received = document.querySelector('#e_received');
|
|
123
|
+
|
|
124
|
+
// Add click event listener to the send button
|
|
125
|
+
sendButton.addEventListener('click', function() {
|
|
126
|
+
// Get the content from the first textarea
|
|
127
|
+
const textareaContent = e_input.value;
|
|
128
|
+
|
|
129
|
+
// Store the content in local storage
|
|
130
|
+
localStorage.setItem('textareaContent', textareaContent);
|
|
131
|
+
let data = parse_input( textareaContent );
|
|
132
|
+
|
|
133
|
+
let json_out = o2j( data, null, 2 );
|
|
134
|
+
e_sent.value = json_out;
|
|
135
|
+
|
|
136
|
+
let x_data = j2o( o2j( data ) );
|
|
137
|
+
delete x_data.action;
|
|
138
|
+
let x_json = o2j( x_data, null, 4 );
|
|
139
|
+
|
|
140
|
+
e_received.value = "...";
|
|
141
|
+
|
|
142
|
+
RPC( data, function( rsp ) {
|
|
143
|
+
e_received.value = o2j( rsp, null, 2 );
|
|
144
|
+
}, function( err ) {
|
|
145
|
+
e_received.value = "ERROR:\n" + o2j( err, null, 2 );
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Load content from local storage when page loads
|
|
151
|
+
window.addEventListener('pageshow', function() {
|
|
152
|
+
const savedContent = localStorage.getItem('textareaContent');
|
|
153
|
+
if (savedContent) {
|
|
154
|
+
e_input.value = savedContent;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
function toInt( v ) {
|
|
159
|
+
return parseInt( v );
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function j2o( v ) {
|
|
163
|
+
return JSON.parse( v );
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function o2j( v, null_to_empty, indent ) {
|
|
167
|
+
return JSON.stringify( v, null, indent );
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// parse the input into a data object
|
|
171
|
+
function parse_input( input ) {
|
|
172
|
+
let data = {};
|
|
173
|
+
let action = null;
|
|
174
|
+
let lines = input.trim().split( /\n+/ ).filter( ln => ln );
|
|
175
|
+
for( let line of lines ) {
|
|
176
|
+
let words = line.split( /\s+/ );
|
|
177
|
+
let k = words.shift();
|
|
178
|
+
let v = words.join( " " );
|
|
179
|
+
if( k == "action" ) {
|
|
180
|
+
action = v;
|
|
181
|
+
}
|
|
182
|
+
if( k[ 0 ] == "#" ) {
|
|
183
|
+
continue; // ignore
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
if( v[ 0 ] == "{" || v[ 0 ] == "[" ) {
|
|
187
|
+
v = j2o( v ) || v;
|
|
188
|
+
}
|
|
189
|
+
else
|
|
190
|
+
if( /^[\d]+$/.test( v ) ) {
|
|
191
|
+
v = toInt( v );
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
if( v == "true" ) {
|
|
195
|
+
v = true;
|
|
196
|
+
}
|
|
197
|
+
else
|
|
198
|
+
if( v == "false" ) {
|
|
199
|
+
v = false;
|
|
200
|
+
}
|
|
201
|
+
data[ k ] = v;
|
|
202
|
+
}
|
|
203
|
+
return data;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setInterval( function() {
|
|
207
|
+
let data = parse_input( e_input.value );
|
|
208
|
+
let json_out = o2j( data, null, 2 );
|
|
209
|
+
e_preview.value = json_out;
|
|
210
|
+
}, 500 );
|
|
211
|
+
|
|
212
|
+
</script>
|
|
213
|
+
</body>
|
|
214
|
+
</html>
|
package/scaffold/rpc/index.cjs
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
// The "input" object is exactly the same thing that was given to RPC()
|
|
4
|
-
// on the front end.
|
|
5
|
-
// Use okay() to return a normal response, or fail() to return an error.
|
|
6
|
-
// These correspond to the same okay()/fail() functions given to RPC() on
|
|
7
|
-
// the front end.
|
|
2
|
+
delete require.cache[ module.filename ];
|
|
8
3
|
|
|
9
|
-
module.exports = ( input, okay, fail ) => {
|
|
4
|
+
module.exports = ( input, okay, fail, context ) => {
|
|
10
5
|
|
|
11
6
|
const action = input.action;
|
|
12
7
|
|
|
13
8
|
if( action == "hello" ) {
|
|
14
|
-
return okay( "
|
|
9
|
+
return okay( { message: "Welcome!" } );
|
|
15
10
|
}
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
okay( { error: "Invalid action: "+action } );
|
|
18
13
|
|
|
19
14
|
};
|
|
20
15
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
1
2
|
<html>
|
|
2
3
|
<head>
|
|
3
4
|
<title>TLSD Test Page</title>
|
|
@@ -14,6 +15,10 @@
|
|
|
14
15
|
|
|
15
16
|
<h3 id=connect_status>...</h3>
|
|
16
17
|
|
|
18
|
+
<div id="drop_zone">
|
|
19
|
+
<h4>🎯 Drop files here to upload</h4>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
17
22
|
<script src="/rpc/rpc.js"></script>
|
|
18
23
|
|
|
19
24
|
<script>
|
|
@@ -23,26 +28,38 @@
|
|
|
23
28
|
// Websocket or REST based on the size of the JSON encoded msg.
|
|
24
29
|
// Otherwise, you can explicitly choose the transport by using
|
|
25
30
|
// RPC.POST() or RPC.WS().
|
|
26
|
-
|
|
27
|
-
RPC.onmessage = msg => {
|
|
28
|
-
// Message originating from server (not a response to client message).
|
|
29
|
-
// Returning true will prevent tlsd from doing anything further
|
|
30
|
-
// with the message.
|
|
31
|
-
return false;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
RPC.on_connect = function( evt ) {
|
|
31
|
+
RPC.connect( function( evt ) {
|
|
35
32
|
// RPC is ready to be used (websocket is connected)
|
|
36
|
-
RPC( { action: "hello" },
|
|
37
|
-
document.getElementById( "connect_status" ).innerText =
|
|
33
|
+
RPC( { action: "hello" }, response => {
|
|
34
|
+
document.getElementById( "connect_status" ).innerText = response.message;
|
|
38
35
|
} );
|
|
39
|
-
}
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
// Drag/drop upload
|
|
39
|
+
const dropZone = document.getElementById( "drop_zone" );
|
|
40
|
+
dropZone.addEventListener( 'dragover', ( e ) => e.preventDefault() );
|
|
41
|
+
dropZone.addEventListener( 'drop', function( e ) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
const file = e.dataTransfer.files[ 0 ];
|
|
44
|
+
const xhr = new XMLHttpRequest();
|
|
45
|
+
const uploadURL = '/';
|
|
46
|
+
xhr.open( 'PUT', uploadURL, true );
|
|
47
|
+
xhr.setRequestHeader( 'Content-Type', file.type || 'application/octet-stream' );
|
|
48
|
+
xhr.onload = () => {
|
|
49
|
+
if( xhr.status === 200 ) {
|
|
50
|
+
let result = JSON.parse( xhr.responseText );
|
|
51
|
+
alert( "Successfully uploaded file. Hash=" + result.hash );
|
|
52
|
+
} else {
|
|
53
|
+
alert( "Upload failed with status: " + xhr.status );
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
xhr.onerror = () => {
|
|
58
|
+
alert( "Upload failed" );
|
|
59
|
+
};
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// The RPC code will reconnect automatically.
|
|
44
|
-
document.getElementById( "connect_status" ).innerText = "[ DISCONNECTED ]";
|
|
45
|
-
}
|
|
61
|
+
xhr.send( file );
|
|
62
|
+
} );
|
|
46
63
|
|
|
47
64
|
</script>
|
|
48
65
|
|
package/tlsd
CHANGED
package/tlsd.js
CHANGED
|
@@ -2,30 +2,33 @@
|
|
|
2
2
|
// Copyright 2025 Sleepless Software Inc.
|
|
3
3
|
// All Rights Reserved
|
|
4
4
|
|
|
5
|
-
const { path, http,
|
|
5
|
+
const { path, http, fs, } = require( "allcore" );
|
|
6
6
|
|
|
7
7
|
const connect = require( "connect" );
|
|
8
8
|
const websocket = require( "websocket" );
|
|
9
|
-
const serveStatic = require( "serve-static" )
|
|
9
|
+
const serveStatic = require( "serve-static" );
|
|
10
10
|
|
|
11
11
|
const body_parser = require( "body-parser" ).json( { limit: "10mb" } );
|
|
12
12
|
const compression = require( "compression" )();
|
|
13
13
|
const cors = require( "cors" )();
|
|
14
14
|
const queryString = require( "querystring" );
|
|
15
15
|
|
|
16
|
-
const { log5, o2j, j2o, toInt, is_dir } = require( "sleepless" );
|
|
16
|
+
const { log5, o2j, j2o, toInt, is_dir, sha1 } = require( "sleepless" );
|
|
17
17
|
const L = log5.mkLog( "TLSD: " );
|
|
18
18
|
const { D, V, I, W, E } = L;
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
const UPLOAD_DIR = process.env.UPLOAD_DIR || "/tmp/tlsd-put-files/";
|
|
22
|
+
const UPLOAD_MAX_BYTES = toInt( process.env.UPLOAD_MAX_BYTES ) || ( 200 * 1024 * 1024 );
|
|
23
|
+
|
|
21
24
|
let dev_mode = false;
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
function usage() {
|
|
25
28
|
const base = path.basename( module.filename );
|
|
26
|
-
|
|
29
|
+
I(
|
|
27
30
|
`Usage:
|
|
28
|
-
TLS mode:
|
|
31
|
+
Production/TLS mode:
|
|
29
32
|
DOMAINS_ROOT=./my_domains MAINTAINER_EMAIL=foo@bar.com VERBOSITY=4 node ` + base + `
|
|
30
33
|
Dev mode:
|
|
31
34
|
node ` + base + ` site_root listen_port verbosity
|
|
@@ -42,13 +45,17 @@ function next_seq() {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
// Handles incoming RPC
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
48
|
+
// Handles incoming RPC messages.
|
|
49
|
+
// Load order for RPC handlers under the provided root:
|
|
50
|
+
// 1) Tries explicit CommonJS module at "root/rpc/index.cjs"
|
|
51
|
+
// 2) Falls back to legacy directory loader at "root/rpc"
|
|
52
|
+
// If loading succeeds, passes the message to the handler.
|
|
53
|
+
// context may be one of:
|
|
54
|
+
// { transport_type: "WS", connection: { ... } }
|
|
55
|
+
// { transport_type: "GET", connection: { req, res } }
|
|
56
|
+
// { transport_type: "POST", connection: { req, res } }
|
|
57
|
+
// Additionally, context.upload_dir is set to the server's upload directory.
|
|
58
|
+
function rpc_handler( root, msg, context, _okay, _fail ) {
|
|
52
59
|
|
|
53
60
|
V( "RPC " + o2j( msg ).abbr( 70 ) );
|
|
54
61
|
|
|
@@ -64,13 +71,14 @@ function rpc_handler( root, msg, transport, _okay, _fail ) {
|
|
|
64
71
|
_fail();
|
|
65
72
|
};
|
|
66
73
|
|
|
74
|
+
context.upload_dir = UPLOAD_DIR;
|
|
75
|
+
|
|
67
76
|
try {
|
|
68
77
|
// try loading explicit common js module
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const mod = require( path );
|
|
78
|
+
const mod_path = root + "/rpc/index.cjs";
|
|
79
|
+
const mod = require( mod_path );
|
|
72
80
|
try {
|
|
73
|
-
|
|
81
|
+
mod( msg, okay, fail, context );
|
|
74
82
|
} catch( err ) {
|
|
75
83
|
fail( err );
|
|
76
84
|
}
|
|
@@ -79,11 +87,10 @@ function rpc_handler( root, msg, transport, _okay, _fail ) {
|
|
|
79
87
|
|
|
80
88
|
// try loading it the old way using the dir
|
|
81
89
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const mod = require( path );
|
|
90
|
+
const mod_path = root + "/rpc";
|
|
91
|
+
const mod = require( mod_path );
|
|
85
92
|
try {
|
|
86
|
-
|
|
93
|
+
mod( msg, okay, fail, context );
|
|
87
94
|
} catch( err ) {
|
|
88
95
|
fail( err );
|
|
89
96
|
}
|
|
@@ -99,7 +106,7 @@ function rpc_handler( root, msg, transport, _okay, _fail ) {
|
|
|
99
106
|
// Glue function to call rpc handler module and then return response
|
|
100
107
|
// via the websockets msg object
|
|
101
108
|
function ws_msg_handler( root, msg, host, connection ) {
|
|
102
|
-
rpc_handler( root, msg.msg, {
|
|
109
|
+
rpc_handler( root, msg.msg, { transport_type: "WS", host, connection }, data => {
|
|
103
110
|
msg.reply( data );
|
|
104
111
|
}, err => {
|
|
105
112
|
msg.error( err || "Unspecified Error" );
|
|
@@ -124,8 +131,9 @@ function logger( req, res, next ) {
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
|
|
127
|
-
// Creates and returns a handler
|
|
128
|
-
//
|
|
134
|
+
// Creates and returns a handler that intercepts and services requests to
|
|
135
|
+
// the "/rpc" endpoint. In TLS mode only POST is allowed; in dev mode
|
|
136
|
+
// GET is also allowed with query-string payloads.
|
|
129
137
|
function rpc_post( root ) {
|
|
130
138
|
return function( req, res, next ) {
|
|
131
139
|
let { method, url, query, body } = req;
|
|
@@ -133,7 +141,7 @@ function rpc_post( root ) {
|
|
|
133
141
|
url = url.split( "?" ).shift();
|
|
134
142
|
if( url != "/rpc" && url != "/rpc/" ) {
|
|
135
143
|
next();
|
|
136
|
-
return
|
|
144
|
+
return;
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
// Only allow POSTS in TLS mode.
|
|
@@ -152,7 +160,7 @@ function rpc_post( root ) {
|
|
|
152
160
|
D( method + " >------> " + o2j( input, null, 2 ) );
|
|
153
161
|
|
|
154
162
|
// Summon the rpc handler for the domain root.
|
|
155
|
-
rpc_handler( root, input, {
|
|
163
|
+
rpc_handler( root, input, { transport_type: method, connection: { host: req.headers[ "host" ], req, res, } } , output => {
|
|
156
164
|
res.writeHead( 200, {
|
|
157
165
|
"Content-Type": "application/json",
|
|
158
166
|
"Cache-Control": "no-store",
|
|
@@ -183,8 +191,8 @@ function not_found( root ) {
|
|
|
183
191
|
return function( req, res, next ) {
|
|
184
192
|
// Can't just do a redirect here because if /404 doesn't exist, it will
|
|
185
193
|
// just go into a redirect loop. So test to see if the dir exists first.
|
|
186
|
-
let
|
|
187
|
-
is_dir(
|
|
194
|
+
let mod_path = root + "/static/404";
|
|
195
|
+
is_dir( mod_path, so => {
|
|
188
196
|
if( so ) {
|
|
189
197
|
let p404 = "/404/?original_path=" + encodeURIComponent( req.url );
|
|
190
198
|
res.writeHead( 302, { "Location": p404, } ); // redirect to path
|
|
@@ -196,6 +204,87 @@ function not_found( root ) {
|
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
|
|
207
|
+
function put_handler( req, res, next ) {
|
|
208
|
+
|
|
209
|
+
if( req.method != "PUT" ) {
|
|
210
|
+
next();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fail( error, stack ) {
|
|
215
|
+
E( error, stack );
|
|
216
|
+
res.writeHead( 500, { "Content-Type": "application/json" } );
|
|
217
|
+
res.write( o2j( { error } ) );
|
|
218
|
+
res.end();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
I( "PUT " + req.url );
|
|
223
|
+
|
|
224
|
+
// Preflight size check from Content-Length if provided
|
|
225
|
+
const content_length = toInt( req.headers[ "content-length" ] );
|
|
226
|
+
if( content_length && content_length > UPLOAD_MAX_BYTES ) {
|
|
227
|
+
W( "PUT: Content-Length " + content_length + " exceeds cap " + UPLOAD_MAX_BYTES );
|
|
228
|
+
res.writeHead( 413, { "Content-Type": "application/json" } );
|
|
229
|
+
res.write( o2j( { error: "Payload too large" } ) );
|
|
230
|
+
res.end();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let local_path = UPLOAD_DIR;
|
|
235
|
+
fs.mkdirSync( local_path, { recursive: true } );
|
|
236
|
+
|
|
237
|
+
// generate random hash to store file under locally
|
|
238
|
+
const hash = sha1( "" + ( Date.now() + Math.random() ) );
|
|
239
|
+
local_path += "/" + hash;
|
|
240
|
+
|
|
241
|
+
D( "PUT: " + local_path );
|
|
242
|
+
|
|
243
|
+
const writeStream = fs.createWriteStream( local_path );
|
|
244
|
+
|
|
245
|
+
let responded = false;
|
|
246
|
+
let received_bytes = 0;
|
|
247
|
+
const abort_too_large = function( ) {
|
|
248
|
+
if( responded ) return;
|
|
249
|
+
responded = true;
|
|
250
|
+
try { req.unpipe( writeStream ); } catch( _e ) {}
|
|
251
|
+
try { writeStream.destroy(); } catch( _e ) {}
|
|
252
|
+
try { req.destroy(); } catch( _e ) {}
|
|
253
|
+
try { fs.unlink( local_path, function( ){} ); } catch( _e ) {}
|
|
254
|
+
res.writeHead( 413, { "Content-Type": "application/json" } );
|
|
255
|
+
res.write( o2j( { error: "Payload too large" } ) );
|
|
256
|
+
res.end();
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Streaming size guard
|
|
260
|
+
req.on( "data", function( chunk ) {
|
|
261
|
+
received_bytes += chunk.length;
|
|
262
|
+
if( received_bytes > UPLOAD_MAX_BYTES ) {
|
|
263
|
+
W( "PUT: stream exceeded cap: " + received_bytes + " > " + UPLOAD_MAX_BYTES );
|
|
264
|
+
abort_too_large( );
|
|
265
|
+
}
|
|
266
|
+
} );
|
|
267
|
+
|
|
268
|
+
req.pipe( writeStream );
|
|
269
|
+
|
|
270
|
+
writeStream.on( "finish", ( ) => {
|
|
271
|
+
if( responded ) return;
|
|
272
|
+
I( "PUT: " + local_path );
|
|
273
|
+
res.writeHead( 200, { "Content-Type": "application/json" } );
|
|
274
|
+
res.write( o2j( { hash } ) );
|
|
275
|
+
res.end();
|
|
276
|
+
} );
|
|
277
|
+
|
|
278
|
+
writeStream.on( "error", ( error ) => {
|
|
279
|
+
if( responded ) return;
|
|
280
|
+
fail( "PUT: " + local_path + " failed during stream", error.stack );
|
|
281
|
+
} );
|
|
282
|
+
} catch ( error ) {
|
|
283
|
+
fail( "PUT: failed", error.stack );
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
}
|
|
287
|
+
|
|
199
288
|
|
|
200
289
|
// Create and return a connect app/function that implements
|
|
201
290
|
// default (basic) functionality.
|
|
@@ -206,6 +295,7 @@ function basic_handler( root ) {
|
|
|
206
295
|
app.use( cors ); // allow requests from other domains
|
|
207
296
|
app.use( populate_query );
|
|
208
297
|
app.use( logger );
|
|
298
|
+
app.use( put_handler )
|
|
209
299
|
app.use( rpc_post( root ) );
|
|
210
300
|
app.use( rpc_static );
|
|
211
301
|
app.use( serveStatic( root + "/static" ) ); // static files for domain
|
|
@@ -220,7 +310,8 @@ const cached_basic_handlers = {};
|
|
|
220
310
|
// Handle REST calls (as opposed to websocket messages)
|
|
221
311
|
function rest_handler( root, req, rsp ) {
|
|
222
312
|
|
|
223
|
-
|
|
313
|
+
const remote_ip = req.socket.remoteAddress;
|
|
314
|
+
I( remote_ip + " " + req.headers[ "host" ] + ": " + req.method + " " + req.url );
|
|
224
315
|
D( "rest_handler root: " + root );
|
|
225
316
|
|
|
226
317
|
const call = function( handler ) {
|
|
@@ -237,14 +328,15 @@ function rest_handler( root, req, rsp ) {
|
|
|
237
328
|
}
|
|
238
329
|
|
|
239
330
|
// try loading custom handler first
|
|
331
|
+
let handler;
|
|
240
332
|
try {
|
|
241
333
|
handler = require( root );
|
|
242
|
-
D( "Using custom REST handler loaded from "+root );
|
|
334
|
+
D( "Using custom REST handler loaded from " + root );
|
|
243
335
|
call( handler );
|
|
244
336
|
} catch( err ) {
|
|
245
337
|
// attempt to load custom handler threw an exception
|
|
246
338
|
|
|
247
|
-
|
|
339
|
+
handler = cached_basic_handlers[ root ];
|
|
248
340
|
if( ! handler ) {
|
|
249
341
|
handler = basic_handler( root );
|
|
250
342
|
cached_basic_handlers[ root ] = handler;
|
|
@@ -267,11 +359,12 @@ function ws_attach( server, msg_handler ) {
|
|
|
267
359
|
|
|
268
360
|
const host = wsreq.httpRequest.headers[ "host" ];
|
|
269
361
|
|
|
270
|
-
const socket = wsreq.accept( null, wsreq.origin );
|
|
362
|
+
const socket = wsreq.accept( null, wsreq.origin || "*" );
|
|
271
363
|
|
|
272
364
|
const name = "ws-conn-" + next_seq(); // XXX just use the websocket id
|
|
273
365
|
|
|
274
|
-
//
|
|
366
|
+
// Send a message to the connected client.
|
|
367
|
+
// XXX "cb" is currently unused - not sure it should be that way
|
|
275
368
|
const send = function( msg, cb ) {
|
|
276
369
|
if( msg.msg_id === undefined ) {
|
|
277
370
|
msg.msg_id = "msg-id-" + next_seq(); // every message must have an id
|
|
@@ -336,7 +429,7 @@ if( argv.length == 2 ) {
|
|
|
336
429
|
|
|
337
430
|
VERBOSITY = toInt( VERBOSITY );
|
|
338
431
|
|
|
339
|
-
if( DOMAINS_ROOT && MAINTAINER_EMAIL && VERBOSITY ) {
|
|
432
|
+
if( DOMAINS_ROOT && MAINTAINER_EMAIL && VERBOSITY >= 0 ) {
|
|
340
433
|
|
|
341
434
|
L( toInt( VERBOSITY ) );
|
|
342
435
|
|
|
@@ -374,14 +467,14 @@ if( argv.length == 2 ) {
|
|
|
374
467
|
else
|
|
375
468
|
if( argv.length == 5 ) {
|
|
376
469
|
|
|
377
|
-
let [
|
|
470
|
+
let [ _exe, _script, SITE_ROOT, PORT, VERBOSITY, ] = argv;
|
|
378
471
|
|
|
379
472
|
VERBOSITY = toInt( VERBOSITY );
|
|
380
473
|
PORT = toInt( PORT );
|
|
381
474
|
|
|
382
475
|
dev_mode = true;
|
|
383
476
|
|
|
384
|
-
if( SITE_ROOT && PORT && VERBOSITY ) {
|
|
477
|
+
if( SITE_ROOT && PORT && VERBOSITY >= 0 ) {
|
|
385
478
|
|
|
386
479
|
L( toInt( VERBOSITY ) );
|
|
387
480
|
|
|
@@ -415,7 +508,7 @@ if( argv.length == 3 && argv[ 2 ] == "-v" ) {
|
|
|
415
508
|
|
|
416
509
|
// pull package version from package.json and print it
|
|
417
510
|
const p = require( __dirname + "/package.json" );
|
|
418
|
-
|
|
511
|
+
I( p.version );
|
|
419
512
|
|
|
420
513
|
}
|
|
421
514
|
else {
|