AJAX / XHR states / CORS / Data access

Tasks

  • Create a simple HTML page with an info text field and a single button
  • Implement a JavaScript function which is triggered when the button is clicked
    • The function should fetch relatively large file (e.g. 100-200MB)
    • in the text field show following states:
      • loading - when the open method was called
      • loaded - when the send method was called
      • downloading - while the data is being downloaded
      • finished downloading - when the data has beeen downloaded
  • you can use Promise, async/await

Description

AJAX overview:

  • Asynchronous JavaScript and XML
  • technique for creating better, faster, and more interactive web applications
  • relies on XML, JSON, HTML, CSS and JavaScript
  • AJAX is not a programming language

Running this demo by using the jquery module to achieve the ajax request. Following the xhr state in https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState. The browser console would show the current XMLHttpRequest state when starting the server.

Deal with the problem of CORS

To deal with the CORS problem in the ajax request, I upload the large file to the Amazon S3 bucket and set the following json code to allow the local server accessing the remote resources.

Data access

A company needs to design an AJAX aplication that will access various resources on the Web by using JavaScript code running in a browser. This application is not public and only internal employees of the company can use it. This application will be available at the address http://company.at. Following table shows a list of URL addresses of resources, their formats and HTTP methods that the application will use to access these resources. Add all possible access technologies to the table for methods GET and DELETE. Note: parameter {dob} means date of birth.

Resource Format GET DELETE
http://company.at/customers XML AJAX (1) AJAX (2)
http://company.at/suppliers JSON AJAX, JSONP (3) AJAX (4)
http://weather.at/innsbruck XML AJAX-CORS (5) AJAX-CORS (6)
http://people.at/students JSON AJAX-CORS, JSONP (7) AJAX-CORS (8)
http://people.at/{dob}/contact VCARD AJAX-CORS (9) AJAX-CORS (10)
  • (1) (2): Follow the Same Origin Policy
  • (3) (4): JSONP only works on GET method
  • (5) (6) (9) (10): Using CORS to deal with problem of different domain
  • (7) (8): JSONP only works on GET method
  1. AJAX-CORS (add http header in server)
1
2
3
4
    res.writeHead(200, {
        'Content-Type': 'Application/json',
        'Access-Control-Allow-Origin': '*'
    });
  1. AJAX-CORS (Using CORS Anywhere which adds CORS headers to the proxied request)

Code (index.html)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!DOCTYPE html>
<html>
  <head>
    <title> HW2 - AJAX and XHR states </title>
  </head>
  <body>
    <input type="button" value="Download File" onclick="DownloadFile()" />
    <input type="text" value="Ready to Download" id="state">
    <div id="progressCounter"></div>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    
  <script>
    var progressElem = $('#progressCounter');
    function DownloadFile() {
      var url = "https://mytest1.s3.amazonaws.com/file.bz2";
      progressElem.text(url);
      $.ajax({
        url: url,
        cache: false,
        xhr: function() {
          var xhr = new XMLHttpRequest();
          xhr.onreadystatechange = function() {
            if ( xhr.readyState == 1 ) {
              document.getElementById('state').setAttribute('value', 'loading');
              console.log("Loading - when the open method was called");
            } // if
            else if (xhr.readyState == 2) {
              if ( xhr.status == 200 ) {
                xhr.responseType = "blob";
              } // if

              document.getElementById('state').setAttribute('value', 'loaded');
              console.log("Loaded - when the send method was called");
            } // else if
            else if ( xhr.readyState == 3 ) {
              document.getElementById('state').setAttribute('value', 'downloading');
              console.log("downloading - while the data is being downloaded");
            } // else if
            else {
              document.getElementById('state').setAttribute('value', 'finished downloading');
              console.log("finished downloading - when the data has beeen downloade");
            } // else 

          }; // xhr.onready

          xhr.addEventListener("progress", function (evt) {
            console.log(evt.lengthComputable);
            if (evt.lengthComputable) {
                var percentComplete = evt.loaded / evt.total;
                progressElem.html(Math.round(percentComplete * 100) + "%");
            }
          }, false);

          return xhr;
        },
        success: function (data) {
          console.log("nono");
           // Convert the Byte Data to BLOB object.
          var blob = new Blob([data], { type: "application/octetstream" });
          var link = window.URL.createObjectURL(blob);
          var a = $("<a />");
          a.attr("download", "file.bz2"); // attributes,value
          a.attr("href", link);
          console.log(link);
          $("body").append(a);
          a[0].click();
          $("body").remove(a);
        }

      });

    }; // DownloadFile()
    
    
  </script>
  <br>

  </body>
</html>

Node.js server in Docker

Tasks

  • Implement a simple HTTP server in Node.js
    • implement a “hello NAME” service endpoint
    • request: “http://localhost:8080/John”, response “Hello John”
  • Define an image Dockerfile, with the following specifications:
    • build on a chosen node.js image
    • the container should listen on port 8888
    • load the server implementation from a local directory
    • run the server
  • Create a docker image from the Dockerfile
  • Create a container for the image
    • tip: use the -p parameter to map public port to a private port inside the container (e.g. -p 8080:8888)
  • Check if the container is running (docker container ls)
  • Send several requests to the server running within the container
    • you can use ping, curl or telnet for testing
  • Stop the container

Description

Docker Overview:

  • package an application and its dependencies in a virtual container
  • build, commit and share images
  • based and primarily for Linux
  • allows operating-system-level virtualization
  • run isolated packages, also known, as containers
  1. Define an image Dockerfile and create a docker image
1
2
3
4
FROM --platform=linux/amd64 node:10-alpine
COPY . /app
WORKDIR /app
CMD node /app/server.js
1
docker build -t hw3-docker . 
  1. Create a container and run the curl. -p (Port mapping for the container and the host machine)
1
docker run -p 8080:8888 hw3-docker

Docker advanced (Redis)

Tasks

  • Task 1: Start a docker container for a redis server:
    • build on a chosen redis image redis
    • run the server
  • Task 2: Start a docker container for a redis client:
    • build on a chosen redis image redis
    • with the client insert some user info where key is the person name, and value is the address
  • Task 3: Implement a simple HTTP server in Node.js
    • implement a “http://localhost:8080/person/{person_name}/address” API, which returns the address of a person
    • request: GET “http://localhost:8080/person/John/address”, response “Thakurova 9, 160 00, Prague”
    • the server should fetch the data from a Redis server. Redis runs in a separate container than the node.js server! (see above).
  • Task 4: Define an image Dockerfile (for the node.js server), with the following specifications:
    • build on a chosen node.js image
    • load the server implementation from a local directory
    • run the server
    • Create a docker image from the Dockerfile
    • Create and run a container
    • Test the server - it shoudl return the address for a person retrieved from the linked redis server container

Description

Task1 - Run the Redis Server in a container Redis-server stores the data from Redis-client, and can handle the request from the Node.js

1
sudo docker run -p 6379:6379 --name redis-server -d redis

Task 2 - Run the Redis client in a container Redis-client is linked to the Redis-server

1
sudo docker run -it --link redis-server:redis --name redis-client -d redis 

Redis-client let user insert the data {kay,value}

Task3 - Implement a simple HTTP server and run it on the host, and Node.js fetches the data from the Redis-server

Failture
2022/04/03 - I faced the problem in the recent version of Redis, it seems that the problems are MAC docker or Redis version. https://stackoverflow.com/questions/71529419/redis-overiding-remote-host-in-nodejs I implemented the methods of docker-compose or link-container, but still encountered the connection refused problem.
Success

Must use redis version 3.0.2!!!!!!!!!

1
npm install redis@3.0.2

Task4 - Docker-compose file

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# version of docker-compose
version: '3.9'
# 'services' are equivalent to 'containers'
services:
  redis: 
    image: 'redis'
    ports:
     - '6379:6379'

  tnode11:
    image: 'hw4-docker'
    # Specify an array of ports to map
    ports:
      - '8080:8888'

Code (Redis server)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const http = require('http');
const redis = require('redis')
var url = require("url");

  const server = http.createServer((req,res) => {
    res.setHeader('Content-Type', 'text/plain');
    get_api_string = req.url.split('/');
    const client = redis.createClient({
      host: 'redis',
      port: 6379
    })
    // get request from
    if ( get_api_string[1] == "person" && get_api_string[3] == "address" ) {
      client.get(get_api_string[2], function (error, result) {
        console.log('GET result ->' + result);
        if ( result == null ) res.write("You must input the data to the redis-server from the redis client");
        else res.write("Hello " + result );
        res.end(); 

      });
    }
    else {
      var echo_name = req.url.substring(1);
      res.write("Hello " + echo_name);
      res.end();
    } 
  });

  server.listen(8888, '0.0.0.0', () => {
    console.log("listening for 8080");
  });

HTTP/2 Push mechanism

Tasks

  • Implement an HTTP/2 push mechanism
    • Implement a simple HTML page consisting of at least 3 additional resources (e.g. css, js, image, ..)
    • Implement a simple http server serving the page via HTTP/2
    • When the html page is requested, push all those files together with the requested page
  • Use http2 module

Description

HTTP/2 with the push method HTTP/2 without the push method The above results indicate that HTTP/2 using the push method is faster than the method without the push method. The key reason is the concurrency in the different types of files in one connection (Extend this concept to the QUIC protocol, fixing the HoL blocking problem).

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const sendFile = (stream, fileName) => {
  const fd = fs.openSync(fileName, "r");
  const stat = fs.fstatSync(fd);
  const headers = {
    "content-length": stat.size,
    "last-modified": stat.mtime.toUTCString(),
    "content-type": mime.getType(fileName)
  };
  stream.respondWithFD(fd, headers); // key point
};

const pushFile = (stream, path, fileName) => {
  stream.pushStream({ ":path": path }, (err, pushStream) => {
    if (err) {
      throw err;
    }

    sendFile(pushStream, fileName);
  });
};

Server-Sent Events

Task

Design and implement simple SSE server in Node.js and simple client in HTML/JavaScript.

  • The client connects to server and displays all incoming messages.
  • The server sends every 2 seconds one message - e.g. one line of text file.

Description

  • Server-sent events are functions extended on top of HTTP.
  • The server can actively send data to the client based on the new MIME type( text/event-stream ).

Client: The URL is passed to EventSource() and creates a new EventSource object. onmessage() is triggered by receiving the data.

Server: new MIME type(text/event-stream).

Code

1
2
3
4
const message =
  `retry: ${refreshRate}\nid:${id}\ndata: ${data}\n\n`;
	res.write(message);
}, refreshRate);

retry: Specify the time interval for the browser to re-initiate the connection

id: ID for each event-stream

data: the event data

‘\n’ gaps the indicator. ‘\n\n’ switches the lines

OAuth-Browser-Based-App

Task

Design and implement a simple OAuth - Browser-Based App. Browser-based apps run entirely in the browser after loading the source code from a web page.

  • Use a simple server (https) for serving static content (html, js, …​).
  • Configure any OAuth application of your choice.
  • You can use any OAuth solution as authorization and resource server: Google, GitHub, …​
  • The app in browser connects to the authorization server and allows access to the resources.
  • The app collects an presents in the browser any resource from the resource server using the provided code/token (e.g. list of contacts, files, messages, repositories, …​)
  • Do not use any OAuth library (e.g. GoogleAuth, …​)

Description

Workflow:

  1. Log in to the Google cloud console to register a new project. Get the client_id and client_secret and set the redirect_link

  2. Client calls getGoogleAuthURL() to get the code from https://accounts.google.com/o/oauth2/v2/auth

    Scope type: userinfo is the public data. If we want to leverage the common services, just like Google drive or photo…, we need to add the test users until publishing the project.

  3. The server posts the code with Axios to get the token from the https://oauth2.googleapis.com/token

  4. Call jwt.decode(token) to get the data

  5. Create a new project in the Google cloud console

  6. Enter index.html to log in through ouath2 After logging, Authorization: OK. Print the callback data

Code (OAuth server implement)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
const HTTP2_PORT = 3000;
const http2 = require('http2');
const mime = require('mime');
const fs = require('fs');
const url = require("url");
const qs = require('querystring');
const axios = require('axios');
const jwt = require('jsonwebtoken');

const serverOptions = {
  key: fs.readFileSync('localhost-privkey.pem'),
  cert: fs.readFileSync('localhost-cert.pem')
};

// fill your info
var ClientID = "";
var ClientSercet = "";
var RedirectLink = "";

async function getTokenFromGoogle(get_token_url, value)  {
  
  try {
    var res = await axios.post(get_token_url, qs.stringify(value), {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      },
    });
    } catch(err) {
      console.log(err);
    }

    return res.data;
} 

// read and send file content in the stream
const sendFile = (stream, fileName) => {
  const fd = fs.openSync(fileName, "r");
  const stat = fs.fstatSync(fd);
  const headers = {
    "content-length": stat.size,
    "last-modified": stat.mtime.toUTCString(),
    "content-type": mime.getType(fileName)
  };
  stream.respondWithFD(fd, headers); // key point
};

// handle requests
const http2Handlers = (req, res) => {
    // send empty response for favicon.ico
    if (req.url === "/favicon.ico") {
      res.stream.respond({ ":status": 200 });
      res.stream.end();
      return;
    }

    if (req.url === "/" || req.url === "/index.html" ) {
      req.url = "/index.html";
      const fileName = __dirname + req.url;
      sendFile(res.stream, fileName);
    } // if
    else if (req.url.includes( "/api/oauth/google" ) ) {
      var parsed = url.parse(req.url);
      var query  = qs.parse(parsed.query);
      var code = query.code;
      // get the toake with code
      var get_token_url = "https://oauth2.googleapis.com/token";
      const value = {
        code,
        client_id: ClientID,
        client_secret: ClientSercet,
        redirect_uri: RedirectLink,
        grant_type: "authorization_code",
      };

      
      getTokenFromGoogle(get_token_url, value).then(function(result) {
        var googleUser = jwt.decode(result.id_token);
        res.write( '<h1> Hello ' + googleUser.name + '</h1>');
        res.write('Your email address: ' + googleUser.email + '<br>');
        res.write('Locale: ' + googleUser.locale + '<br>');
        res.end('<img src=' + googleUser.picture + ' referrerpolicy="no-referrer"/>')
        console.log(googleUser);
      });
      
    }

};

http2
  .createSecureServer(serverOptions, http2Handlers)
  .listen(HTTP2_PORT, () => {
    console.log("http2 server started on port", HTTP2_PORT);
  });