Monday, November 22, 2021

Gulp & React

Replace & Delete files in build folder using Gulp gulpfile.js
 
const gulp       = require('gulp');
const replace    = require('gulp-string-replace');
const rimraf     = require('gulp-rimraf');

function changeLang() {
    const searchFor = '<html lang="en">';
    const replaceBy = '<html lang="de">';
  
    return gulp.src('./build/*.html')
      .pipe(replace(searchFor, replaceBy))
      .pipe(gulp.dest('./build'));
}

function deleteFiles () {
  return gulp
    .src('./build/static/js/*.*.txt', { read: false })
    .pipe(rimraf());
}

exports.change_lang = changeLang;
exports.delete_files = deleteFiles;

package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build && npm run gulp-change-language && npm run gulp-delete_files",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "gulp-change-language": "gulp change_lang",
    "gulp-delete_files": "gulp delete_files"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "gulp": "^4.0.2",
    "gulp-rimraf": "^1.0.0",
    "gulp-string-replace": "^1.1.2"
  }
}

Test gulp

npm run build

Friday, November 19, 2021

CSP and Dynamically Generated JavaScript tag

CSP HTTP header and Meta Tag must be synchronized. There must be two hashes one for hard coded JavaScript other for generated JavaScript tag. 

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Content Security Policy Demo</title>
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; font-src 'self' ; img-src 'self' https://images.unsplash.com ; script-src 'self' 'sha256-B/8L7bCoRRLbK80hqzXcLc/NSWDtHVADDkdElr18Q2U=' 'sha256-BQBWQMByG186hXi3h+x/zUK8eOCr3iy91FDzS9uKCNs='; style-src 'self' https://cdn.jsdelivr.net; frame-src 'self'">
    <link rel="stylesheet" href="style.css" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
      integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
      crossorigin="anonymous"
      />
    <script src="node_modules/jquery/dist/jquery.min.js"></script>
  </head>
  <body>
    <script type = "text/javascript" >
      $(document).ready(function() {
         $('div.grid').after('<div>new element</div>');
      });
      let newScript = document.createElement("script");
      let inlineScript = document.createTextNode("alert('Hello World!');");
      newScript.appendChild(inlineScript);
      $(document.body).append(newScript);
    </script>
    <div class="box">
      <div class="grid">
        <img
          src="https://images.unsplash.com/photo-1535089780340-34cc939f9996?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80"
          alt=""
          />
        <img
          src="https://images.unsplash.com/photo-1587081917197-95be3a2d2103?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80"
          alt=""
          />
        <img
          src="https://images.unsplash.com/photo-1502248103506-76afc15f5c45?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
          alt=""
          />
      </div>
    </div>
  </body>
</html>
server.js
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const app = express();
 
app.use(function (req, res, next) {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; font-src 'self' ; img-src 'self' https://images.unsplash.com ; script-src 'self' 'sha256-B/8L7bCoRRLbK80hqzXcLc/NSWDtHVADDkdElr18Q2U=' 'sha256-BQBWQMByG186hXi3h+x/zUK8eOCr3iy91FDzS9uKCNs='; style-src 'self' https://cdn.jsdelivr.net; frame-src 'self'"
  );
  next();
});
 
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname)));
 
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname + '/index.html'));
});
 
const server = app.listen(process.env.PORT || 5500, () => {
  const { port } = server.address();
  console.log(`Server running on PORT ${port}`);
});
 
package.json
{
  "name": "csp-javascript",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "npx nodemon server.js"
  },
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "jquery": "^3.6.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.6"
  }
}
 

Monday, November 15, 2021

GitLab Pipeline and React

GitLab for React

Make sure you are using same node image in the pipeline and Dockerfiles

optimised .gitlab-ci.yml using artifacts
stages:
  - build
  - test
  - push

image:
  name: node:16-alpine
  entrypoint: [""]

build_r:
  stage: build
  artifacts:
    paths:
      - node_modules/
      - package.json
      - package-lock.json
    expire_in: 1 day
  before_script:
    - apk -U upgrade
  script:
    - node -v
    - ls -lA
    - npm install
    - ls -lA
  only:
    - main

test_r:
  stage: test
  before_script:
    - apk -U upgrade
  script:
    - ls -lA
    - CI=true npm test
  only:
    - main

push_r:
  stage: push
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor --cache=false --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile-prod --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA 
  only:
    - main
 
.gitlab-ci.yml using cache
stages:
  - build
  - test
  - push

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - node_modules/

image:
  name: node:16-alpine

build_r:
  stage: build
  before_script:
    - export ENV_VAR="value"
  script:
    - node -v
    - echo "$ENV_VAR"
    - echo "$CI_COMMIT_REF_SLUG"
    - npm install
  only:
    - main

test_r:
  stage: test
  script:
    - echo "$CI_COMMIT_REF_SLUG"
    - CI=true npm test
  only:
    - main

push_r:
  stage: push
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor --cache=false --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile-prod --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA 
  only:
    - main
 
Dockerfile
FROM node:16-alpine

RUN apk -U upgrade

WORKDIR /app

COPY package.json ./

RUN npm install

COPY ./ ./

CMD ["npm", "start"]
Dockerfile-prod
FROM node:16-alpine as build

WORKDIR /usr/app

COPY package.json .

RUN npm install

COPY  . .

RUN REACT_APP_BASE_URL=example.com yarn build

FROM nginx:1.19.0

WORKDIR /usr/share/nginx/html

RUN rm -rf ./*

COPY --from=build /usr/app/build /usr/share/nginx/html

ENTRYPOINT ["nginx", "-g", "daemon off;"]
This will work for any react app that has tests.

On target machine:
docker login registry.gitlab.com
docker run -it --name react registry.gitlab.com/USER-NAME/IMAGE-NAME:TAG