Log session timeout and connection drop

This commit is contained in:
Zaid Abdulla committed 2017-10-09 00:25:32 -07:00
1 parent ef712de9e3
commit 342138d92e
3 files changed
+214 -153

No files matched your search

+212 -150
View File
@@ -52,13 +52,37 @@ throwError = (err, httpResponse) ->
throw err
writeResponse = (sessionContext, res) ->
writeResponse = (sessionContext, res, close) ->
# Close connection after last request in session
res.headers['Connection'] = 'close' if close
if config.get('DEBUG', sessionContext.deviceId)
dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n"
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
throwError(err) if err
)
finish = () ->
stats.concurrentRequests -= 1
if sessionContext.httpRequest.connection.destroyed
logger.accessError({
sessionContext: sessionContext
message: 'Connection dropped'
})
else if close
endSession(sessionContext, (err, isNew) ->
return throwError(err) if err
if isNew
logger.accessInfo({
sessionContext: sessionContext
message: 'New device registered'
})
)
else
sessionContext.lastActivity = Date.now()
currentSessions.set(sessionContext.httpRequest.connection, sessionContext)
# respond using the same content-encoding as the request
if sessionContext.httpRequest.headers['content-encoding']? and res.data.length > 0
switch sessionContext.httpRequest.headers['content-encoding']
@@ -75,17 +99,13 @@ writeResponse = (sessionContext, res) ->
res.headers['Content-Length'] = data.length
sessionContext.httpResponse.writeHead(res.code, res.headers)
sessionContext.httpResponse.end(data)
delete sessionContext.httpRequest
delete sessionContext.httpResponse
stats.concurrentRequests -= 1
finish()
)
else
res.headers['Content-Length'] = res.data.length
sessionContext.httpResponse.writeHead(res.code, res.headers)
sessionContext.httpResponse.end(res.data)
delete sessionContext.httpRequest
delete sessionContext.httpResponse
stats.concurrentRequests -= 1
finish()
recordFault = (sessionContext, fault, provisions, channels) ->
@@ -112,7 +132,7 @@ recordFault = (sessionContext, fault, provisions, channels) ->
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true
logger.accessError({
logger.accessWarn({
sessionContext: sessionContext
message: 'Channel has faulted'
fault: fault
@@ -452,8 +472,6 @@ nextRpc = (sessionContext) ->
endSession = (sessionContext, callback) ->
delete currentSessions.get(sessionContext.httpRequest.connection)[sessionContext.sessionId]
saveCache = sessionContext.cacheUntil?
counter = 3
@@ -534,17 +552,7 @@ endSession = (sessionContext, callback) ->
sendAcsRequest = (sessionContext, id, acsRequest) ->
if not acsRequest?
return endSession(sessionContext, (err, isNew) ->
return throwError(err, sessionContext.httpResponse) if err
if isNew
logger.accessInfo({
sessionContext: sessionContext
message: 'New device registered'
})
writeResponse(sessionContext, soap.response(null))
)
return writeResponse(sessionContext, soap.response(null), true)
if acsRequest.name is 'Download'
if not acsRequest.url?
@@ -585,39 +593,63 @@ sendAcsRequest = (sessionContext, id, acsRequest) ->
writeResponse(sessionContext, res)
getSession = (httpRequest, sessionId, callback) ->
return callback() if not sessionId?
getSession = (connection, sessionId, callback) ->
sessionContext = currentSessions.get(connection)
sessionContext = currentSessions.get(httpRequest.connection)?[sessionId]
if sessionContext?
if sessionContext
currentSessions.delete(connection)
return callback(null, sessionContext)
return callback() if not sessionId
setTimeout(() ->
cache.pop("session_#{sessionId}", (err, sessionContextString) ->
return callback(err) if err or not sessionContextString
session.deserialize(sessionContextString, (err, sessionContext) ->
return callback(err) if err
currentSessions.get(httpRequest.connection)[sessionId] = sessionContext
callback(null, sessionContext)
)
)
, 100
session.deserialize(sessionContextString, callback)
)
, 100)
currentSessions = new WeakMap()
# When socket closes, store active sessions in cache
onConnection = (socket) ->
currentSessions.set(socket, {})
# The property remoteAddress may be undefined after the connection is
# closed, unless we read it at least once (caching?)
socket.remoteAddress
socket.on('close', () ->
sessions = currentSessions.get(socket)
for sessionId, sessionContext of sessions
sessionContext = currentSessions.get(socket)
return if not sessionContext
currentSessions.delete(socket)
now = Date.now()
lastActivity = sessionContext.lastActivity
timeoutMsg = logger.flatten({
sessionContext: sessionContext
message: 'Session timeout'
sessionTimestamp: sessionContext.timestamp
})
timeout = (sessionContext.lastActivity + sessionContext.timeout * 1000) - now
if timeout <= 0
return logger.accessError(timeoutMsg)
setTimeout(() ->
cache.get("session_#{sessionContext.sessionId}", (err, sessionContextString) ->
return throwError(err) if err
return if not sessionContextString
session.deserialize(sessionContextString, (err, sessionContext) ->
return throwError(err) if err
if sessionContext.lastActivity == lastActivity
logger.accessError(timeoutMsg)
)
)
, timeout + 1000).unref()
session.serialize(sessionContext, (err, sessionContextString) ->
return throwError(err) if err
# TODO don't set if process is shutting down
cache.set("session_#{sessionId}", sessionContextString, sessionContext.timeout, (err) ->
cache.set("session_#{sessionContext.sessionId}", sessionContextString, Math.ceil(timeout / 1000) + 3, (err) ->
return throwError(err) if err
)
)
@@ -701,118 +733,7 @@ cacheDueTasksAndFaultsAndOperations = (deviceId, tasks, faults, operations, cach
cache.set("#{deviceId}_tasks_faults_operations", JSON.stringify(v), ttl, callback)
listener = (httpRequest, httpResponse) ->
stats.totalRequests += 1
if httpRequest.method != 'POST'
httpResponse.writeHead 405, {'Allow': 'POST'}
httpResponse.end('405 Method Not Allowed')
return
sessionId = null
# Separation by comma is important as some devices don't comform to standard
COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g
while match = COOKIE_REGEX.exec(httpRequest.headers.cookie)
sessionId = match[2] if match[1] == 'session'
# If overloaded, ask CPE to retry in 60 seconds
if not sessionId and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
httpResponse.writeHead(503, {'Retry-after': 60})
httpResponse.end('503 Service Unavailable')
stats.droppedRequests += 1
return
if httpRequest.headers['content-encoding']?
switch httpRequest.headers['content-encoding']
when 'gzip'
stream = httpRequest.pipe(zlib.createGunzip())
when 'deflate'
stream = httpRequest.pipe(zlib.createInflate())
else
httpResponse.writeHead(415)
httpResponse.end('415 Unsupported Media Type')
return
else
stream = httpRequest
chunks = []
bytes = 0
stream.on('data', (chunk) ->
chunks.push(chunk)
bytes += chunk.length
)
httpRequest.getBody = () ->
# Write all chunks into a Buffer
body = new Buffer(bytes)
offset = 0
chunks.forEach((chunk) ->
chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length
)
return body
stream.on('end', () ->
stats.concurrentRequests += 1
rpc = null
parseWarnings = []
getSession(httpRequest, sessionId, f = (err, sessionContext) ->
return throwError(err, httpResponse) if err
rpc ?= soap.request(httpRequest, sessionContext?.cwmpVersion, parseWarnings)
if not sessionContext?
if rpc.cpeRequest?.name isnt 'Inform'
httpResponse.writeHead(400)
httpResponse.end('Session is expired')
stats.concurrentRequests -= 1
return
stats.initiatedSessions += 1
deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId)
return session.init(deviceId, rpc.cwmpVersion, rpc.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId), (err, sessionContext) ->
return throwError(err, httpResponse) if err
sessionContext.sessionId = crypto.randomBytes(8).toString('hex')
currentSessions.get(httpRequest.connection)[sessionContext.sessionId] = sessionContext
httpRequest.connection.setTimeout(sessionContext.timeout * 1000)
getDueTasksAndFaultsAndOperations(deviceId, sessionContext.timestamp, (err, dueTasks, faults, operations, cacheUntil) ->
return throwError(err, httpResponse) if err
sessionContext.tasks = dueTasks
sessionContext.faults = faults
sessionContext.retries = {}
for k, v of faults
sessionContext.retries[k] = v.retries
sessionContext.operations = operations
sessionContext.cacheUntil = cacheUntil
# Delete expired faults
for k, v of sessionContext.faults
if v.expiry >= sessionContext.timestamp
delete sessionContext.faults[k]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[k] = true
f(null, sessionContext)
)
)
sessionContext.httpRequest = httpRequest
sessionContext.httpResponse = httpResponse
for w in parseWarnings
w.sessionContext = sessionContext
w.rpc = rpc;
logger.accessWarn(w)
if config.get('DEBUG', sessionContext.deviceId)
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n"
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
return throwError(err) if err
)
processRequest = (sessionContext, rpc) ->
if rpc.cpeRequest?
if rpc.cpeRequest.name is 'Inform'
logger.accessInfo({
@@ -873,6 +794,147 @@ listener = (httpRequest, httpResponse) ->
nextRpc(sessionContext)
)
listener = (httpRequest, httpResponse) ->
stats.totalRequests += 1
if httpRequest.method != 'POST'
httpResponse.writeHead 405, {'Allow': 'POST', 'Connection': 'close'}
httpResponse.end('405 Method Not Allowed')
return
sessionId = null
# Separation by comma is important as some devices don't comform to standard
COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g
while match = COOKIE_REGEX.exec(httpRequest.headers.cookie)
sessionId = match[2] if match[1] == 'session'
# If overloaded, ask CPE to retry in 60 seconds
if not sessionId and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
httpResponse.end('503 Service Unavailable')
stats.droppedRequests += 1
return
if httpRequest.headers['content-encoding']?
switch httpRequest.headers['content-encoding']
when 'gzip'
stream = httpRequest.pipe(zlib.createGunzip())
when 'deflate'
stream = httpRequest.pipe(zlib.createInflate())
else
httpResponse.writeHead(415, {'Connection': 'close'})
httpResponse.end('415 Unsupported Media Type')
return
else
stream = httpRequest
stats.concurrentRequests += 1
httpRequest.on('aborted', () ->
stats.concurrentRequests -= 1
# In some cases event end can be emitted after aborted event
httpRequest.removeAllListeners('end')
)
chunks = []
bytes = 0
stream.on('data', (chunk) ->
chunks.push(chunk)
bytes += chunk.length
)
stream.on('end', () ->
body = new Buffer(bytes)
offset = 0
chunks.forEach((chunk) ->
chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length
)
parsedRpc = (sessionContext, rpc, parseWarnings) ->
sessionContext.httpRequest = httpRequest
sessionContext.httpResponse = httpResponse
for w in parseWarnings
w.sessionContext = sessionContext
w.rpc = rpc;
logger.accessWarn(w)
if config.get('DEBUG', sessionContext.deviceId)
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{body}\n\n"
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
return throwError(err) if err
)
processRequest(sessionContext, rpc)
getSession(httpRequest.connection, sessionId, (err, sessionContext) ->
return throwError(err, httpResponse) if err
if sessionContext and (sessionContext.sessionId != sessionId or
sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now())
logger.accessError({message: 'Invalid session', sessionContext: sessionContext})
httpResponse.writeHead(400, {'Connection': 'close'})
httpResponse.end('Invalid session')
stats.concurrentRequests -= 1
return
# Check again just in case device included old session ID from the previous session
if not sessionContext and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
httpResponse.end('503 Service Unavailable')
stats.droppedRequests += 1
stats.concurrentRequests -= 1
return
parseWarnings = []
rpc = soap.request(body, sessionContext?.cwmpVersion, parseWarnings)
return parsedRpc(sessionContext, rpc, parseWarnings) if sessionContext
if rpc.cpeRequest?.name isnt 'Inform'
msg = {
message: 'Invalid session'
sessionContext: sessionContext or {}
}
msg.sessionContext.httpRequest = httpRequest
msg.sessionContext.httpResponse = httpResponse
logger.accessError(msg)
httpResponse.writeHead(400, {'Connection': 'close'})
httpResponse.end('Invalid session')
stats.concurrentRequests -= 1
return
stats.initiatedSessions += 1
deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId)
return session.init(deviceId, rpc.cwmpVersion, rpc.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId), (err, sessionContext) ->
return throwError(err, httpResponse) if err
sessionContext.sessionId = crypto.randomBytes(8).toString('hex')
httpRequest.connection.setTimeout(sessionContext.timeout * 1000)
getDueTasksAndFaultsAndOperations(deviceId, sessionContext.timestamp, (err, dueTasks, faults, operations, cacheUntil) ->
return throwError(err, httpResponse) if err
sessionContext.tasks = dueTasks
sessionContext.faults = faults
sessionContext.retries = {}
for k, v of faults
sessionContext.retries[k] = v.retries
sessionContext.operations = operations
sessionContext.cacheUntil = cacheUntil
# Delete expired faults
for k, v of sessionContext.faults
if v.expiry >= sessionContext.timestamp
delete sessionContext.faults[k]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[k] = true
parsedRpc(sessionContext, rpc, parseWarnings)
)
)
)
)
+1
View File
@@ -311,6 +311,7 @@ function accessError(details) {
exports.init = init;
exports.close = close;
exports.flatten = flatten;
exports.info = info;
exports.warn = warn;
exports.error = error;
+1 -3
View File
@@ -410,15 +410,13 @@ function fault(xml, cwmpVersion) {
}
function request(httpRequest, cwmpVersion, warn) {
function request(data, cwmpVersion, warn) {
warnings = warn;
const rpc = {
cwmpVersion: cwmpVersion
};
const data = httpRequest.getBody();
if (!data.length)
return rpc;