1
1
#!/usr/bin/env node
2
- // title : clipboard-server
3
- // description : Forward clipboard access over a http socket
4
- // author : Wei Kin Huang
5
- // date : 2022-05-28
6
- // version : 1.0.0
7
- // usage : clipboard-server start|stop|server [--pidfile FILE] [--socket FILE|PORT]
8
- // requires : node pbcopy pbpaste
9
- // ===============================================================================
10
- // ssh -o StreamLocalBindMask=0277 -R /tmp/clipboard-server.sock:/tmp/clipboard-server.sock user@HOST
11
- // if the remote host doesn't have StreamLocalBindUnlink=yes in sshd_config, use a port
12
- // ssh -R 127.0.0.1:29009:/tmp/clipboard-server.sock user@HOST
13
- // ssh -o StreamLocalBindMask=0077 -o StreamLocalBindUnlink=yes -R 127.0.0.1:3306:/tmp/clipboard-server.sock [email protected] -vv
2
+ /**
3
+ * title : clipboard-server
4
+ * description : Forward clipboard access over a http socket
5
+ * author : Wei Kin Huang
6
+ * date : 2022-05-28
7
+ * version : 1.0.0
8
+ * requires : node pbcopy pbpaste
9
+ * =============================================================================
10
+ * Usage: clipboard-server COMMAND [OPTION]...
11
+ * Forward clipboard access over a http socket.
12
+ * Example: clipboard-server start --disable-paste
13
+ *
14
+ * Commands:
15
+ * start Run the server in the background
16
+ * stop Stop the server if it is running
17
+ * restart Restart the server
18
+ * server Start the server in the foreground
19
+ *
20
+ * Options:
21
+ * -d, --disable-paste Disable local clipboard access, only alloys the remote
22
+ * server to send contents to local clipboard.
23
+ * default: false
24
+ * -p, --pidfile FILE Path to the pid file for the server.
25
+ * default: ~/.config/clipboard-server/clipboard-server.pid
26
+ * -s, --socket FILE|PORT A port number or path to the socket file location
27
+ * default: ~/.config/clipboard-server/clipboard-server.sock
28
+ *
29
+ * Using over SSH
30
+ *
31
+ * Via binding a socket on the remote host
32
+ * ssh -o StreamLocalBindMask=0177 -R /tmp/clipboard-server.sock:$HOME/.config/clipboard-server/clipboard-server.sock user@HOST
33
+ * If the remote host doesn't have StreamLocalBindUnlink=yes in sshd_config, it will not clean up the socket file, in
34
+ * this case use a port instead
35
+ *
36
+ * Via binding a port on the remote host
37
+ * ssh -R 127.0.0.1:29009:$HOME/.config/clipboard-server/clipboard-server.sock user@HOST
38
+ *
39
+ * This server is integrated with the dotfiles provided pbcopy/pbpaste commands when used over ssh, additionally a env
40
+ * var must be exported on the remote host to make use of this server:
41
+ *
42
+ * export CLIPBOARD_SERVER_PORT=29009 # the remote bound port
43
+ * or if using a socket (the default is /tmp/clipboard-server.sock if not set)
44
+ * export CLIPBOARD_SERVER_SOCK=/tmp/clipboard-server.sock
45
+ *
46
+ * Testing
47
+ *
48
+ * To read from the clipboard
49
+ * curl -sSLi --unix-socket ~/.config/clipboard-server/clipboard-server.sock -X GET http://localhost/clipboard
50
+ *
51
+ * To write to the clipboard
52
+ * date | curl -i -sSL --unix-socket ~/.config/clipboard-server/clipboard-server.sock http://localhost/clipboard --data-binary @-
53
+ */
14
54
15
55
const http = require ( "http" ) ;
16
56
const childProcess = require ( "child_process" ) ;
17
57
const fs = require ( "fs/promises" ) ;
58
+ const os = require ( "os" ) ;
59
+ const path = require ( "path" ) ;
18
60
const { unlinkSync } = require ( "fs" ) ;
19
61
20
62
// set umask for files to be 0600
@@ -37,7 +79,6 @@ async function setClipboard(req, res) {
37
79
} ) ;
38
80
}
39
81
40
- // @TODO : option to disable forwarding clipboard contents to remote
41
82
async function getClipboard ( res ) {
42
83
return await new Promise ( ( resolve , reject ) => {
43
84
const subprocess = childProcess . spawn ( "pbpaste" , {
@@ -54,6 +95,17 @@ async function getClipboard(res) {
54
95
} ) ;
55
96
}
56
97
98
+ async function createBaseDir ( filepath ) {
99
+ const dirname = path . dirname ( filepath ) ;
100
+ try {
101
+ await fs . stat ( dirname ) ;
102
+ return ;
103
+ } catch { }
104
+ const mask = process . umask ( 0o0077 ) ;
105
+ await fs . mkdir ( dirname , { recursive : true , mode : 0o700 } ) ;
106
+ process . umask ( mask ) ;
107
+ }
108
+
57
109
async function getPidFromPidFile ( pidFile ) {
58
110
try {
59
111
const pid = parseInt ( await ( await fs . readFile ( pidFile , "utf8" ) ) . trim ( ) , 10 ) ;
@@ -107,7 +159,7 @@ async function isListening(socket) {
107
159
} ) ;
108
160
}
109
161
110
- async function start ( { pidFile, socket } ) {
162
+ async function start ( { pidFile, socket, disablePaste } ) {
111
163
// check if pid file exists
112
164
const existingPid = await getPidFromPidFile ( pidFile ) ;
113
165
// check if already running
@@ -116,10 +168,15 @@ async function start({ pidFile, socket }) {
116
168
return ;
117
169
}
118
170
171
+ const args = [ "--pidfile" , pidFile , "--socket" , socket ] ;
172
+ if ( disablePaste ) {
173
+ args . push ( "--disable-paste" ) ;
174
+ }
175
+
119
176
// spawn detached process
120
177
const subprocess = childProcess . spawn (
121
178
process . argv [ 0 ] ,
122
- [ process . argv [ 1 ] , "server" , "--pidfile" , pidFile , "--socket" , socket ] ,
179
+ [ process . argv [ 1 ] , "server" , ... args ] ,
123
180
{
124
181
detached : true ,
125
182
stdio : "ignore" ,
@@ -150,6 +207,7 @@ async function start({ pidFile, socket }) {
150
207
151
208
// write child pid to file
152
209
// console.log(subprocess.pid);
210
+ await createBaseDir ( pidFile ) ;
153
211
await fs . writeFile ( pidFile , String ( subprocess . pid ) , "utf8" ) ;
154
212
}
155
213
@@ -172,30 +230,36 @@ async function stop({ pidFile }) {
172
230
} catch { }
173
231
}
174
232
175
- async function server ( { socket } ) {
176
- // cleanup
177
- process . on ( "SIGHUP" , ( ) => {
178
- try {
179
- if ( socketIsFile ( socket ) ) {
233
+ async function server ( { socket, disablePaste } ) {
234
+ if ( socketIsFile ( socket ) ) {
235
+ await createBaseDir ( socket ) ;
236
+ // cleanup
237
+ process . on ( "SIGHUP" , ( ) => {
238
+ try {
180
239
unlinkSync ( socket ) ;
181
- }
182
- } catch { }
183
- process . exit ( ) ;
184
- } ) ;
240
+ } catch { }
241
+ process . exit ( ) ;
242
+ } ) ;
243
+ }
185
244
186
- // ============================ plaintext h1 server
187
- const h1Server = http . createServer ( { } ) ;
188
- h1Server . on ( "error" , ( err ) => console . error ( err ) ) ;
189
- h1Server . on ( "connection" , ( ) => {
245
+ const server = http . createServer ( { } ) ;
246
+ server . on ( "error" , ( err ) => console . error ( err ) ) ;
247
+ server . on ( "connection" , ( ) => {
190
248
// connectLogger('connect: h1');
191
249
} ) ;
192
- h1Server . on ( "request" , async ( req , res ) => {
250
+ server . on ( "request" , async ( req , res ) => {
193
251
try {
194
252
switch ( req . url ) {
195
253
case "/clipboard" :
196
254
if ( req . method === "GET" ) {
197
255
res . setHeader ( "content-type" , "application/octet-stream" ) ;
198
- await getClipboard ( res ) ;
256
+ if ( ! disablePaste ) {
257
+ await getClipboard ( res ) ;
258
+ } else {
259
+ res . statusMessage = "Forbidden" ;
260
+ res . statusCode = 403 ;
261
+ res . end ( ) ;
262
+ }
199
263
} else if ( req . method === "POST" ) {
200
264
res . setHeader ( "content-type" , "text/plain" ) ;
201
265
res . statusCode = 200 ;
@@ -206,29 +270,63 @@ async function server({ socket }) {
206
270
}
207
271
break ;
208
272
case "/ping" :
273
+ res . setHeader ( "content-type" , "text/plain" ) ;
209
274
res . statusCode = 200 ;
210
275
res . end ( "ok\n" ) ;
211
276
break ;
212
277
default :
213
- res . statusCode = 400 ;
278
+ res . statusMessage = "Not Found" ;
279
+ res . statusCode = 404 ;
214
280
res . end ( ) ;
215
281
break ;
216
282
}
217
283
} catch ( e ) {
284
+ res . statusMessage = "Forbidden" ;
218
285
res . statusCode = 500 ;
219
286
res . end ( ) ;
220
287
}
221
288
} ) ;
222
- h1Server . listen ( socket , ( ) => console . log ( "ready" ) ) ;
289
+ server . listen ( socket , ( ) => console . log ( "ready" ) ) ;
290
+ }
291
+
292
+ function help ( ) {
293
+ return `
294
+ Usage: clipboard-server COMMAND [OPTION]...
295
+ Forward clipboard access over a http socket.
296
+ Example: clipboard-server start --disable-paste
297
+
298
+ Commands:
299
+ start Run the server in the background
300
+ stop Stop the server if it is running
301
+ restart Restart the server
302
+ server Start the server in the foreground
303
+
304
+ Options:
305
+ -d, --disable-paste Disable local clipboard access, only alloys the remote
306
+ server to send contents to local clipboard.
307
+ default: false
308
+ -p, --pidfile FILE Path to the pid file for the server.
309
+ default: ~/.config/clipboard-server/clipboard-server.pid
310
+ -s, --socket FILE|PORT A port number or path to the socket file location
311
+ default: ~/.config/clipboard-server/clipboard-server.sock
312
+ --help Show this message
313
+ ` . trim ( ) ;
223
314
}
224
315
225
316
async function main ( [ _1 , _2 , subcommand , ...args ] ) {
317
+ const userInfo = os . userInfo ( ) ;
318
+ let showHelp = subcommand == "--help" ;
226
319
const opts = {
227
- pidFile : "/tmp/clipboard-server.pid" ,
228
- socket : "/tmp/clipboard-server.sock" ,
320
+ pidFile : `${ userInfo . homedir } /.config/clipboard-server/clipboard-server.pid` ,
321
+ socket : `${ userInfo . homedir } /.config/clipboard-server/clipboard-server.sock` ,
322
+ disablePaste : false ,
229
323
} ;
230
324
for ( let i = 0 ; i < args . length ; i ++ ) {
231
325
switch ( args [ i ] ) {
326
+ case "-d" :
327
+ case "--disable-paste" :
328
+ opts . disablePaste = true ;
329
+ break ;
232
330
case "-p" :
233
331
case "--pidfile" :
234
332
opts . pidFile = args [ ++ i ] ;
@@ -240,16 +338,30 @@ async function main([_1, _2, subcommand, ...args]) {
240
338
throw new Error ( "socket must be a file or a port." ) ;
241
339
}
242
340
break ;
341
+ case "--help" :
342
+ showHelp = true ;
343
+ break ;
243
344
}
244
345
}
245
346
347
+ if ( showHelp ) {
348
+ console . log ( help ( ) ) ;
349
+ return ;
350
+ }
351
+
246
352
switch ( subcommand ) {
247
353
case "start" :
248
354
await start ( opts ) ;
249
355
break ;
250
356
case "stop" :
251
357
await stop ( opts ) ;
252
358
break ;
359
+ case "restart" :
360
+ try {
361
+ await stop ( opts ) ;
362
+ } catch { }
363
+ await start ( opts ) ;
364
+ break ;
253
365
case "server" :
254
366
await server ( opts ) ;
255
367
break ;
0 commit comments