###############################################################################1#2# CoCalc: Collaborative Calculation in the Cloud3#4# Copyright (C) 2016, Sagemath Inc.5#6# This program is free software: you can redistribute it and/or modify7# it under the terms of the GNU General Public License as published by8# the Free Software Foundation, either version 3 of the License, or9# (at your option) any later version.10#11# This program is distributed in the hope that it will be useful,12# but WITHOUT ANY WARRANTY; without even the implied warranty of13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# GNU General Public License for more details.15#16# You should have received a copy of the GNU General Public License17# along with this program. If not, see <http://www.gnu.org/licenses/>.18#19###############################################################################202122############################################################################23#24# sage.coffee -- TCP interface between NodeJS and a Sage server instance25#26# The TCP interface to the sage server is necessarily "weird" because27# the Sage process that is actually running the code *is* the server28# one talks to via TCP after starting a session. Since Sage itself is29# blocking when running code, and running as the user when running30# code can't be trusted, e.g., anything in the server can be31# arbitrarily modified, all *control* messages, e.g., sending signals,32# cleaning up, etc. absolutely require making a separate TCP connection.33#34# So:35#36# 1. Make a connection to the TCP server, which runs as root and37# forks on connection.38#39# 2. Create a new session, which drops privileges to a random clean40# user, and continues to listen on the TCP socket when not doing41# computations.42#43# 3. Send request-to-exec, etc., messages to the socket in (2)44# and get back output over (2).45#46# 4. To send a signal, get files, save worksheet state, etc.,47# make a new connection to the TCP server, and send a message48# in the freshly forked off process, which runs as root.49#50# With this architecture, the sage process that the user is51# interacting with has ultimate control over the messages it sends and52# receives (which is incredibly powerful and customizable), with no53# stupid pexpect or anything else like that to get in the way. At the54# same time, we still have a root out-of-process control mechanism,55# though with the overhead of establishing a new connection each time.56# Since control messages are much less frequent, this overhead is57# acceptable.58#59############################################################################6061net = require('net')62winston = require('winston') # https://github.com/flatiron/winston6364message = require("smc-util/message")65misc = require('smc-util/misc')66{defaults, required} = misc6768{connect_to_locked_socket, enable_mesg} = require('smc-util-node/misc_node')6970exports.send_control_message = (opts) ->71opts = defaults opts,72host : 'localhost'73port : required74secret_token : required75mesg : required7677sage_control_conn = new exports.Connection78secret_token : opts.secret_token79host : opts.host80port : opts.port81cb : ->82sage_control_conn.send_json(opts.mesg)83sage_control_conn.close()8485exports.send_signal = (opts) ->86opts = defaults opts,87host : 'localhost'88port : required89secret_token : required90pid : required91signal : required9293exports.send_control_message94host : opts.host95port : opts.port96secret_token : opts.secret_token97mesg : message.send_signal(pid:opts.pid, signal:opts.signal)9899100class exports.Connection101constructor: (options) ->102options = defaults options,103secret_token : required104port : required105host : 'localhost' # should always be there, since we use port forwarding for security106recv : undefined107cb : undefined108@host = options.host109@port = options.port110111connect_to_locked_socket112port : @port113token : options.secret_token114cb : (err, _conn) =>115if err116options.cb(err)117return118119if not _conn120options.cb("unable to connect to a locked socket")121return122123@conn = _conn124125@recv = options.recv # send message to client126@buf = null127@buf_target_length = -1128129@conn.on 'error', (err) =>130winston.error("sage connection error: #{err}")131@recv?('json', message.terminate_session(reason:"#{err}"))132133enable_mesg(@conn, 'connection to a sage server')134@conn.on 'mesg', (type, data) =>135@recv?(type, data)136137options.cb()138139send_json: (mesg) ->140@conn?.write_mesg('json', mesg)141142send_blob: (uuid, blob) ->143@conn?.write_mesg('blob', {uuid:uuid, blob:blob})144145# Close the connection with the server. You probably instead want146# to send_signal(...) using the module-level function to kill the147# session, in most cases, since this will leave the Sage process running.148close: () ->149@conn?.end()150@conn?.destroy()151152153