###############################################################################1#2# CoCalc: Collaborative Calculation in the Cloud3#4# Copyright (C) 2014 -- 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###############################################################################2021if not Primus?22alert("Library not fully built (Primus not defined) -- refresh your browser")23setTimeout((->window.location.reload()), 1000)2425$ = window.$26_ = require('underscore')2728client = require('smc-util/client')2930misc_page = require('./misc_page')3132APP_LOGO_WHITE = misc_page.APP_LOGO_WHITE3334# these idle notifications were in misc_page, but importing it here failed3536idle_notification_html = ->37{redux} = require('./smc-react')38customize = redux.getStore('customize')39"""40<div>41<img src="#{APP_LOGO_WHITE}">42<h1>... is on standby</h1>43— click to resume —44</div>45"""4647idle_notification_state = undefined4849idle_notification = (show) ->50if idle_notification_state? and idle_notification_state == show51return52$idle = $("#smc-idle-notification")53if show54if $idle.length == 055box = $("<div/>", id: "smc-idle-notification" ).html(idle_notification_html())56$("body").append(box)57# quick slide up, just to properly slide down on the fist time58box.slideUp 0, ->59box.slideDown "slow"60else61$idle.slideDown "slow"62else63$idle.slideUp "slow"64idle_notification_state = show6566# end idle notifications6768auth_token = misc_page.get_query_param('auth_token')6970class Connection extends client.Connection71constructor: (opts) ->72# Security note: not easily exposing this to the global scope would make it harder73# for an attacker who is eval'ing dangerous code in a Sage worksheet (say).74# However, even if it were not exposed, the attacker could just do75# "conn = new Primus(url, opts)"76# and make a Primus connection, and start sending/receiving messages. This would work,77# because the primus connection authenticates based on secure https cookies,78# which are there. So we could make everything painful and hard to program and79# actually get zero security gain.80#81# **CRITICAL:** If the smc object isn't defined in your Google Chrome console session,82# you have to change the context to *top*! See83# http://stackoverflow.com/questions/3275816/debugging-iframes-with-chrome-developer-tools/8581276#858127684#85super(opts)86@_setup_window_smc()8788# This is used by the base class for marking file use notifications.89@_redux = require('./smc-react').redux9091setTimeout(@_init_idle, 15 * 1000)9293_setup_window_smc: () =>94# if we are in DEBUG mode, inject the client into the global window object95if not DEBUG96return97window.smc ?= {}98window.smc.client = @99window.smc.misc = require('smc-util/misc')100window.smc.immutable = require('immutable')101window.smc.done = window.smc.misc.done102window.smc.sha1 = require('sha1')103window.smc.schema = require('smc-util/schema')104# use to enable/disable verbose synctable logging105window.smc.synctable_debug = require('smc-util/synctable').set_debug106window.smc.idle_trigger = => @emit('idle', 'away')107108# Client-side testing code -- we use require.ensure so this stuff only109# ever gets loaded by the browser if actually used.110window.smc.test = (modules) ->111require.ensure ['./test-client/init'], ->112require('./test-client/init').run(modules)113window.smc.test_clear = () ->114require.ensure ['./test-client/init'], ->115require('./test-client/init').clear()116117_init_idle: () =>118# Do not bother on mobile, since mobile devices already automatically disconnect themselves119# very aggressively to save battery life.120if require('./feature').IS_TOUCH121return122123###124The @_init_time is a timestamp in the future.125It is pushed forward each time @_idle_reset is called.126The setInterval timer checks every minute, if the current time is past this @_init_time.127If so, the user is 'idle'.128To keep 'active', call webapp_client.idle_reset as often as you like:129A document.body event listener here and one for each jupyter iframe.body (see jupyter.coffee).130###131132# 15 min default in case it isn't set (it will get set when user account settings are loaded)133@_idle_timeout ?= 15 * 60 * 1000134@_idle_reset()135setInterval(@_idle_check, 60 * 1000)136137# call this idle_reset like a function138# will reset timer on *first* call and then every 15secs while being called139@idle_reset = _.throttle(@_idle_reset, 15 * 1000)140141# activate a listener on our global body (universal sink for bubbling events, unless stopped!)142$(document).on('click mousemove keydown focusin touchstart', @idle_reset)143$('#smc-idle-notification').on('click mousemove keydown focusin touchstart', @_idle_reset)144145delayed_disconnect = undefined146147reconn = =>148if @_connection_is_totally_dead # CRITICAL: See https://github.com/primus/primus#primusopen !!!!!149@_conn?.open()150reconn = _.throttle(reconn, 10*1000) # never attempt to reconnect more than once per 10s, no matter what.151152disconn = =>153if @_connected154@_conn?.end()155156@on 'idle', (state) ->157#console.log("idle state: #{state}")158switch state159160when "away"161idle_notification(true)162delayed_disconnect ?= setTimeout(disconn, 15 * 1000)163164when "active"165idle_notification(false)166if delayed_disconnect?167clearTimeout(delayed_disconnect)168delayed_disconnect = undefined169reconn()170setTimeout(reconn, 5000) # avoid race condition???171172# This is called periodically. If the user hasn't been active173# for @_idle_timeout ms, then we emit an idle event.174_idle_check: =>175if not @_idle_time?176return177now = (new Date()).getTime()178# console.log("idle: checking idle #{@_idle_time} < #{now}")179if @_idle_time < now180@emit('idle', 'away')181182# Set @_idle_time to the **moment in in the future** at which the user will be183# considered idle, and also emit event indicator using is currently active.184_idle_reset: =>185@_idle_time = (new Date()).getTime() + @_idle_timeout + 1000186# console.log '_idle_reset', new Date(@_idle_time), ' _idle_timeout=', @_idle_timeout187@emit('idle', 'active')188189# Change the standby timeout to a particular time in minutes.190# This gets called when the user configuration settings are set/loaded.191set_standby_timeout_m: (time_m) =>192@_idle_timeout = time_m * 60 * 1000193@_idle_reset()194195_connect: (url, ondata) ->196log = (mesg) ->197console.log("websocket -", mesg)198log("connect")199200@url = url201if @ondata?202# handlers already setup203return204205@ondata = ondata206207###208opts =209ping : 25000 # used for maintaining the connection and deciding when to reconnect.210pong : 12000 # used to decide when to reconnect211strategy : 'disconnect,online,timeout'212reconnect :213max : 5000214min : 1000215factor : 1.25216retries : 100000 # why ever stop trying if we're only trying once every 5 seconds?217conn = new Primus(url, opts)218###219220conn = new Primus(url)221222@_conn = conn223conn.on 'open', () =>224@_connected = true225@_connection_is_totally_dead = false226if @_conn_id?227conn.write(@_conn_id)228else229conn.write("XXXXXXXXXXXXXXXXXXXX")230protocol = if window.WebSocket? then 'websocket' else 'polling'231@emit("connected", protocol)232log("connected; protocol='#{protocol}'")233@_num_attempts = 0234235conn.removeAllListeners('data')236f = (data) =>237@_conn_id = data.toString()238conn.removeListener('data',f)239conn.on('data', ondata)240conn.on("data", f)241242if auth_token?243@sign_in_using_auth_token244auth_token : auth_token245cb : (err, resp) ->246auth_token = undefined247248conn.on 'outgoing::open', (evt) =>249log("connecting")250@emit("connecting")251252conn.on 'offline', (evt) =>253log("offline")254@_connected = false255@emit("disconnected", "offline")256257conn.on 'online', (evt) =>258log("online")259260conn.on 'message', (evt) =>261ondata(evt.data)262263conn.on 'error', (err) =>264log("error: ", err)265# NOTE: we do NOT emit an error event in this case! See266# https://github.com/sagemathinc/cocalc/issues/1819267# for extensive discussion.268269conn.on 'close', () =>270log("closed")271@_connected = false272@emit("disconnected", "close")273274conn.on 'end', =>275@_connection_is_totally_dead = true276277conn.on 'reconnect scheduled', (opts) =>278@_num_attempts = opts.attempt279@emit("disconnected", "close") # This just informs everybody that we *are* disconnected.280@emit("connecting")281conn.removeAllListeners('data')282log("reconnect scheduled in #{opts.scheduled} ms (attempt #{opts.attempt} out of #{opts.retries})")283284conn.on 'incoming::pong', (time) =>285#log("pong latency=#{conn.latency}")286if not window.document.hasFocus? or window.document.hasFocus()287# networking/pinging slows down when browser not in focus...288@emit "ping", conn.latency289290#conn.on 'outgoing::ping', () =>291# log(new Date() - 0, "sending a ping")292293@_write = (data) =>294conn.write(data)295296# return latest ping/pong time (latency) if connected; otherwise, return undefined297latency: () =>298if @_connected299return @_conn.latency300301_fix_connection: (delete_cookies) =>302if delete_cookies303console.log("websocket -- deleting haproxy cookies")304document.cookie = 'SMCSERVERID3=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'305console.log("websocket --_fix_connection... ")306@_conn.end()307@_conn.open()308309_cookies: (mesg) =>310$.ajax(url:mesg.url, data:{id:mesg.id, set:mesg.set, get:mesg.get, value:mesg.value})311312alert_message: (args...) =>313require('./alerts').alert_message(args...)314315connection = undefined316exports.connect = (url) ->317if connection?318return connection319else320return connection = new Connection(url)321322exports.connect()323324325