smc-hub
Version:
CoCalc: Backend webserver component
132 lines (111 loc) • 4.76 kB
text/coffeescript
#########################################################################
# This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
# License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
#########################################################################
# sage.coffee -- TCP interface between NodeJS and a Sage server instance
#
# The TCP interface to the sage server is necessarily "weird" because
# the Sage process that is actually running the code *is* the server
# one talks to via TCP after starting a session. Since Sage itself is
# blocking when running code, and running as the user when running
# code can't be trusted, e.g., anything in the server can be
# arbitrarily modified, all *control* messages, e.g., sending signals,
# cleaning up, etc. absolutely require making a separate TCP connection.
#
# So:
#
# 1. Make a connection to the TCP server, which runs as root and
# forks on connection.
#
# 2. Create a new session, which drops privileges to a random clean
# user, and continues to listen on the TCP socket when not doing
# computations.
#
# 3. Send request-to-exec, etc., messages to the socket in (2)
# and get back output over (2).
#
# 4. To send a signal, get files, save worksheet state, etc.,
# make a new connection to the TCP server, and send a message
# in the freshly forked off process, which runs as root.
#
# With this architecture, the sage process that the user is
# interacting with has ultimate control over the messages it sends and
# receives (which is incredibly powerful and customizable), with no
# stupid pexpect or anything else like that to get in the way. At the
# same time, we still have a root out-of-process control mechanism,
# though with the overhead of establishing a new connection each time.
# Since control messages are much less frequent, this overhead is
# acceptable.
#
net = require('net')
winston = require('./logger').getLogger('sage')
message = require("smc-util/message")
misc = require('smc-util/misc')
{defaults, required} = misc
{connect_to_locked_socket, enable_mesg} = require('smc-util-node/misc_node')
exports.send_control_message = (opts) ->
opts = defaults opts,
host : 'localhost'
port : required
secret_token : required
mesg : required
sage_control_conn = new exports.Connection
secret_token : opts.secret_token
host : opts.host
port : opts.port
cb : ->
sage_control_conn.send_json(opts.mesg)
sage_control_conn.close()
exports.send_signal = (opts) ->
opts = defaults opts,
host : 'localhost'
port : required
secret_token : required
pid : required
signal : required
exports.send_control_message
host : opts.host
port : opts.port
secret_token : opts.secret_token
mesg : message.send_signal(pid:opts.pid, signal:opts.signal)
class exports.Connection
constructor: (options) ->
options = defaults options,
secret_token : required
port : required
host : 'localhost' # should always be there, since we use port forwarding for security
recv : undefined
cb : undefined
@host = options.host
@port = options.port
connect_to_locked_socket
port : @port
token : options.secret_token
cb : (err, _conn) =>
if err
options.cb(err)
return
if not _conn
options.cb("unable to connect to a locked socket")
return
@conn = _conn
@recv = options.recv # send message to client
@buf = null
@buf_target_length = -1
@conn.on 'error', (err) =>
winston.error("sage connection error: #{err}")
@recv?('json', message.terminate_session(reason:"#{err}"))
enable_mesg(@conn, 'connection to a sage server')
@conn.on 'mesg', (type, data) =>
@recv?(type, data)
options.cb()
send_json: (mesg) ->
@conn?.write_mesg('json', mesg)
send_blob: (uuid, blob) ->
@conn?.write_mesg('blob', {uuid:uuid, blob:blob})
# Close the connection with the server. You probably instead want
# to send_signal(...) using the module-level function to kill the
# session, in most cases, since this will leave the Sage process running.
close: () ->
@conn?.end()
@conn?.destroy()