node Initial - - HTTP module and static file server

HTTP module

The HTTP module is one of the most important modules in the node, not one.
This module provides the ability to execute HTTP services and generate HTTP requests. In fact, we will use the server written by node, mainly the HTTP module. Let's first look at the code needed by a simple HTTP server.

const http = require('http')
const server = http.createServer()
const port = 8899

server.on('request', (request, response) => {
  console.log(request.method, request.url)
  console.log(request.headers)

  response.writeHead(200, {'content-Type': 'text/html'})
  response.write('<h1>hello world</h1>')
  response.end()
})
 
server.listen(port, () => {
  console.log('server listening on port', port)
})

The HTTP module actually establishes a TCP link behind it and listens on the port port. The request event is triggered only when someone links to it and sends the correct HTTP message and can be correctly parsed out. The callback function for this event has two objects, one is the request object and the other is the response object.These two objects allow you to respond appropriately to the request event.Here the request and response have been parsed to read the related properties directly from the request, and the content written by the response is written directly into the body of the response because the response header has been automatically written.
How does the HTTP module work? The general code is as follows:

class httpServer {
  constructor(requestHandler) {
    var net = require('net')
    this.server = net.createServer(conn => {
      var head = parse() // parse data comming from conn
      if (isHttp(conn)) {
        requestHandler(new RequestObject(conn), new ResponseObject(conn))
      } else {
        conn.end()
      }
    })
  }

  listen(prot, f) {
    this.server.listen(port, f)
  }
}

Execute the script in the node, open the browser to access port localhost:8899, and the browser sends a request to the server, which responds to a simple HTML page.
A real web server is much more complex than this one, it needs to judge what the client is trying to do based on the requested method and find out the resource for action processing based on the requested URL, rather than the brainless output like ours.

The request function of the HTTP module can also be used as an HTTP client. Since it is not running in a browser environment, there is no cross-domain request problem and we can send requests to any server.

$ node
> req = http.request('http://www.baidu.com/',response => global.res = response)
> req.end()
> global.res.statusCode

Specific API s to view documentation

Exercise - Simple Message Board

Write a simple message board using the HTTP module

const http = require('http')
const server = http.createServer()
const port = 8890
const querystring = require('querystring')

const msgs = [{
  content: 'hello',
  name: 'zhangsan'
}]

server.on('request', (request, response) => {
  if (request.method == 'GET') {

    response.writeHead(200, {
      'Content-Type': 'text/html; charset=UTF-8'
    })

    response.write(`
      <form action='/' method='post'>
        <input name='name'/>
        <textarea name='content'></textarea>
        <button>Submit</button>
      </form>
      <ul>
        ${
      msgs.map(msg => {
        return `
              <li>
                <h3>${msg.name}</h3>
                <pre>${msg.content}</pre>
              </li>
            `
      }).join('')
      }
      </ul>
    `)
  }

  if (request.method == 'POST') {
    request.on('data', data => {
      var msg = querystring.parse(data.toString())
      msgs.push(msg)
    })
    response.writeHead(301, {
      'Location': '/'
    })
    response.end()
  }
})

server.listen(port, () => {
  console.log('server listening on port', port)
})

Exercise - Static File Server

Implement a static file server using the HTTP module:

  • Give Way http://localhost : 8090/Can access the contents of a folder on your computer (e.g. c:/foo/bar/baz/)
  • If you access a folder, return the index.html file under that folder
  • If the file does not exist, return to a page that contains the contents of the folder, and the contents can be clicked
  • Need to return the correct Content-Type for a specific file
const http = require('http')
const fs = require('fs')
const fsp = fs.promises
const path = require('path')

const port = 8090
/* const baseDir = './' // Here'. /'is relative to the node's working directory path, not the path where the file is located*/
const baseDir = __dirname // Here_u dirname is the absolute path to the file

const server = http.createServer((req, res) => {
  var targetPath = decodeURIComponent(path.join(baseDir, req.url)) //The target address is the splicing of the base path and the relative path of the file. decodeURIComponent() decodes the Chinese characters in the path
  console.log(req.method, req.url, baseDir, targetPath)
  fs.stat(targetPath, (err, stat) => { // Determine whether a file exists
    if (err) { // If not, return 404
      res.writeHead(404)
      res.end('404 Not Found')
    } else {
      if (stat.isFile()) { // Determine whether it is a file
        fs.readFile(targetPath, (err, data) => {
          if (err) { // Even if a file exists, it may not be opened, such as reading permissions.
            res.writeHead(502)
            res.end('502 Internal Server Error')
          } else {
            res.end(data)
          }
        })
      } else if (stat.isDirectory()) { 
        var indexPath = path.join(targetPath, 'index.html') // If it is a folder, stitch the address of the index.html file
        fs.stat(indexPath, (err, stat) => {
          if (err) { // If there is no index.html file in the folder
            if (!req.url.endsWith('/')) { // If the address bar does not end with/jump to the same address ending with/
              res.writeHead(301, {
                'Location': req.url + '/'
              })
              res.end()
              return
            }
            fs.readdir(targetPath, {withFileTypes: true}, (err, entries) => {
              res.writeHead(200, {
               'Content-Type': 'text/html; charset=UTF-8'
              })
              res.end(`
              ${
                entries.map(entry => {// Determine if it's a folder, if it's a folder, add a'/'after it and return to a page with the file descriptions in the folder, and each file name is a link
                  var slash = entry.isDirectory() ? '/' : '' 
                    return ` 
                      <div>
                        <a href='${entry.name}${slash}'>${entry.name}${slash}</a>
                      </div>
                    `
                  }).join('')
                }
              `)
            })
          } else { // If there is an index.html file, return the file contents directly
            fs.readFile(indexPath, (err, data) => {
              res.end(data)
            })
          }
        })
      }
    }
  })

})

server.listen(port, () => {
  console.log(port)
})

Using callback functions, this end of the code above has implemented a simple static file server, but the indentation level of the code is too high. You can use async, await to make the code simpler, but there are also some details to improve, such as different types of files need to be opened in different formatsWait, the next section of code optimizes

const http = require('http')
const fs = require('fs')
const fsp = fs.promises
const path = require('path')

const port = 8090
const baseDir = __dirname

var mimeMap = { // Create an object with some file types
  '.jpg': 'image/jpeg',
  '.html': 'text/html',
  '.css': 'text/stylesheet',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.txt': 'text/plain',
  'xxx': 'application/octet-stream',
}
const server = http.createServer(async (req, res) => {
  var targetPath = decodeURIComponent(path.join(baseDir, req.url))
  console.log(req.method, req.url, baseDir, targetPath)
  try {
    var stat = await fsp.stat(targetPath)
    if (stat.isFile()) {
      try {
        var data = await fsp.readFile(targetPath)
        var type = mimeMap[path.extname(targetPath)]
        if (type) {// If the file type is in a mimeMap object, the corresponding decoding method is used
          res.writeHead(200, {'Content-Type': `${type}; charset=UTF-8`})
        } else { //If not, decode as stream
          res.writeHead(200, {'Content-Type': `application/octet-stream`})
        }
        res.end(data)
      } catch(e) {
        res.writeHead(502)
        res.end('502 Internal Server Error')
      }
    } else if (stat.isDirectory()) {
      var indexPath = path.join(targetPath, 'index.html')
      try {
        await fsp.stat(indexPath)
        var indexContent = await fsp.readFile(indexPath)
        var type = mimeMap[path.extname(indexPath)]
        if (type) {// If the file type is in a mimeMap object, the corresponding decoding method is used
          res.writeHead(200, {'Content-Type': `${type}; charset=UTF-8`})
        } else { //If not, decode as stream
          res.writeHead(200, {'Content-Type': `application/octet-stream`})
        }
        res.end(indexContent)
      } catch(e) {
        if (!req.url.endsWith('/')) { 
          res.writeHead(301, {
            'Location': req.url + '/'
          })
          res.end()
          return
        }
        var entries = await fsp.readdir(targetPath, {withFileTypes: true})
        res.writeHead(200, {
          'Content-Type': 'text/html; charset=UTF-8'
        })
        res.end(`
          ${
            entries.map(entry => {
              var slash = entry.isDirectory() ? '/' : ''
                return `
                  <div>
                    <a href='${entry.name}${slash}'>${entry.name}${slash}</a>
                  </div>
                `
            }).join('') 
          }
        `)
      }
    }
  } catch(e) {
      res.writeHead(404)
      res.end('404 Not Found')
  }
})

server.listen(port, () => {
  console.log(port)
})

For example, the indentation level is significantly reduced, although it is possible to find the corresponding decoding method by using the way you create the mimeMap object yourself, it is still cumbersome, requires you to write a lot of content yourself, and it is not possible to list all the file formats.In fact, there are installation packages (such as mime) on npm that can query file MIME type specifically by extension name.npm i mime needs to be installed before use, which makes writing much easier.

const http = require('http')
const fs = require('fs')
const fsp = fs.promises
const path = require('path')
const mime = require('mime')

const port = 8090
const baseDir = __dirname

const server = http.createServer(async (req, res) => {
  var targetPath = decodeURIComponent(path.join(baseDir, req.url))
  console.log(req.method, req.url, baseDir, targetPath)
  try {
    var stat = await fsp.stat(targetPath)
    if (stat.isFile()) {
      try {
        var data = await fsp.readFile(targetPath)
        var type = mime.getType(targetPath)
        if (type) {// If the file type is in a mimeMap object, the corresponding decoding method is used
          res.writeHead(200, {'Content-Type': `${type}; charset=UTF-8`})
        } else { //If not, decode as stream
          res.writeHead(200, {'Content-Type': `application/octet-stream`})
        }
        res.end(data)
      } catch(e) {
        res.writeHead(502)
        res.end('502 Internal Server Error')
      }
    } else if (stat.isDirectory()) {
      var indexPath = path.join(targetPath, 'index.html')
      try {
        await fsp.stat(indexPath)
        var indexContent = await fsp.readFile(indexPath)
        var type = mime.getType(indexPath)
        if (type) {
          res.writeHead(200, {'Content-Type': `${type}; charset=UTF-8`})
        } else {
          res.writeHead(200, {'Content-Type': `application/octet-stream`})
        }
        res.end(indexContent)
      } catch(e) {
        if (!req.url.endsWith('/')) { 
          res.writeHead(301, {
            'Location': req.url + '/'
          })
          res.end()
          return
        }
        var entries = await fsp.readdir(targetPath, {withFileTypes: true})
        res.writeHead(200, {
          'Content-Type': 'text/html; charset=UTF-8'
        })
        res.end(`
          ${
            entries.map(entry => {
              var slash = entry.isDirectory() ? '/' : ''
                return `
                  <div>
                    <a href='${entry.name}${slash}'>${entry.name}${slash}</a>
                  </div>
                `
            }).join('') 
          }
        `)
      }
    }
  } catch(e) {
      res.writeHead(404)
      res.end('404 Not Found')
  }
})

server.listen(port, () => {
  console.log(port)
})

In the code just now, the path problem and decoding problem have been solved very well.The last and most important issue we have left is security.
For example, the base path to access is/home/pi/www/, and the web address entered is http://localhost:8090/../../../../../../../etc/passwd
When the two paths are combined, they are reduced to / etc/passwd, where information about user groups may be stored
Similarly, you can theoretically access any file on your computer in this way
In fact, what we want is to use the base path as the root directory of the HTTP server, without having access to files outside the root directory
The solution is that the access path to the HTTP server must start with the base path
Another example is that there may be hidden folders in the folder, such as.git, that store submissions from some users.Or there are configuration files in the folder that contain sensitive information that you do not want to be accessed by the outside world

const server = http.createServer(async (req, res) => {  
                     ..
                     ..
  const baseDir = path.resolve('./')
   // Note that baseDir must be an absolute path here
  var targetPath = decodeURIComponent(path.join(baseDir, req.url))
   // Prevent sending files other than baseDir
  if (!targetPath.startsWith(baseDir)) { 
    res.end('hello hacker')
    return
  }
   // Prevent sending files in folders that start with a dot (hidden files)
  if (targetPath.split(path.sep).some(seg => seg.startsWith('.'))) { // Here path.sep is the way to read the system separator
    res.end('hello hacker')
    return
  }
                     ..
                     ..

})

That way, our static file server is almost finished

Keywords: node.js JSON npm Web Server Javascript

Added by robin on Sat, 28 Sep 2019 19:25:58 +0300