So this is pretty weird.
I am running an app with angular, node and passport. It's managing some information and you can view a video associated with it.
When I use the app in the browser, all seems to work fine. But when I access the app with an android phone browser (didn't test with iOS), I can login and all works fine, but when I access the video (in the same session!) passport denies access! I am baffled as the rest of the application works fine in the android browser...
routes:
app.all('*',ensureAuthenticated)
app.get('/ping', routes.ping)
app.get('/logout', routes.logout)
app.get('/start', routes.start)
#more routes
app.get('/videos/:name', can_view_video, media.play) #can_view_video checks if a user has the rights to see the video
#Middleware to check that a user is authenticated
ensureAuthenticated = (req, res, next) ->
if (req.isAuthenticated())
return next()
logger.debug("Not authenticated!")
res.redirect('/')
#Middleware to check that user can view video
can_view_video = (req, res, next) ->
url = req.url
url_params_only = url.substring("/videos/".length)
id = url_params_only.substring(0, url_params_only.indexOf("/"))
can_access(req, res, id, (err) ->
if err?
handle_error(err, err.message, res)
else
return next()
)
can_access = (req, res, id=null, next) ->
role = req.user.role
if id is null
id = req.params.userId
user_id = req.user.id
User.findOne({ user_id:id }, (err, user) ->
if err?
handle_error(err, err.message, res)
if not user?
text = "no user with that id"
handle_error(err, text, res)
else if not id == user_id or role is not "admin"
text = "no user with that id"
logger.debug(text)
handler_error(new Error(text), text, res)
else
logger.debug "Login: can access, ok"
next()
)
play the video:
exports.play = (req,res) ->
video_path = __dirname + "/media/" + req.user.id + "/" + req.params.video
fs.readFile(video_path, (err, data) ->
if err?
handle_error(err, "video can't be read", res)
else
range = req.headers.range
if range?
total = data.length
parts = range.replace(/bytes=/, "").split("-")
partialstart = parts[0]
partialend = parts[1]
start = parseInt(partialstart, 10)
end = if partialend then parseInt(partialend, 10) else total-1
chunksize = (end-start)+1
res.writeHead(206,
"Content-Range": "bytes " + start + "-" + end + "/" + total,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": 'video/mp4' )
else
logger.debug("no range")
res.writeHead(200,
"Content-Length": data.length,
"Content-Type": 'video/mp4')
res.end(data)
)
The output when accessing with the android browser is:
no range
Not authenticated!
EDIT: If I remove the passport authentication, the video can be displayed on the phone...
EDIT2: on request: the serialize functions
passport.serializeUser((user, done) ->
done(null, user._id)
)
passport.deserializeUser((id, done) ->
User.findById(id, (err, user) ->
done(err, user)
)
)
So it seems the code runs fine, the video gets loaded and returned to the browser, but it cannot play it for whatever weird reason which is pretty puzzling for me...
In a normal desktop browser (e.g. Chrome), I get a range in the headers, then the video returns and it gets played - no "Not authenticated!" message!
the potential problem could be here:
res.writeHead(206,
"Content-Range": "bytes " + start + "-" + end + "/" + total,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": 'video/mp4' )
...
res.end(data)
We are telling the header here that we send a chunk, but are actually sending the whole file...
From here I found the way to do that correctly:
res.end(data.slice(start, end+1), "binary");
This seems to improve things a lot but we'll need to check further