From 0faaaa96bb1a9f237c4456deee0347aadf2db013 Mon Sep 17 00:00:00 2001
From: klu909 <55161078+klu909@users.noreply.github.com>
Date: Mon, 5 Oct 2020 17:30:09 -0700
Subject: [PATCH] fix(2068): Large step logs causes browser to hang (#2200)

* fix(2068): Large step logs causes browser to hang

* fix uuid

* fix uuid

* fix test

* add comment

Co-authored-by: Kevin Lu <klu01@c02zm197lvdv.t-mobile.com>
---
 package.json                 |  4 +--
 plugins/builds/steps/logs.js | 50 +++++++++++++++++++++++++++++-------
 test/plugins/builds.test.js  | 25 ++++++++++++++++++
 3 files changed, 68 insertions(+), 11 deletions(-)

diff --git a/package.json b/package.json
index 2494f9432..4566177dd 100644
--- a/package.json
+++ b/package.json
@@ -74,14 +74,14 @@
     "@hapi/good-console": "^9.0.0",
     "@hapi/good-squeeze": "^6.0.0",
     "@hapi/hapi": "^20.0.0",
-    "@hapi/hoek": "^9.0.4",
+    "@hapi/hoek": "^9.1.0",
     "@hapi/inert": "^6.0.1",
     "@hapi/vision": "^6.0.0",
     "@promster/hapi": "^4.2.0",
     "async": "^3.2.0",
     "config": "^1.31.0",
     "date-fns": "^1.30.1",
-    "dayjs": "^1.8.33",
+    "dayjs": "^1.9.1",
     "deepmerge": "^4.2.2",
     "hapi-auth-bearer-token": "^6.1.6",
     "hapi-auth-jwt2": "^10.1.0",
diff --git a/plugins/builds/steps/logs.js b/plugins/builds/steps/logs.js
index a87dc9f1a..ad2948e78 100644
--- a/plugins/builds/steps/logs.js
+++ b/plugins/builds/steps/logs.js
@@ -6,6 +6,8 @@ const request = require('request');
 const ndjson = require('ndjson');
 const MAX_LINES_SMALL = 100;
 const MAX_LINES_BIG = 1000;
+const jwt = require('jsonwebtoken');
+const uuid = require('uuid');
 
 const logger = require('screwdriver-logger');
 
@@ -144,7 +146,7 @@ module.exports = config => ({
         notes: 'Returns the logs for a step',
         tags: ['api', 'builds', 'steps', 'log'],
         auth: {
-            strategies: ['token'],
+            strategies: ['token', 'session'],
             scope: ['user', 'pipeline', 'build']
         },
         plugins: {
@@ -156,7 +158,6 @@ module.exports = config => ({
             const { stepFactory } = req.server.app;
             const buildId = req.params.id;
             const stepName = req.params.name;
-            const { headers } = req;
 
             return stepFactory
                 .get({ buildId, name: stepName })
@@ -174,10 +175,28 @@ module.exports = config => ({
 
                     const isDone = stepModel.code !== undefined;
                     const baseUrl = `${config.ecosystem.store}/v1/builds/${buildId}/${stepName}/log`;
-                    const authToken = headers.authorization;
-                    const { sort } = req.query;
-                    const pagesToLoad = req.query.pages;
-                    const linesFrom = req.query.from;
+                    const authToken = jwt.sign(
+                        {
+                            buildId,
+                            stepName,
+                            scope: ['user']
+                        },
+                        config.authConfig.jwtPrivateKey,
+                        {
+                            algorithm: 'RS256',
+                            expiresIn: '5s',
+                            jwtid: uuid.v4()
+                        }
+                    );
+                    const { sort, type } = req.query;
+                    let pagesToLoad = req.query.pages;
+                    let linesFrom = req.query.from;
+
+                    if (type === 'download' && isDone) {
+                        // 100 lines per page
+                        pagesToLoad = Math.ceil(stepModel.lines / 100);
+                        linesFrom = 0;
+                    }
 
                     // eslint-disable-next-line max-len
                     return getMaxLines({ baseUrl, authToken })
@@ -191,9 +210,22 @@ module.exports = config => ({
                                 maxLines
                             })
                         )
-                        .then(([lines, morePages]) =>
-                            h.response(lines).header('X-More-Data', (morePages || !isDone).toString())
-                        );
+                        .then(([lines, morePages]) => {
+                            if (type !== 'download') {
+                                return h.response(lines).header('X-More-Data', (morePages || !isDone).toString());
+                            }
+
+                            let res = '';
+
+                            for (let i = 0; i < lines.length; i += 1) {
+                                res = `${res}${lines[i].m}\n`;
+                            }
+
+                            return h
+                                .response(res)
+                                .type('text/plain')
+                                .header('content-disposition', `attachment; filename="${stepName}-log.txt"`);
+                        });
                 })
                 .catch(err => {
                     throw err;
diff --git a/test/plugins/builds.test.js b/test/plugins/builds.test.js
index c8a287914..2c05c5a53 100644
--- a/test/plugins/builds.test.js
+++ b/test/plugins/builds.test.js
@@ -4525,6 +4525,31 @@ describe('build plugin test', () => {
                 });
         });
 
+        it('returns download link for download option', () => {
+            nock('https://store.screwdriver.cd')
+                .get(`/v1/builds/${id}/${step}/log.0`)
+                .twice()
+                .replyWithFile(200, `${__dirname}/data/step.log.ndjson`);
+
+            const expectedLog = 'Building stuff\nStill building...\nDone Building stuff\n';
+
+            return server
+                .inject({
+                    url: `/builds/${id}/steps/${step}/logs?type=download`,
+                    auth: {
+                        credentials: {
+                            scope: ['user']
+                        },
+                        strategy: ['session']
+                    }
+                })
+                .then(reply => {
+                    assert.equal(reply.statusCode, 200);
+                    assert.deepEqual(reply.result, expectedLog);
+                    assert.propertyVal(reply.headers, 'content-disposition', `attachment; filename="${step}-log.txt"`);
+                });
+        });
+
         it('returns logs for a step that is split across pages', () => {
             nock('https://store.screwdriver.cd')
                 .get(`/v1/builds/${id}/${step}/log.0`)