restreamer icon indicating copy to clipboard operation
restreamer copied to clipboard

Restreamer v2 support to secured streams with JWT

Open utilsites opened this issue 3 years ago • 2 comments

Does Restreamer v2 has option to create private/secured streams, using some token in the url? We implemented in v0.x a JWT token that's has some attributes like time allowed to play, ads on or off, etc. It would be good that Restreamer v2 would have this kind of functionality, otherwise we have to clone v2 and do the same changes to support this (hope that the code is similar :) If you are interested in this we can show how we did it and share the code. Best regards

utilsites avatar Jun 06 '22 14:06 utilsites

This is currently not implemented, but it's on our roadmap. v2 has a completely different codebase from v0.x.

It would be a great help and might accelerate the development on our side if you could share the code how you solved this.

ioppermann avatar Jun 07 '22 10:06 ioppermann

Code with JWT support for limiting stream time play and show or not ads, for Restreamer 0.x, need to be adapted for v2, it would be very good if you reviewed this code and implemented/adapted this solution on/to v2. This solution works very well now, its stable.

The main website passes a jwt token in the query string for restreamer player.html page, that is validated (has time limit to be valid) and if it allows ads or not (in a inside attribute). The player.html page passes the jwt token to the other urls used to obtain the stream video file and be validated again.

/* FILE: webserver/app.js  (part) */

    initProd() {
        const self = this;
        logger.debug("Init webserver with PROD environment");
        this.initAlways();

        this.app.get("/", async (req, res) => {
            res.sendFile(path.join(global.__public, "index.prod.html"));
        });

        this.app.get("/player.html", async (req, res) => {
            const jwt = req.query.jwt;
            if (jwt) {
                try {
                    let decodedJWT = await self.authLib.verifyToken(jwt);
                    if (decodedJWT.show_video_ads == "1") {
                        let templateFilePath = path.join(global.__public, "playerAdTemplate.html");
                        fs.readFile(templateFilePath, "utf8", function (err, data) {
                            if (err) {
                                return console.log(err);
                            }
                            var result = data.replace("tag: ''",  "tag: '" + process.env.RS_AD_TAG + "'");
                            result = result.replace("hls/live.stream.m3u8", "hls/live.stream.m3u8?jwt=" + jwt);

                            res.send(result);
                            res.end();
                        });
                    } else {
                        let defaultFilePath = path.join(global.__public, "player.html")
                        fs.readFile(defaultFilePath, "utf8", function (err, data) {
                            if (err) {
                                return console.log(err);
                            }
                            let result = data.replace("hls/live.stream.m3u8", "hls/live.stream.m3u8?jwt=" + jwt);

                            res.send(result);
                            res.end();
                        });
                    }
                } catch (err) {
                    logger.info("Error app.js: " + err, false);
                }
            } else {
                res.sendFile(path.join(global.__public, "player.html"));
            }
        });
        this.add404ErrorHandling();
        this.add500ErrorHandling();
    }


 /* FILE: webserver/controllers/index.js (part)
   * Handle HLS serving files */
   
    restreamer.app.get("/hls/:file", async (req, res) => {
        const file = req.params.file;
        var ext = file.substr(file.lastIndexOf(".") + 1);

        // check file extension
        if (ext != "ts" && ext != "m3u8") {
            res.writeHead(401, {
                "Content-Type": "text/plain",
            });
            res.end("Unauthorized");
            return;
        }

        // check jwt
        const token = req.query.jwt || "";
        if (ext == "m3u8") {
            if (token) {
                try {
                    await restreamer.authLib.verifyToken(token);
                } catch (err) {
                    res.writeHead(401, {
                        "Content-Type": "text/plain",
                    });
                    res.end("Unauthorized");
                    return;
                }
            } else {
                res.writeHead(401, {
                    "Content-Type": "text/plain",
                });
                res.end("Unauthorized");
                return;
            }
        }

        res.sendFile(`/tmp/hls/${req.params.file}`);
    });


 /*** FILE: classes/Auth.js (part) ****
   *** JWT util lib ***/
   
const jwt = require("jsonwebtoken");
const logger = require.main.require("./classes/Logger")("Webserver");

class Auth {
    constructor(jwt_secret, jwt_expiration_seconds) {
        this.jwt_secret = jwt_secret;
        this.jwt_expiration_seconds = jwt_expiration_seconds;
    }

    signToken(id) {
        const self = this;
        return new Promise((resolve, reject) => {
            jwt.sign(
                { id: id },
                self.jwt_secret,
                {
                    expiresIn: self.jwt_expiration_seconds,
                },
                (err, token) => {
                    if (err) {
                        logger.error('Error sign Auth.js: ' + err, false);
                        reject(err);
                        return;
                    }
                    resolve(token);
                }
            );
        });
    }

    verifyToken(token) {
        const self = this;
        return new Promise((resolve, reject) => {
            jwt.verify(token, self.jwt_secret, (err, decoded) => {
                if (err) {
                    logger.error('Error verify Auth.js: ' + err, false);
                    reject(err);
                    return;
                }
                resolve(decoded);
            });
        });
    }
}

module.exports = Auth;

 /*** FILE: package.json (part) ****/
"dependencies": {
        "jsonwebtoken": "^8.5.1",
	...
}

/*** FILE conf/live.json (part) ****/
added new variable: RS_JWT_SECRET_KEY


utilsites avatar Jul 19 '22 16:07 utilsites

Hello

We are closing your ticket https://github.com/datarhei/restreamer/issues/357.

This may be due to the following reasons:

  • Problem/inquiry has been solved
  • Ticket remained unanswered by you for a more extended period
  • Problem was explained and handled in another ticket

You can reopen this ticket at any time!

Please only open related tickets once! Always answer/ask in the original ticket with the same issue!

With kind regards, Your datarhei team

svenerbeck avatar Nov 23 '22 21:11 svenerbeck