diff --git a/docker/compose/slycat-compose/requirements.txt b/docker/compose/slycat-compose/requirements.txt index 5106368e0..f3cd2efd6 100644 --- a/docker/compose/slycat-compose/requirements.txt +++ b/docker/compose/slycat-compose/requirements.txt @@ -11,3 +11,4 @@ routes scipy npTDMS pandas +pysmb diff --git a/docker/compose/slycat-compose/slycat-docker-compose-authenticated-config.ini b/docker/compose/slycat-compose/slycat-docker-compose-authenticated-config.ini index a47fcf198..73803fc9e 100644 --- a/docker/compose/slycat-compose/slycat-docker-compose-authenticated-config.ini +++ b/docker/compose/slycat-compose/slycat-docker-compose-authenticated-config.ini @@ -19,7 +19,7 @@ error-log-count: 100 error-log-size: 10000000 module-name: " " password-check: {"plugin": "slycat-identity-password-check"} -plugins: [ "plugins", "plugins/slycat-cca", "plugins/slycat-run-command", "plugins/slycat-column-wizard", "plugins/slycat-model-wizards", "plugins/slycat-parameter-image", "plugins/slycat-project-wizards", "plugins/slycat-remap-wizard", "plugins/slycat-timeseries-model", "plugins/slycat-video-swarm", "plugins/slycat-dac"] +plugins: [ "plugins", "plugins/slycat-smb", "plugins/slycat-cca", "plugins/slycat-run-command", "plugins/slycat-column-wizard", "plugins/slycat-model-wizards", "plugins/slycat-parameter-image", "plugins/slycat-project-wizards", "plugins/slycat-remap-wizard", "plugins/slycat-timeseries-model", "plugins/slycat-video-swarm", "plugins/slycat-dac"] projects-redirect: "/projects" remote-hosts: [{ "hostnames": ["localhost"], "agent.old": {"command":"/home/slycat/install/conda/bin/python /home/slycat/src/slycat/agent/slycat-docker-agent.py --json /home/slycat/src/slycat/agent/json"}}] remote-authentication: {"method":"password", "port":22, "localhost":"sshd"} diff --git a/packages/slycat/web/server/__init__.py b/packages/slycat/web/server/__init__.py index f84cfa1ec..27901c28c 100644 --- a/packages/slycat/web/server/__init__.py +++ b/packages/slycat/web/server/__init__.py @@ -764,14 +764,8 @@ def decode_username_and_password(): # cherrypy.log.error("decoding username and password") user_name = str(base64_decode(cherrypy.request.json["user_name"]).decode()) password = str(base64_decode(cherrypy.request.json["password"]).decode()) - - # try and get the redirect path for after successful login - try: - location = cherrypy.request.json["location"] - except Exception as e: - location = None - # cherrypy.log.error("no location provided moving on") except Exception as e: + cherrypy.log.error(str(e)) # cherrypy.log.error("username and password could not be decoded") cherrypy.log.error("slycat-standard-authentication.py authenticate", "cherrypy.HTTPError 400") raise cherrypy.HTTPError(400) diff --git a/packages/slycat/web/server/engine.py b/packages/slycat/web/server/engine.py index 902742dae..e9ebaaf1e 100644 --- a/packages/slycat/web/server/engine.py +++ b/packages/slycat/web/server/engine.py @@ -140,9 +140,11 @@ def abspath(path): dispatcher.connect("post-log", "/log", slycat.web.server.handlers.post_log, conditions={"method" : ["POST"]}) dispatcher.connect("post-projects", "/projects", slycat.web.server.handlers.post_projects, conditions={"method" : ["POST"]}) - #TODO: scrub sid dispatcher.connect("post-remote-browse", "/remotes/:hostname/browse{path:.*}", slycat.web.server.handlers.post_remote_browse, conditions={"method" : ["POST"]}) dispatcher.connect("post-remotes", "/remotes", slycat.web.server.handlers.post_remotes, conditions={"method" : ["POST"]}) + dispatcher.connect("post-remotes-smb", "/remotes/smb", slycat.web.server.handlers.post_remotes_smb, conditions={"method" : ["POST"]}) + dispatcher.connect("post-smb-browse", "/smb/remotes/:hostname/browse{path:.*}", slycat.web.server.handlers.post_smb_browse, conditions={"method" : ["POST"]}) + dispatcher.connect("put-model-arrayset-array", "/models/:mid/arraysets/:aid/arrays/:array", slycat.web.server.handlers.put_model_arrayset_array, conditions={"method" : ["PUT"]}) diff --git a/packages/slycat/web/server/handlers.py b/packages/slycat/web/server/handlers.py index 7181a4e82..9950eec64 100644 --- a/packages/slycat/web/server/handlers.py +++ b/packages/slycat/web/server/handlers.py @@ -25,6 +25,7 @@ import slycat.web.server.hdf5 import slycat.web.server.plugin import slycat.web.server.remote +import slycat.web.server.smb import slycat.web.server.streaming import slycat.web.server.upload import stat @@ -68,19 +69,26 @@ def get_sid(hostname): for index, host_session in enumerate(session["sessions"]): if host_session["hostname"] == hostname: sid = host_session["sid"] - if(not slycat.web.server.remote.check_session(sid)): + session_type = host_session["session_type"] + if(host_session["session_type"] == "ssh" and not slycat.web.server.remote.check_session(sid)): cherrypy.log.error("error %s SID:%s Keys %s" % (slycat.web.server.remote.check_session(sid), sid, list(slycat.web.server.remote.session_cache.keys()))) slycat.web.server.remote.delete_session(sid) del session["sessions"][index] database.save(session) raise cherrypy.HTTPError("404") + elif(host_session["session_type"] == "smb" and not slycat.web.server.smb.check_session(sid)): + cherrypy.log.error("error %s SID:%s Keys %s" % (slycat.web.server.smb.check_session(sid), sid, list(slycat.web.server.smb.session_cache.keys()))) + slycat.web.server.smb.delete_session(sid) + del session["sessions"][index] + database.save(session) + raise cherrypy.HTTPError("404") break except Exception as e: cherrypy.log.error("could not retrieve host session for remotes %s" % e) raise cherrypy.HTTPError("404") if sid is None: raise cherrypy.HTTPError("400 session is None value") - return sid + return sid, session_type def require_json_parameter(name): @@ -1012,14 +1020,18 @@ def put_upload_file_part(uid, fid, pid, file=None, hostname=None, path=None): data = file.file.read() elif file is None and hostname is not None and path is not None: - sid = get_sid(hostname) - with slycat.web.server.remote.get_session(sid) as session: - filename = "%s@%s:%s" % (session.username, session.hostname, path) - if stat.S_ISDIR(session.sftp.stat(path).st_mode): - cherrypy.log.error("slycat.web.server.handlers.py put_upload_file_part", - "cherrypy.HTTPError 400 cannot load directory %s." % filename) - raise cherrypy.HTTPError("400 Cannot load directory %s." % filename) - data = session.sftp.file(path).read() + sid, session_type = get_sid(hostname) + if session_type == "ssh": + with slycat.web.server.remote.get_session(sid) as session: + filename = "%s@%s:%s" % (session.username, session.hostname, path) + if stat.S_ISDIR(session.sftp.stat(path).st_mode): + cherrypy.log.error("slycat.web.server.handlers.py put_upload_file_part", + "cherrypy.HTTPError 400 cannot load directory %s." % filename) + raise cherrypy.HTTPError("400 Cannot load directory %s." % filename) + data = session.sftp.file(path).read() + elif session_type == "smb": + with slycat.web.server.smb.get_session(sid) as session: + data = session.get_file(path=path) else: cherrypy.log.error("slycat.web.server.handlers.py put_upload_file_part", "cherrypy.HTTPError 400 must supply file parameter, or sid and path parameters.") @@ -1208,7 +1220,7 @@ def clear_ssh_sessions(): sid = cherrypy.request.cookie["slycatauth"].value couchdb = slycat.web.server.database.couchdb.connect() session = couchdb.get("session", sid) - cherrypy.log.error("ssh sessions cleared for user session: %s" % session) + cherrypy.log.error("ssh and smb sessions cleared for user session: %s" % session) cherrypy.response.status = "200" if session is not None: for ssh_session in session["sessions"]: @@ -1899,18 +1911,16 @@ def validate_table_columns(columns): columns = [(int(spec[0]), int(spec[1]) if len(spec) == 2 else int(spec[0]) + 1) for spec in columns] columns = numpy.concatenate([numpy.arange(begin, end) for begin, end in columns]) columns = columns[columns >= 0] - return columns - except: + except Exception as e: + cherrypy.log.error(str(e)) cherrypy.log.error("slycat.web.server.handlers.py validate_table_columns", "cherrypy.HTTPError 400 malformed columns argument must be a comma separated collection of column indices or half-open index ranges.") raise cherrypy.HTTPError( "400 Malformed columns argument must be a comma separated collection of column indices or half-open index ranges.") - if numpy.any(columns < 0): cherrypy.log.error("slycat.web.server.handlers.py validate_table_columns", "cherrypy.HTTPError 400 column values must be non-negative.") raise cherrypy.HTTPError("400 Column values must be non-negative.") - return columns @@ -2473,13 +2483,73 @@ def post_remotes(): if("sid" in session["sessions"][i] and session["sessions"][i]["sid"] is not None): slycat.web.server.remote.delete_session(session["sessions"][i]["sid"]) del session["sessions"][i] - session["sessions"].append({"sid": sid, "hostname": hostname, "username": username}) + session["sessions"].append({"sid": sid, "hostname": hostname, "username": username, "session_type": "ssh"}) + database.save(session) + except Exception as e: + cherrypy.log.error("login could not save session for remotes %s" % e) + msg = "login could not save session for remote host" + return {"sid": sid, "status": True, "msg": msg} + + +@cherrypy.tools.json_in(on=True) +@cherrypy.tools.json_out(on=True) +def post_remotes_smb(): + """ + Given username, hostname, password as a json payload + establishes a session with the remote host and attaches + it to the users session + :return: {"sid":sid, "status":boolean, msg:""} + user_name password + + encode with in js + + b64EncodeUnicode(str) { + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { + return String.fromCharCode('0x' + p1); + })); + } + """ + # try and decode the username and password + username, password = slycat.web.server.decode_username_and_password() + server = cherrypy.request.json["server"] + share = cherrypy.request.json["share"] + cherrypy.log.error("username:%s server:%s share:%s" % (username, server , share)) + if username == None: + username = cherrypy.request.login + msg = "" + sid = slycat.web.server.smb.create_session(username,password,server,share) + ''' + save sid to user session + the session will be stored as follows in the users session + {sessions:[{{"sid": sid,"hostname": hostname, "username": username}},...]} + ''' + try: + database = slycat.web.server.database.couchdb.connect() + cherrypy.log.error("got the database") + session = database.get("session", cherrypy.request.cookie["slycatauth"].value) + for i in range(len(session["sessions"])): + if session["sessions"][i]["hostname"] == server: + if("sid" in session["sessions"][i] and session["sessions"][i]["sid"] is not None): + slycat.web.server.remote.delete_session(session["sessions"][i]["sid"]) + del session["sessions"][i] + cherrypy.log.error("adding session") + session["sessions"].append({"sid": sid, "hostname": server, "username": username, "session_type": "smb"}) + cherrypy.log.error("saving") database.save(session) + cherrypy.log.error("saved") except Exception as e: cherrypy.log.error("login could not save session for remotes %s" % e) msg = "login could not save session for remote host" return {"sid": sid, "status": True, "msg": msg} +@cherrypy.tools.json_in(on=True) +@cherrypy.tools.json_out(on=True) +def post_smb_browse(hostname, path): + cherrypy.log.error("path:%s hostname:%s" % (path, hostname)) + sid, session_type = get_sid(hostname) + cherrypy.log.error("sid:%s path:%s hostname:%s" % (sid, path, hostname)) + with slycat.web.server.smb.get_session(sid) as session: + return session.browse(path=path) @cherrypy.tools.json_out(on=True) def get_remotes(hostname): @@ -2496,12 +2566,16 @@ def get_remotes(hostname): session = database.get("session", cherrypy.request.cookie["slycatauth"].value) for h_session in session["sessions"]: if h_session["hostname"] == hostname: - if slycat.web.server.remote.check_session(h_session["sid"]): + if h_session["session_type"] == "ssh" and slycat.web.server.remote.check_session(h_session["sid"]): + status = True + msg = "hostname session was found" + elif h_session["session_type"] == "smb" and slycat.web.server.smb.check_session(h_session["sid"]): status = True msg = "hostname session was found" else: session["sessions"][:] = [tup for tup in session["sessions"] if tup["hostname"] != hostname] database.save(session) + except Exception as e: cherrypy.log.error("status could not save session for remotes %s" % e) return {"status": status, "msg": msg, "hostName": hostname} @@ -2539,7 +2613,7 @@ def delete_remote(sid): @cherrypy.tools.json_out(on=True) def get_session_status(hostname): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return "success" @@ -2547,7 +2621,7 @@ def get_session_status(hostname): @cherrypy.tools.json_in(on=True) @cherrypy.tools.json_out(on=True) def post_remote_launch(hostname): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) command = cherrypy.request.json["command"] with slycat.web.server.remote.get_session(sid) as session: return session.launch(command) @@ -2556,7 +2630,7 @@ def post_remote_launch(hostname): @cherrypy.tools.json_in(on=True) @cherrypy.tools.json_out(on=True) def post_submit_batch(hostname): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) filename = cherrypy.request.json["filename"] with slycat.web.server.remote.get_session(sid) as session: return session.submit_batch(filename) @@ -2564,28 +2638,28 @@ def post_submit_batch(hostname): @cherrypy.tools.json_out(on=True) def get_checkjob(hostname, jid): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return session.checkjob(jid) @cherrypy.tools.json_out(on=True) def delete_job(hostname, jid): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return session.cancel_job(jid) @cherrypy.tools.json_out(on=True) def get_job_output(hostname, jid, path): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return session.get_job_output(jid, path) @cherrypy.tools.json_out(on=True) def get_user_config(hostname): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return session.get_user_config() @@ -2615,7 +2689,7 @@ def job_time(nodes, tasks, size): @cherrypy.tools.json_out(on=True) def set_user_config(hostname): # TODO add user config mapping - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) config = cherrypy.request.json["config"] cherrypy.log.error("user_config %s" % config) with slycat.web.server.remote.get_session(sid) as session: @@ -2657,7 +2731,7 @@ def post_remote_command(hostname): type: string field type eg string, int... """ - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) command = cherrypy.request.json["command"] with slycat.web.server.remote.get_session(sid) as session: return session.run_remote_command(command) @@ -2665,7 +2739,7 @@ def post_remote_command(hostname): @cherrypy.tools.json_out(on=True) def get_remote_job_status(hostname, jid): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return session.get_remote_job_status(jid) @@ -2673,7 +2747,7 @@ def get_remote_job_status(hostname, jid): @cherrypy.tools.json_in(on=True) @cherrypy.tools.json_out(on=True) def post_remote_browse(hostname, path): - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) file_reject = re.compile( cherrypy.request.json.get("file-reject")) if "file-reject" in cherrypy.request.json else None file_allow = re.compile(cherrypy.request.json.get("file-allow")) if "file-allow" in cherrypy.request.json else None @@ -2727,9 +2801,20 @@ def get_remote_file(hostname, path, **kwargs): “Access denied” The session user doesn’t have permissions to access the file. """ - sid = get_sid(hostname) - with slycat.web.server.remote.get_session(sid) as session: - return session.get_file(path, **kwargs) + sid, session_type = get_sid(hostname) + if session_type == 'smb': + with slycat.web.server.smb.get_session(sid) as session: + split_list = path.split('/') + del split_list[1] + del split_list[0] + content_type, encoding = slycat.mime_type.guess_type(path) + if content_type is None: + content_type = "application/octet-stream" + cherrypy.response.headers["content-type"] = content_type + return session.get_file(path='/{0}'.format('/'.join(split_list))) + else: + with slycat.web.server.remote.get_session(sid) as session: + return session.get_file(path, **kwargs) def get_remote_image(hostname, path, **kwargs): @@ -2741,7 +2826,7 @@ def get_remote_image(hostname, path, **kwargs): :param kwargs: :return: image """ - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return session.get_image(path, **kwargs) @@ -2755,7 +2840,7 @@ def get_time_series_names(hostname, path, **kwargs): :param kwargs: :return: json object of column names """ - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: csv_file = str(session.get_file(path, **kwargs)) csv_file = csv_file.replace("\\r\\n","\r\n") @@ -2802,7 +2887,7 @@ def get_remote_video(hostname, vsid): :param vsid: video uuid :return: video """ - sid = get_sid(hostname) + sid, session_type = get_sid(hostname) with slycat.web.server.remote.get_session(sid) as session: return session.get_video(vsid) diff --git a/packages/slycat/web/server/remote.py b/packages/slycat/web/server/remote.py index 1ae33363f..408972a97 100644 --- a/packages/slycat/web/server/remote.py +++ b/packages/slycat/web/server/remote.py @@ -1113,17 +1113,9 @@ def check_session(sid): ------- boolean : """ - # client = cherrypy.request.headers.get("x-forwarded-for") - with session_cache_lock: _expire_session(sid) response = True - if sid in session_cache: - session = session_cache[sid] - # Only the originating client can access a session. - # if client != session.client: - # response = False - if sid not in session_cache: response = False if response: diff --git a/packages/slycat/web/server/smb.py b/packages/slycat/web/server/smb.py new file mode 100644 index 000000000..f51014e29 --- /dev/null +++ b/packages/slycat/web/server/smb.py @@ -0,0 +1,270 @@ +import tempfile +import threading +import time +import uuid +from smb.SMBConnection import SMBConnection +import socket +import logging +import datetime +import cherrypy +import slycat.mime_type +session_cache = {} +session_cache_lock = threading.Lock() + +class Smb(object): + """ + usage: + smb = Smb('user', 'password', 'server', 'share_name') + """ + def __init__(self, username, password, server, share, domain='', port=445): + # setup data + now = datetime.datetime.utcnow() + self.domain = str(domain) + self.username = str(username) + self.password = str(password) + self.client = socket.gethostname() + self.server = str(server) + self.server_ip = socket.gethostbyname(server) + self.share = str(share) + self.port = port + self.conn = None + self.connected = False + # SMB.SMBConnection logs too much + self._created = now + self._accessed = now + smb_logger = logging.getLogger('SMB.SMBConnection') + smb_logger.setLevel(logging.INFO) + self._lock = threading.Lock() + + def __enter__(self): + self._lock.__enter__() + return self + + def __exit__(self, exc_type, exc_value, traceback): + return self._lock.__exit__(exc_type, exc_value, traceback) + + @property + def accessed(self): + """Return the time the session was last accessed.""" + return self._accessed + + def connect(self): + try: + self.conn = SMBConnection(self.username, self.password, + self.client, self.server, + is_direct_tcp=True) + self.connected = self.conn.connect(self.server_ip, self.port) + connected = True + cherrypy.log.error('Connected to %s smb server' % self.server) + return self.connected + except Exception as e: + cherrypy.log.error('Connect failed. Reason: %s', e) + return False + + def list_shares(self): + try: + return self.conn.listShares() + except Exception as e: + raise cherrypy.HTTPError("401 Remote smb connection failed reseting: %s" % str(e)) + + def list_path(self, share=None, path='/'): + if share is None: + share=self.share + try: + for sub_share in sorted(self.conn.listPath(share,path), key=lambda item: item.filename): + cherrypy.log.error(" File=", sub_share.filename) + cherrypy.log.error(" Fileinfo= %s" % hex(sub_share.file_attributes)) + cherrypy.log.error(" File isdir= %s" % sub_share.isDirectory) + except Exception as e: + cherrypy.log.error(str(e)) + cherrypy.log.error('### can not list shares') + + def list_Attributes(self, share=None, path='/'): + if share is None: + cherrypy.log.error('getting %s'%path) + share=self.share + try: + return self.conn.getAttributes(share,path) + except Exception as e: + cherrypy.log.error(str(e)) + cherrypy.log.error('### can not list shares') + return None + + # Gets the file as a byte object. + def get_file(self, share=None, path='/'): + if share is None: + share=self.share + file_obj = tempfile.NamedTemporaryFile() + file_attributes, filesize = self.conn.retrieveFile(share, path, file_obj) + file_obj.seek(0) + return file_obj.read() + + # Handle the 'browse' command. + def browse(self, share=None, path='/'): + if share is None: + share=self.share + if path is None: + raise Exception("Missing path.") + + listing = { + "path": path, + "names": [], + "sizes": [], + "types": [], + "mtimes": [], + "mime-types": [], + } + + for sub_share in sorted(self.conn.listPath(share,path), key=lambda item: item.filename): + cherrypy.log.error(" File=", sub_share.filename) + cherrypy.log.error(" Fileinfo= %s" % hex(sub_share.file_attributes)) + cherrypy.log.error(" File isdir= %s" % sub_share.isDirectory) + ftype = "d" if sub_share.isDirectory else "f" + if ftype == "d": + mime_type = "application/x-directory" + else: + mime_type = slycat.mime_type.guess_type(sub_share.filename)[0] + + listing["names"].append(sub_share.filename) + listing["sizes"].append(sub_share.file_size) + listing["types"].append(ftype) + listing["mtimes"].append(datetime.datetime.fromtimestamp(sub_share.last_write_time).isoformat()) + listing["mime-types"].append(mime_type) + + return listing + +def create_session(username, password, server, share): + """ + Create a cached smb remote session for the given host. + + Parameters + ---------- + username : string + Username for ssh authentication. + password : string + Password for ssh authentication. + server : string + server that the share is connected to + share : string + share name that is being connected to + + Returns + ------- + smb_id : string + A unique session identifier. + """ + _start_session_cleanup_worker() + smb_id = uuid.uuid4().hex + try: + with session_cache_lock: + cherrypy.log.error("create the seesion and add it to the session_cache") + session_cache[smb_id] = Smb(username, password, server, share) + if session_cache[smb_id].connect(): + return smb_id + raise cherrypy.HTTPError("401 Remote smb connection failed: could not connect to smb drive") + except Exception as e: + cherrypy.log.error("Unknown exception for %s@%s: %s %s" % (username, server, type(e), str(e))) + cherrypy.log.error("slycat.web.server.smb.py create_session", + "cherrypy.HTTPError 500 unknown exception for %s@%s: %s %s." % ( + username, server, type(e), str(e))) + raise cherrypy.HTTPError("401 Remote smb connection failed: %s" % str(e)) + + +def check_session(sid): + """ + Return a true if session is active + + If the session has timed-out or doesn't exist, returns false + + Parameters + ---------- + sid : string + Unique session identifier returned by :func:`slycat.web.server.remote.create_session`. + + Returns + ------- + boolean : + """ + with session_cache_lock: + _expire_session(sid) + response = True + if sid not in session_cache: + response = False + if response: + session = session_cache[sid] + session._accessed = datetime.datetime.utcnow() + return response + + +def delete_session(sid): + """ + Delete a cached remote session. + + Parameters + ---------- + sid : string, required + Unique session identifier returned by :func:`slycat.web.server.remote.create_session`. + """ + with session_cache_lock: + if sid in session_cache: + session = session_cache[sid] + cherrypy.log.error( + "Deleting remote session for %s@%s" % (session.username, session.hostname)) + del session_cache[sid] + +def get_session(sid): + """ + Return a cached smb remote session. + + If the session has timed-out or doesn't exist, raises a 404 exception. + + Parameters + ---------- + sid : string + Unique session identifier returned by :func:`slycat.web.server.smb.create_session`. + + Returns + ------- + session : :class:`slycat.web.server.smb.Session` + Session object that encapsulates the connection to a smb remote host. + """ + with session_cache_lock: + _expire_session(sid) + if sid not in session_cache: + raise cherrypy.HTTPError("404 not a session") + session = session_cache[sid] + session._accessed = datetime.datetime.utcnow() + return session + +def _expire_session(sid): + """ + Test an existing session to see if it is expired. + + Assumes that the caller already holds session_cache_lock. + """ + if sid in session_cache: + session = session_cache[sid] + with session as con: + if not con.list_Attributes(): + cherrypy.log.error( + "removing remote session for %s@%s from %s" % (session.username, session.hostname, session.client)) + del session_cache[sid] + +def _session_monitor(): + while True: + cherrypy.log.error("Remote session cleanup worker running.") + with session_cache_lock: + for sid in list(session_cache.keys()): # We make an explicit copy of the keys because we may be modifying the dict contents + _expire_session(sid) + cherrypy.log.error("Remote SMB session cleanup worker finished.") + time.sleep(datetime.timedelta(minutes=15).total_seconds()) + +def _start_session_cleanup_worker(): + if _start_session_cleanup_worker.thread is None: + cherrypy.log.error("Starting remote SMB session cleanup worker.") + _start_session_cleanup_worker.thread = threading.Thread(name="SMB Monitor", target=_session_monitor) + _start_session_cleanup_worker.thread.daemon = True + _start_session_cleanup_worker.thread.start() + + +_start_session_cleanup_worker.thread = None diff --git a/web-server/components/ModalMedium.tsx b/web-server/components/ModalMedium.tsx index 17c924e34..3a3769115 100644 --- a/web-server/components/ModalMedium.tsx +++ b/web-server/components/ModalMedium.tsx @@ -42,7 +42,7 @@ export default class ModalMedium extends React.Component +
diff --git a/web-server/components/RemoteFileBrowser.tsx b/web-server/components/RemoteFileBrowser.tsx index 8e916a47e..92474dbe7 100644 --- a/web-server/components/RemoteFileBrowser.tsx +++ b/web-server/components/RemoteFileBrowser.tsx @@ -106,7 +106,7 @@ export default class RemoteFileBrowser extends React.Component { + /** + *Creates an instance of SlycatRemoteControls. + * @param {callBack, ConnectButton} props, + * callback: function + * where hostname, username, password, and session exist are return to the callee + * every time the hostname is changed. session exist should always be checked before + * moving on in in your logic structure. + * connectButton: bool tells UI to include connect + * @memberof SmbAuthentication + */ + constructor(props) { + super(props); + const display = this.populateDisplay(); + this.state = { + remote_hosts: [], + hostname: display.hostname?display.hostname:null, + username: display.username?display.username:null, + session_exists: null, + password: "", + share: display.share?display.share:null, + hostnames : [], + loadingData: this.props.loadingData, + initialLoad: false, + smb_info: this.props.smb_info + }; + } + private poll; + /** + * function used to test if we have an ssh connection to the hostname + * @param {hostname} + * @memberof SmbAuthentication + */ + checkRemoteStatus = async (hostname) => { + return client.get_remotes_fetch(hostname) + .then((json) => { + this.setState({ + session_exists:json.status, + initialLoad:true, + loadingData:false + }, () => { + this.props.callBack(this.state.hostname, this.b64EncodeUnicode(this.state.username), + this.b64EncodeUnicode(this.state.password), this.state.share, this.state.session_exists); + }); + }); + }; + /** + * takes a string value and encodes it to b64 + * @param str string to be encode + * @returns encoded result + */ + b64EncodeUnicode = (str) => { + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { + return String.fromCharCode('0x' + p1); + })); +}; + /** + * gets a list of all the known remote hosts that we can connect to + * via ssh + * + * @memberof SmbAuthentication + */ + getRemoteHosts = async () => { + return client.get_configuration_remote_hosts_fetch() + .then((json)=>{ + this.setState({hostnames:json}); + }) + }; + + async componentDidMount(){ + await this.checkRemoteStatus(this.state.hostname); + await this.getRemoteHosts(); + this.poll = setInterval( + async () => await this.checkRemoteStatus(this.state.hostname), + 3000 + ); + } + + /** + * checks local browser storage for the last used hostname and username + * + * @memberof SmbAuthentication + */ + populateDisplay = ():any => { + const display:any = {}; + if(!this.props.hover) { + if(localStorage.getItem("slycat-smb-remote-controls-hostname")){ + display.hostname = localStorage.getItem("slycat-smb-remote-controls-hostname") ? + localStorage.getItem("slycat-smb-remote-controls-hostname"):null; + } + if(localStorage.getItem("slycat-smb-remote-controls-username")){ + display.username = localStorage.getItem("slycat-smb-remote-controls-username") ? + localStorage.getItem("slycat-smb-remote-controls-username"):null; + } + if(localStorage.getItem("slycat-smb-remote-controls-share")){ + display.share = localStorage.getItem("slycat-smb-remote-controls-share") ? + localStorage.getItem("slycat-smb-remote-controls-share"):null; + } + } + else { + display.hostname = this.props.smb_info["hostname"]; + display.share = this.props.smb_info["collab"]; + } + return display; + }; + + /** + * updates local storage and react state depending on which input + * is being typed in + * + * @memberof SmbAuthentication + */ + onValueChange = (value, type) => { + switch(type) { + case "share": + localStorage.setItem("slycat-smb-remote-controls-share", value); + this.setState({share: value},() => { + this.props.callBack(this.state.hostname, this.b64EncodeUnicode(this.state.username), + this.b64EncodeUnicode(this.state.password), this.state.share, this.state.session_exists); + }); + break; + case "username": + localStorage.setItem("slycat-smb-remote-controls-username", value); + this.setState({username: value},() => { + this.props.callBack(this.state.hostname, this.b64EncodeUnicode(this.state.username), + this.b64EncodeUnicode(this.state.password), this.state.share, this.state.session_exists); + }); + break; + case "hostname": + localStorage.setItem("slycat-smb-remote-controls-hostname", value); + this.checkRemoteStatus(value); + this.setState({hostname: value},() => { + this.props.callBack(this.state.hostname, this.b64EncodeUnicode(this.state.username), + this.b64EncodeUnicode(this.state.password), this.state.share, this.state.session_exists); + }); + break; + case "password": + this.setState({password: value},() => { + this.props.callBack(this.state.hostname, this.b64EncodeUnicode(this.state.username), + this.b64EncodeUnicode(this.state.password), this.state.share, this.state.session_exists); + }); + break; + default: + throw new Error("bad Case"); + } + }; + + /** + * cleanup for when the component is unmounted + * + * @memberof SmbAuthentication + */ + componentWillUnmount() { + const display = this.populateDisplay(); + clearInterval(this.poll); + const state = { + remote_hosts: [], + enable: true, + hostname: display.hostname?display.hostname:null, + username: display.username?display.username:null, + session_exists: false, + password: null, + initialLoad: false + }; + this.setState(state); + } + + /** + * if the 'enter key' is pressed try and connect to + * the input hostname + * + * @memberof SmbAuthentication + */ + handleKeyDown = (e) => { + if (e.key === 'Enter') { + this.props.callBack(this.state.hostname, this.b64EncodeUnicode(this.state.username), + this.b64EncodeUnicode(this.state.password), this.state.share, this.state.session_exists); + } + } + + /** + * creates JSX form input if a session does not already exist for the given hostname + * + * @memberof SmbAuthentication + */ + getFormInputsJSX = () => { + return ( +
+
+ +
+ this.onValueChange(e.target.value, "share")} /> +
+
+
+ +
+ this.onValueChange(e.target.value, "username")} /> +
+
+
+ +
+ this.onValueChange(e.target.value, "password")} /> +
+
+
+ ); + } + + /** + * maps the hostnames as dropdowns items JSX + * + * @memberof SmbAuthentication + */ + getHostnamesJSX = () => { + const hostnamesJSX = this.state.hostnames.map((hostnameObject, i) => { + return ( +
  • + this.onValueChange(e.target.text, "hostname")}> + {hostnameObject.hostname} + +
  • + ) + }); + return hostnamesJSX; + } + + /** + * JSX for SlycatRemoteControls + * + * @returns JSX for rendering the component + * @memberof SmbAuthentication + */ + render() { + //make sure our data is loaded before we render + if(!this.state.initialLoad){ + return (
    ) + } + return ( +
    +
    + +
    +
    +
    +
    + this.onValueChange(e.target.value, "hostname")} /> +
    +
    +
    + {!this.state.session_exists&&this.getFormInputsJSX()} +
    + ); + } +} diff --git a/web-server/components/SmbRemoteFileBrowser.tsx b/web-server/components/SmbRemoteFileBrowser.tsx new file mode 100644 index 000000000..c22947a8f --- /dev/null +++ b/web-server/components/SmbRemoteFileBrowser.tsx @@ -0,0 +1,391 @@ +'use strict'; +import * as React from 'react'; +import client from 'js/slycat-web-client'; +import SlycatSelector, {Option} from 'components/SlycatSelector.tsx'; + +/** + * @member hostname name of the host we are connecting + * (assumes we have a connection already to the host) + * @member persistenceId uuid for local storage + * @member onSelectFileCallBack called every time a file is selected + * returns the files info (path, file.type, file:FileMetaData) + * @member onSelectParserCallBack called every time a parser is selected + * returns the parser type (dakota or csv) + * @member onReauthCallBack called every time we lose connection to the host + * @export + * @interface RemoteFileBrowserProps + */ +export interface RemoteFileBrowserProps { + hostname: string + persistenceId?: string + onSelectFileCallBack: Function + onSelectParserCallBack: Function + onReauthCallBack: Function +} + +/** + * @member path path shown in box + * @member pathInput path to current file when selected + * @member persistenceId uuid add to get local storage say if there were + * two of these classes being used + * @member rawFiles list of the current files meta data we are looking at + * @member pathError do we have a path error + * @member browseError do we have a browsing error + * @member browserUpdating are we in the middle of getting data + * @member selected id of selected file + * @export + * @interface RemoteFileBrowserState + */ +export interface RemoteFileBrowserState { + path:string + pathInput:string + persistenceId:string + rawFiles: FileMetaData[] + pathError: boolean + browseError: boolean + browserUpdating: boolean + selected:number +} + +/** + * @member type file type + * @member name filename + * @member size size of file + * @member mtime last accessed time + * @member mimeType type of file + * @interface FileMetaData + */ +interface FileMetaData { + type: string + name: string + size: string + mtime: string + mimeType: string +} + +/** + * used to create a file browsing window like using 'ls' and 'cd' in a linux terminal + * + * @export + * @class RemoteFileBrowser + * @extends {React.Component} + */ +export default class SmbRemoteFileBrowser extends React.Component { + public constructor(props:RemoteFileBrowserProps) { + super(props) + this.state = { + path:"/", + pathInput: "/", + rawFiles: [], + pathError: false, + browseError: false, + persistenceId: props.persistenceId === undefined ? '' : props.persistenceId, + browserUpdating: false, + selected:-1 + } + } + + /** + * given a path return all the items in said path (like ls) + * + * @param pathInput path to return all ls properties from + * @private + * @memberof RemoteFileBrowser + */ + private browse = (pathInput:string) => + { + // First check if we have a remote connection... + client.get_remotes_fetch(this.props.hostname) + .then((json) => { + // If we have a session, go on. + if(json.status) { + pathInput = (pathInput === ""?"/":pathInput); + this.setState({ + rawFiles:[], + browserUpdating:true, + selected:-1, + path:pathInput, + pathInput + }); + client.post_remote_browse_smb( + { + hostname : this.props.hostname, + path : pathInput, + success : (results:any) => + { + localStorage.setItem("slycat-remote-browser-path-" + this.state.persistenceId + this.props.hostname, pathInput); + this.setState({ + browseError:false, + pathError:false, + }); + + let files: FileMetaData[] = [] + if(pathInput != "/") + files.push({type: "", name: "..", size: "", mtime: "", mimeType:"application/x-directory"}); + for(let i = 0; i != results.names.length; ++i) + files.push({name:results.names[i], size:results.sizes[i], type:results.types[i], mtime:results.mtimes[i], mimeType:results["mime-types"][i]}); + this.setState({ + rawFiles:files, + browserUpdating:false + }); + }, + error : (results:any) => + { + if(this.state.path != this.state.pathInput) + { + this.setState({pathError:true, browserUpdating:false}); + } + if(results.status == 400){ + alert("bad file path") + } + this.setState({browseError:true, browserUpdating:false}); + } + }); + } + // Otherwise...we don't have a session anymore, so + // run the reauth callback if one was passed. + else { + if(this.props.onReauthCallBack) { + this.props.onReauthCallBack(); + } + } + }); + } + + /** + * takes a path and returns the directory above it + * + * @param path string path + * @private + * @returns new string path one level up + * @memberof RemoteFileBrowser + */ + private pathDirname = (path:string):string => + { + var new_path = path.replace(/\/\.?(\w|\-|\.)*\/?$/, ""); + if(new_path == "") + new_path = "/"; + return new_path; + } + + /** + * takes left path and right path and joins them + * @param right string path + * @param left string path + * @private + * @requires joined paths + * @memberof RemoteFileBrowser + */ + private pathJoin = (left:string, right:string):string => + { + var new_path = left; + if(new_path.slice(-1) != "/") + new_path += "/"; + new_path += right; + return new_path; + } + + /** + * given a file(which includes its full path), browse to the path above it + * + * @param file meta data for the file selected to browse up + * one level from said path + * @private + * @memberof RemoteFileBrowser + */ + private browseUpByFile = (file:FileMetaData) => { + this.setState({selected:-1}); + // If the file is our parent directory, move up the hierarchy. + if(file.name === "..") + { + this.browse(this.pathDirname(this.state.path)); + } + // If the file is a directory, move down the hierarchy. + else if(file.type === "d") + { + this.browse(this.pathJoin(this.state.path, file.name)); + } + } + + keyPress = (event:any, pathInput:string) => { + if (event.key == 'Enter'){ + // How would I trigger the button that is in the render? I have this so far. + this.browse(pathInput); + } + } + + + /** + * Given a row id and file info set the selected file and + * callBack to tell caller Path, file.type, file:FileMetaData + * + * @param file an object of FileMetaData + * @param i index of selected row in the table + * @private + * @memberof RemoteFileBrowser + */ + private selectRow = (file:FileMetaData, i:number) => { + let newPath:string = this.state.path; + const path_split:string[] = this.state.path.split("/"); + + /** + * If the user types out the full path, including file name, + * we don't want to join the file name with the path + * (resulting in duplicate file names). + */ + + if(path_split[path_split.length - 1] !== file.name) { + newPath = this.pathJoin(this.state.path, file.name); + } + + this.setState({selected:i},() => { + // tell our create what we selected + this.props.onSelectFileCallBack(newPath, file.type, file); + }) + } + + /** + * takes a list of file info from the state and converts it + * to an html + * + * @private + * @memberof RemoteFileBrowser + * @returns JSX.Element[] with the styled file list + */ + private getFilesAsJsx = ():JSX.Element[] => { + const rawFilesJSX = this.state.rawFiles.map((rawFile, i) => { + if (!rawFile.mtime){ + return null + } + return ( + this.selectRow(rawFile,i)} + onDoubleClick={()=> this.browseUpByFile(rawFile)}> + + {rawFile.mimeType === "application/x-directory"? + : + } + + {rawFile.name} + {rawFile.size} + {rawFile.mtime} + + ) + }) + return rawFilesJSX; + // if(file.mime_type() in component.icon_map) + // { + // icon = component.icon_map[file.mime_type()]; + // } + // else if(_.startsWith(file.mime_type(), "text/")) + // { + // icon = ""; + // } + // else if(_.startsWith(file.mime_type(), "image/")) + // { + // icon = ""; + // } + // else if(_.startsWith(file.mime_type(), "video/")) + // { + // icon = ""; + // } + } + + public async componentDidMount() { + const path = localStorage.getItem("slycat-remote-browser-path-" + + this.state.persistenceId + + this.props.hostname); + if(path != null){ + this.setState({path,pathInput:path}); + await this.browse(this.pathDirname(path)); + } + } + + public render() { + const options: Option[] = [{ + text:'Comma separated values (CSV)', + value:'slycat-csv-parser' + }, + { + text:'Dakota tabular', + value:'slycat-dakota-parser' + }]; + const pathStyle:any = { + width: 'calc(100% - 44px)', + float: 'left', + marginRight: '5px' + } + const styleTable:any = { + position: "relative", + height: (window.innerHeight*0.4)+"px", + overflow: "auto", + display: "block", + border: "1px solid rgb(222, 226, 230)", + } + return ( +
    + +
    +
    +
    + this.keyPress(event, this.state.pathInput)} + onChange={(e:React.ChangeEvent) => { + this.setState({pathInput:e.target.value}) + } + } + /> +
    + +
    +
    +
    + +
    +
    + {/*
    + Oops, that path is not accessible. Please try again. +
    */} +
    + + {!this.state.browserUpdating? +
    + + + + + + + + + + + {this.getFilesAsJsx()} + +
    NameSizeDate Modified
    +
    : + } + +
    + ); + } +} \ No newline at end of file diff --git a/web-server/js/slycat-plugins.js b/web-server/js/slycat-plugins.js index 3b80e4acc..9bd804322 100644 --- a/web-server/js/slycat-plugins.js +++ b/web-server/js/slycat-plugins.js @@ -43,6 +43,9 @@ export async function loadTemplate(name, format) { case "run-command": html = await import(/* webpackChunkName: "run_command_template" */ 'plugins/slycat-run-command/ui.html'); break; + case "smb": + html = await import(/* webpackChunkName: "smb_template" */ 'plugins/slycat-smb/ui.html'); + break; case "VS": html = await import(/* webpackChunkName: "ui_video_swarm_template" */ 'plugins/slycat-video-swarm/html/vs-ui.html'); break; @@ -89,6 +92,9 @@ export async function loadModule(name) { case "run-command": module = await import(/* webpackChunkName: "run_command_module" */ 'plugins/slycat-run-command/ui.js'); break; + case "smb": + module = await import(/* webpackChunkName: "run_command_module" */ 'plugins/slycat-smb/ui.js'); + break; case "VS": module = await import(/* webpackChunkName: "ui_video_swarm_module" */ 'plugins/slycat-video-swarm/js/vs-ui.js'); break; diff --git a/web-server/js/slycat-remotes.js b/web-server/js/slycat-remotes.js index 2d241982d..890460b4f 100644 --- a/web-server/js/slycat-remotes.js +++ b/web-server/js/slycat-remotes.js @@ -8,10 +8,17 @@ import mapping from 'knockout-mapping'; import ispasswordrequired from 'js/slycat-server-ispasswordrequired'; import template from 'templates/slycat-remote-login.html'; import "bootstrap"; +import React from "react"; +import ReactDOM from "react-dom"; +import SmbAuthentication from 'components/SmbAuthentication.tsx'; export function login(params) { var component = {}; + let smb_info = {}; + smb_info["hostname"] = params.hostname; + smb_info["collab"] = params.collab_name; + component.cancel = function() { component.container.children().modal("hide"); @@ -23,32 +30,57 @@ export function login(params) component.remote.enable(false); component.remote.status_type("info"); component.remote.status("Connecting ..."); - client.post_remotes( - { - hostname: params.hostname, - username: component.remote.username(), - password: component.remote.password(), - success: function(sid) + if(!params.smb) { + client.post_remotes( { - component.container.children().modal("hide"); - if(params.success) - params.success(sid); - }, - error: function(request, status, reason_phrase) - { - component.remote.enable(true); - component.remote.status_type("danger"); - component.remote.status(reason_phrase); - component.remote.focus("password"); - }, + hostname: params.hostname, + username: component.remote.username(), + password: component.remote.password(), + success: function(sid) + { + component.container.children().modal("hide"); + if(params.success) + params.success(sid); + }, + error: function(request, status, reason_phrase) + { + component.remote.enable(true); + component.remote.status_type("danger"); + component.remote.status(reason_phrase); + component.remote.focus("password"); + }, + }); + } + else { + client.post_remotes_smb_fetch({ + user_name: component.remote.username(), + password: component.remote.password(), + server: params.hostname, + share: component.remote.share() + }).then((response) => { + if(response.ok){ + component.container.children().modal("hide"); + params.success(response.status); + } else { + component.remote.enable(true); + component.remote.status_type("danger"); + component.remote.focus("password"); + } + }).catch((error)=>{ + component.remote.enable(true); + component.remote.status_type("danger"); + //component.remote.status(reason_phrase); + component.remote.focus("password"); }); + } } component.title = ko.observable(params.title || "Login"); component.message = ko.observable(params.message || ""); - component.remote = mapping.fromJS({username: null, password: null, status: null, enable: true, focus: false, status_type: null}); + component.remote = mapping.fromJS({username: null, password: null, status: null, enable: true, focus: false, status_type: null, share: null, session_exists: null}); component.remote.focus.extend({notify: "always"}); component.container = $($.parseHTML(template)).appendTo($("body")); component.ispasswordrequired = ispasswordrequired; + component.smb = params.smb; component.container.children().on("shown.bs.modal", function() { component.remote.focus(true); @@ -58,6 +90,28 @@ export function login(params) component.container.remove(); }); ko.applyBindings(component, component.container.get(0)); + + // If protocol is SMB, use the React login + if(params.smb) { + const setSmbAuthValues = function(hostname, username, password, share, session_exists) { + //component.remote.hostname(hostname) + component.remote.username(username) + component.remote.password(password) + component.remote.share(share) + component.remote.session_exists(session_exists) + } + ReactDOM.render( +
    + +
    , + document.querySelector(".smb-login") + ); + } component.container.children().modal("show"); } @@ -101,10 +155,12 @@ export function create_pool() { login( { + smb: params.smb, hostname: params.hostname, + collab_name: params.collab_name, title: params.title, message: params.message, - success: function(sid) + success: function(status) { if(params.success) params.success(params.hostname); diff --git a/web-server/js/slycat-web-client.js b/web-server/js/slycat-web-client.js index 6d04c8c2f..90e60894f 100644 --- a/web-server/js/slycat-web-client.js +++ b/web-server/js/slycat-web-client.js @@ -756,6 +756,21 @@ module.post_sensitive_model_command = function(params) { }); }; +module.post_remotes_smb_fetch = function(params) +{ + return fetch(`${api_root}remotes/smb`, + { + method: "POST", + credentials: "same-origin", + cache: "no-store", + dataType: "json", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params || {}) + }) +}; + module.post_sensitive_model_command_fetch = function(params, successFunction, errorFunction) { return fetch(`${api_root}models/${params.mid}/sensitive/${params.type}/${params.command}`, @@ -783,7 +798,6 @@ module.post_sensitive_model_command_fetch = function(params, successFunction, er }); }; - module.put_model_command = function(params) { $.ajax( @@ -1523,6 +1537,30 @@ module.post_remote_command = function(params) { }); }; +module.post_remote_browse_smb = function(params) +{ + console.log("In browse smb"); + $.ajax( + { + contentType: "application/json", + data: JSON.stringify( + { + }), + type: "POST", + url: api_root + "smb/remotes/" + params.hostname + "/browse" + params.path, + success: function(result) + { + if(params.success) + params.success(result); + }, + error: function(request, status, reason_phrase) + { + if(params.error) + params.error(request, status, reason_phrase); + } + }); +}; + module.post_remote_browse = function(params) { $.ajax( diff --git a/web-server/plugins/slycat-no-authentication.py b/web-server/plugins/slycat-no-authentication.py index ae0dfa023..19e1def2d 100644 --- a/web-server/plugins/slycat-no-authentication.py +++ b/web-server/plugins/slycat-no-authentication.py @@ -28,7 +28,7 @@ def authenticate(realm, rules=None): # See if the client already has a valid session. if "slycatauth" in cherrypy.request.cookie: - cherrypy.log.error('ding') + cherrypy.log.error('running cherrypy no authentication') sid = cherrypy.request.cookie["slycatauth"].value couchdb = slycat.web.server.database.couchdb.connect() session = None diff --git a/web-server/plugins/slycat-parameter-image/js/parameter-image-scatterplot.js b/web-server/plugins/slycat-parameter-image/js/parameter-image-scatterplot.js index 5b2c6d8b1..f77c5b942 100644 --- a/web-server/plugins/slycat-parameter-image/js/parameter-image-scatterplot.js +++ b/web-server/plugins/slycat-parameter-image/js/parameter-image-scatterplot.js @@ -2746,8 +2746,6 @@ $.widget("parameter_image.scatterplot", } // If we don't have a session for the image hostname, create one. - var cached_uri = URI(api_root + "projects/" + self.options.model.project + "/cache/" + URI.encode(uri.host() + uri.path())) - console.log("Attempting to load image from server-side cache..."); console.log("Loading image " + image.uri + " from server..."); @@ -2758,17 +2756,23 @@ $.widget("parameter_image.scatterplot", } xhr.image = image; - xhr.open("GET", api_root + "projects/" + self.options.model.project + "/cache/" + URI.encode(uri.host() + uri.path()), true); + xhr.open("GET", api_root + "remotes/" + uri.hostname() + api + uri.pathname() + "?cache=project&project=" + self.options.model.project + "&key=" + URI.encode(URI.encode(uri.host() + uri.path())), true); xhr.responseType = "arraybuffer"; + + //Split path to get collab name. Assume collab name is the first thing in path. + let split_path = uri.path().split("/"); + let collab_name = split_path[1]; xhr.onload = function(e){ //If the image isn't in cache, open an agent session: - if (this.status == 404) { + if (this.status == 404 || this.status == 400) { if(!self.login_open) { self.login_open = true; self.remotes.get_remote({ + smb: uri.protocol() == "smb", hostname: uri.hostname(), + collab_name: collab_name ? collab_name: null, title: "Login to " + uri.hostname(), message: "Loading " + uri.pathname(), cancel: function() { @@ -2827,7 +2831,7 @@ $.widget("parameter_image.scatterplot", xhr.image = image; //Double encode to avoid cherrypy's auto unencode in the controller - xhr.open("GET", api_root + "remotes/" + hostname + api + uri.pathname() + "?cache=project&project=" + self.options.model.project + "&key=" + URI.encode(URI.encode(uri.host() + uri.path())), true); + xhr.open("GET", api_root + "remotes/" + uri.hostname() + api + uri.pathname() + "?cache=project&project=" + self.options.model.project + "&key=" + URI.encode(URI.encode(uri.host() + uri.path())), true); xhr.responseType = "arraybuffer"; xhr.onload = function(e) { // If we get 404, the remote session no longer exists because it timed-out. diff --git a/web-server/plugins/slycat-parameter-image/js/wizard-ui.js b/web-server/plugins/slycat-parameter-image/js/wizard-ui.js index 683bbf44e..e7b22b839 100644 --- a/web-server/plugins/slycat-parameter-image/js/wizard-ui.js +++ b/web-server/plugins/slycat-parameter-image/js/wizard-ui.js @@ -15,6 +15,11 @@ import { remoteControlsReauth } from "js/slycat-remote-controls"; import "js/slycat-remote-browser"; import "js/slycat-table-ingestion"; import parameterImageWizardUI from "../wizard-ui.html"; +import React from "react"; +import ReactDOM from "react-dom"; +import SmbRemoteFileBrowser from 'components/SmbRemoteFileBrowser.tsx' +import SmbAuthentication from 'components/SmbAuthentication.tsx'; + function constructor(params) { @@ -32,7 +37,8 @@ function constructor(params) component.remote = mapping.fromJS({ hostname: null, username: null, - password: null, + password: null, + share: null, status: null, status_type: null, enable: true, @@ -167,7 +173,7 @@ function constructor(params) // Get list of model ids project data is used in client.get_project_data_parameter_fetch({ did: did, param: "mid"}).then((models) => { // if there are no more models using that project data, delete it - if(models.length === 0) { + if(models && models.length === 0) { client.delete_project_data_fetch({ did: did }); } }); @@ -181,6 +187,27 @@ function constructor(params) } }; + const onSelectTableFile = function(path, fileType, file) { + console.log(`newPath:: ${path}, fileType:: ${fileType}, file:: ${file}`); + if(fileType === "f"){ + component.browser.path(path); + } + }; + const onSelectParserCallBack = function(ParserName) { + component.parser(ParserName); + } + const onReauth = function() { + console.log("onReauth"); + }; + + const setSmbAuthValues = function(hostname, username, password, share, session_exists) { + component.remote.hostname(hostname) + component.remote.username(username) + component.remote.password(password) + component.remote.share(share) + component.remote.session_exists(session_exists) + } + component.select_type = function() { var type = component.ps_type(); @@ -190,6 +217,17 @@ function constructor(params) component.existing_table(); } else if (type === "remote") { component.tab(2); + } else if (type === "smb") { + component.tab(2); + ReactDOM.render( +
    + +
    , + document.querySelector(".smb-wizard-login") + ); } }; @@ -265,7 +303,69 @@ function constructor(params) }; fileUploader.uploadFile(fileObject); }; + component.connectSMB = function() { + component.remote.enable(false); + component.remote.status_type("info"); + component.remote.status("Connecting ..."); + if(component.remote.session_exists()) + { + ReactDOM.render( +
    + +
    , + document.querySelector(".smb-wizard-browse") + ); + component.tab(3); + component.remote.enable(true); + component.remote.status_type(null); + component.remote.status(null); + } + else + { + client.post_remotes_smb_fetch({ + user_name: component.remote.username(), + password: component.remote.password(), + server: component.remote.hostname(), + share: component.remote.share() + }).then((response) => { + console.log("authenticated.",response); + if(response.ok){ + component.remote.session_exists(true); + component.remote.enable(true); + component.remote.status_type(null); + component.remote.status(null); + component.tab(3); + ReactDOM.render( +
    + +
    , + document.querySelector(".smb-wizard-browse") + ); + + }else{ + component.remote.enable(true); + component.remote.status_type("danger"); + component.remote.focus("password"); + } + }).catch((error)=>{ + console.log("could not connect",error) + component.remote.enable(true); + component.remote.status_type("danger"); + component.remote.status(reason_phrase); + component.remote.focus("password"); + }); + } + }; component.connect = function() { component.remote.enable(false); component.remote.status_type("info"); @@ -302,6 +402,33 @@ function constructor(params) } }; + component.load_table_smb = function() { + console.log("component.browser.path()",component.browser.path()); + $('.remote-browser-continue').toggleClass("disabled", true); + const file_name = component.browser.path().split("/")[component.browser.path().split("/").length - 1]; + var fileObject ={ + pid: component.project._id(), + hostname: [component.remote.hostname()], + mid: component.model._id(), + paths: [component.browser.path()], + aids: [["data-table"], file_name], + parser: component.parser(), + progress: component.remote.progress, + progress_status: component.remote.progress_status, + progress_final: 90, + success: function(){ + upload_success(component.remote); + }, + error: function(){ + dialog.ajax_error("Did you choose the correct file and filetype? There was a problem parsing the file: ")(); + $('.remote-browser-continue').toggleClass("disabled", false); + component.remote.progress(null); + component.remote.progress_status(''); + } + }; + fileUploader.uploadFile(fileObject); + }; + component.load_table = function() { $('.remote-browser-continue').toggleClass("disabled", true); const file_name = component.browser.selection()[0].split("/")[component.browser.selection()[0].split("/").length - 1]; @@ -483,7 +610,7 @@ function constructor(params) // Get the list of models using that project data client.get_project_data_parameter_fetch({did: did, param: "mid"}).then((models) => { // if there are no more models using that project data, delete it - if(models.length === 0) { + if(models && models.length === 0) { client.delete_project_data_fetch({did: did}); } }); diff --git a/web-server/plugins/slycat-parameter-image/slycat-parameter-image.py b/web-server/plugins/slycat-parameter-image/slycat-parameter-image.py index a35b88f58..52ad7fd84 100644 --- a/web-server/plugins/slycat-parameter-image/slycat-parameter-image.py +++ b/web-server/plugins/slycat-parameter-image/slycat-parameter-image.py @@ -30,7 +30,7 @@ def media_columns(database, model, verb, type, command, **kwargs): [type] -- [description] """ - expression = re.compile('file://|http') + expression = re.compile('smb://|file://|http') search = numpy.vectorize(lambda x: bool(expression.search(x))) columns = [] diff --git a/web-server/plugins/slycat-parameter-image/wizard-ui.html b/web-server/plugins/slycat-parameter-image/wizard-ui.html index 5f1b6014d..39cd0303a 100644 --- a/web-server/plugins/slycat-parameter-image/wizard-ui.html +++ b/web-server/plugins/slycat-parameter-image/wizard-ui.html @@ -10,8 +10,8 @@ @@ -34,6 +34,13 @@
    +
    + +
    +