GNU readline cheatsheet

在 Linux環境中,command line如bash或是interpreter如python常常有支援readline的keymap,以下整理常用的快速鍵,其他可參考以下連結

https://en.wikipedia.org/wiki/GNU_Readline

移動游標

  • Ctrl + f : move forward 1 character →
  • Ctrl + b : move backward 1 character ←
  • Alt + f : move forward 1 word
  • Alt + b : move backward 1 word
  • Ctrl + a : move to the start of the line
  • Ctrl + e : move to the end of the line
  • Ctrl + l : clear screen

command操作

  • Ctrl + n : next command in history ↓
  • Ctrl + p : prev command in history ↑
  • Ctrl + r : reverse search, 再Ctrl + r 再繼續search
  • Ctrl + g : abort search

Edit

  • Ctrl + t : 游標前後字元對調
  • Ctrl + w : 往前刪退一個word
  • Ctrl + k : 刪到行尾
  • Alt + u : uppercase the word after cursor
  • Alt + l : lowercase the word after cursor

離開

  • Ctrl + c : send SIGINT
  • Ctrl + d : send EOF

bash的特別指令

  • Ctrl + x Ctrl + v : show bash version
  • Ctrl + x Ctrl + e : edit current line with default editor 這個是滿好用的快速鍵,可以將現在這行跳到編輯器編輯完再返回bash執行,對於很長的命令需要剪剪貼貼的情境很好用
Posted in System Administration | Leave a comment

WebSocket node.js ws 註解整理 – 1

這裡 註解 整理在Node.js中,WebSocket client/server常用的實現 ws 套件的程式碼

https://github.com/websockets/ws

關於WebSocket protocol,可參考另一篇文章的整理

index.js

'use strict';

const WebSocket = require('./lib/websocket');

WebSocket.createWebSocketStream = require('./lib/stream');
WebSocket.Server = require('./lib/websocket-server');
WebSocket.Receiver = require('./lib/receiver');
WebSocket.Sender = require('./lib/sender');

module.exports = WebSocket;

所有的implementation在lib資料夾,大約3600行,算是一個輕量的實現,可先從index.js export的WebSocket開始追起,這也是一般js WebSocket client使用的class,WebSocket client API在實現上也是參考 browser使用的API,所以https://developer.mozilla.org/en-US/docs/Web/API/WebSocket 也要一併參考

以下逐行整理註解lib/websocket.js

lib/websocket.js

'use strict';

const EventEmitter = require('events');
const https = require('https');
const http = require('http');
const net = require('net');
const tls = require('tls');
const { randomBytes, createHash } = require('crypto');
const { URL } = require('url');

const PerMessageDeflate = require('./permessage-deflate');
const Receiver = require('./receiver');
const Sender = require('./sender');
const {
  BINARY_TYPES,
  EMPTY_BUFFER,
  GUID,
  kStatusCode,
  kWebSocket,
  NOOP
} = require('./constants');
//在Node.js中,繼承EventEmitter會有addListener,removeListener。引入 event-target主要是為了實作相容於web的addEventListener, removeEventListener介面
const { addEventListener, removeEventListener } = require('./event-target');
const { format, parse } = require('./extension');
const { toBuffer } = require('./buffer-util');

//下面定義的4個readyStates可參考 https://developer.mozilla.org/en-US/docs/Web/API/WebSocket ,這4個state在rfc 6455也有明確指出
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
const protocolVersions = [8, 13];
const closeTimeout = 30 * 1000;

上面可以看到支援的兩個version 8、13

8主要是對應https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-08 開始的版本到 -12 ,13主要是對應 https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-13 開始的版本,不過實際上支援的程度或相容性仍要看實作細節

/**
 * Class representing a WebSocket.
 *
 * @extends EventEmitter
 */
class WebSocket extends EventEmitter {
...
}

繼承 EventEmitter,可參考 Nodejs Events整理的說明

constructor的部分:

  /**
   * Create a new `WebSocket`.
   *
   * @param {(String|url.URL)} address The URL to which to connect
   * @param {(String|String[])} [protocols] The subprotocols
   * @param {Object} [options] Connection options
   */
  constructor(address, protocols, options) {
    super(); //ES6繼承使用的語法

    //BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'] 預設使用nodebuffer
    this._binaryType = BINARY_TYPES[0];
    //預設的 close code 1006 代表未完成close,這邊主要是先定義一個fail state,操作成功或是有其他狀態變更才更新
    this._closeCode = 1006;
    //下面4個主要是紀錄close的訊息、close的情形
    this._closeFrameReceived = false;
    this._closeFrameSent = false;
    this._closeMessage = '';
    this._closeTimer = null;
    
    this._extensions = {};
    this._protocol = '';
    //RFC6455 p14 A connection is
   defined to initially be in a CONNECTING state.
    this._readyState = WebSocket.CONNECTING;
    //這邊可以作為實現網路參考,一個receiver, 一個sender
    this._receiver = null;
    this._sender = null;

    //underlying socket
    this._socket = null;

    if (address !== null) {
      this._bufferedAmount = 0;
      this._isServer = false;
      this._redirects = 0;

      //protocols如果是array就展開成字串 , 分隔
      if (Array.isArray(protocols)) {
        protocols = protocols.join(', ');
      } else if (typeof protocols === 'object' && protocols !== null) {
        //如果是object,就判定該argument指的是原本第三個的options,protocols從缺
        options = protocols;
        protocols = undefined;
      }
      initAsClient(this, address, protocols, options);
    } else {
      //WebSocket的address 明確地傳null,會當成server端的connection
      this._isServer = true;
    }
  }

接下來從constructor內的initAsClient往下追

/**
 * Initialize a WebSocket client.
 *
 * @param {WebSocket} websocket The client to initialize
 * @param {(String|url.URL)} address The URL to which to connect
 * @param {String} [protocols] The subprotocols
 * @param {Object} [options] Connection options
    //以下說明options
 * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable
 *     permessage-deflate
 * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
 *     handshake request
 * @param {Number} [options.protocolVersion=13] Value of the
 *     `Sec-WebSocket-Version` header
 * @param {String} [options.origin] Value of the `Origin` or
 *     `Sec-WebSocket-Origin` header
 * @param {Number} [options.maxPayload=104857600] The maximum allowed message
 *     size
 * @param {Boolean} [options.followRedirects=false] Whether or not to follow
 *     redirects
 * @param {Number} [options.maxRedirects=10] The maximum number of redirects
 *     allowed
 * @private
 */


function initAsClient(websocket, address, protocols, options) {
  const opts = {
    protocolVersion: protocolVersions[1], //預設 = 13
    maxPayload: 100 * 1024 * 1024, //預設 = 100MB
    perMessageDeflate: true, //預設開啟permessagedeflate
    followRedirects: false, //預設關閉redirect
    maxRedirects: 10, //預設最多10次redirects
    ...options, //這裡是merge options,options內有定義的field會蓋過前面寫的
    createConnection: undefined,
    socketPath: undefined,
    hostname: undefined,
    protocol: undefined,
    timeout: undefined,
    method: undefined,
    host: undefined,
    path: undefined,
    port: undefined
  };
  //如果protocolVersion不支援,throw RangeError
  if (!protocolVersions.includes(opts.protocolVersion)) {
    throw new RangeError(
      `Unsupported protocol version: ${opts.protocolVersion} ` +
        `(supported versions: ${protocolVersions.join(', ')})`
    );
  }
  let parsedUrl;

  //設定websocket需要的_url value
  //利用URL物件解析傳入的address
  if (address instanceof URL) {
    parsedUrl = address;
    websocket._url = address.href;
  } else {
    parsedUrl = new URL(address);
    websocket._url = address;
  }
  //ws支援unix domain socket
  const isUnixSocket = parsedUrl.protocol === 'ws+unix:';
  //unix socket需要有pathname,其他需要有host
  if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) {
    throw new Error(`Invalid URL: ${websocket.url}`);
  }
  //這邊主要判斷走一般tcp or tls連線
  const isSecure =
    parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:';
  const defaultPort = isSecure ? 443 : 80;
  const key = randomBytes(16).toString('base64'); //gen dynamic key for Sec-WebSocket-Key
  const get = isSecure ? https.get : http.get;
  let perMessageDeflate;
  opts.createConnection = isSecure ? tlsConnect : netConnect;

  opts.defaultPort = opts.defaultPort || defaultPort;
  opts.port = parsedUrl.port || defaultPort;
  opts.host = parsedUrl.hostname.startsWith('[')
    ? parsedUrl.hostname.slice(1, -1)
    : parsedUrl.hostname;
  opts.headers = {
    'Sec-WebSocket-Version': opts.protocolVersion,
    'Sec-WebSocket-Key': key,
    Connection: 'Upgrade',
    Upgrade: 'websocket',
    ...opts.headers
  };
  opts.path = parsedUrl.pathname + parsedUrl.search;
  opts.timeout = opts.handshakeTimeout;
  //如果有開permessagedeflate
  if (opts.perMessageDeflate) {
    perMessageDeflate = new PerMessageDeflate(
      opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
      false,
      opts.maxPayload
    );
   //這邊用es6 [] literal syntax是指 PerMessageDeflate.extensionName的值當成key
    opts.headers['Sec-WebSocket-Extensions'] = format({
      [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
    });
  }
  //subprotocol
  if (protocols) {
    opts.headers['Sec-WebSocket-Protocol'] = protocols;
  }
  //指定Origin,主要是pass一些server的檢查
  if (opts.origin) {
    if (opts.protocolVersion < 13) {
      opts.headers['Sec-WebSocket-Origin'] = opts.origin;
    } else {
      opts.headers.Origin = opts.origin;
    }
  }  
  if (parsedUrl.username || parsedUrl.password) {
    opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
  }
  
  if (isUnixSocket) {
    const parts = opts.path.split(':');

    opts.socketPath = parts[0];
    opts.path = parts[1];
  } 

以下設置http req,透過http.get function,可參考: https://nodejs.org/docs/latest-v12.x/api/http.html#http_http_request_options_callback

  let req = (websocket._req = get(opts)); //發起連線

  if (opts.timeout) {
    //起始連接失敗 abortHandshake
    req.on('timeout', () => {
      abortHandshake(websocket, req, 'Opening handshake has timed out');
    });
  }

  //下面這段需要特別注意,主要是要清楚每一步代表的意思
  //例如 req.on('error' 發生的時機點
  //以及 req = null的時機點
  //以及 req.aborted == true的條件
  req.on('error', (err) => {
    if (req === null || req.aborted) return; //忽略abort的case,在ws, follow redirect是先abort再重新initAsClient
    req = websocket._req = null; //代表error event只會處理一次
    websocket._readyState = WebSocket.CLOSING;
    websocket.emit('error', err);
    websocket.emitClose(); //此處readyState會變為CLOSED
  });

上面的error event handler會檢查req是否為null,在ws中,client的req被set成null只有兩處,一個是error handler內的,主要是不做重複的處理,另外一個是收到upgrade之後,這邊可以理解成當client收到upgrade時,就完全主動掌握req的流程了(open handshake走到最後一步,server已經接受所以回傳Upgrade,等待client驗證完response header後,就切入socket),所以在切入socket前如果有錯誤,client就直接下abortHandshake,最後將控制權轉給socket,並且這段是synchronous code,到轉給socket前,不會進入到event handler。而後續的網路error,則由socket的error event handler處理 (ref websocket.js #174)

下面整理req收到response的整理,需注意再下面還有upgrade event,這兩種情況不會同時發生(有upgrade就不會emit response event),所以response可以理解為server reject(沒送101 switch protocol upgrade)

  //req收到response event的處理
  req.on('response', (res) => {
    const location = res.headers.location;
    const statusCode = res.statusCode;

    if (
      location && //有redirect header
      opts.followRedirects && //有設follow redirect
      statusCode >= 300 && //status code = 3xx
      statusCode < 400
    ) {
      if (++websocket._redirects > opts.maxRedirects) {
        abortHandshake(websocket, req, 'Maximum redirects exceeded');
        return;
      }
      //取消req,重新發起request
      req.abort();

      const addr = new URL(location, address);

      initAsClient(websocket, addr, protocols, options);
    } else if (!websocket.emit('unexpected-response', req, res)) {
      //emit return false代表沒有註冊該event name的listener
      abortHandshake(
        websocket,
        req,
        `Unexpected server response: ${res.statusCode}`
      );
    }
  });

這邊是收到Upgrade後的處理,在ws基本上就是丟棄req物件,改以底層的socket來做傳輸

  req.on('upgrade', (res, socket, head) => {
    websocket.emit('upgrade', res);

    //
    // The user may have closed the connection from a listener of the `upgrade`
    // event.
    //
    if (websocket.readyState !== WebSocket.CONNECTING) return;

    req = websocket._req = null; //關掉req的處理,這邊的操作是安全的,在nodejs lib _http_client.js 裡面 發出upgrade event後,再來就是'close' event

    //計算一下expect key
    const digest = createHash('sha1')
      .update(key + GUID)
      .digest('base64');

    if (res.headers['sec-websocket-accept'] !== digest) {
      abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header');
      return;
    }

    //以下check 其他server response header
    const serverProt = res.headers['sec-websocket-protocol'];
    const protList = (protocols || '').split(/, */);
    let protError;

    if (!protocols && serverProt) {
      protError = 'Server sent a subprotocol but none was requested';
    } else if (protocols && !serverProt) {
      protError = 'Server sent no subprotocol';
    } else if (serverProt && !protList.includes(serverProt)) {
      protError = 'Server sent an invalid subprotocol';
    }

    if (protError) {
      abortHandshake(websocket, socket, protError);
      return;
    }
    //set handshaked subprotocol
    if (serverProt) websocket._protocol = serverProt;

    if (perMessageDeflate) {
      try {
        //檢查server extension response
        const extensions = parse(res.headers['sec-websocket-extensions']);
        //如果server有支援permessagedeflate的話...
        if (extensions[PerMessageDeflate.extensionName]) {
          perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
          websocket._extensions[
            PerMessageDeflate.extensionName
          ] = perMessageDeflate;
        }
      } catch (err) {
        abortHandshake(
          websocket,
          socket,
          'Invalid Sec-WebSocket-Extensions header'
        );
        return;
      }
    }
    //交給socket處理
    websocket.setSocket(socket, head, opts.maxPayload);
  });
}

Upgrade後就轉交給setSocket處理,這邊主要初始化sender, receiver,註冊相關的event handler/listener,然後WebSocket emit ‘open’

  /**
   * Set up the socket and the internal resources.
   *
   * @param {net.Socket} socket The network socket between the server and client
   * @param {Buffer} head The first packet of the upgraded stream
   * @param {Number} [maxPayload=0] The maximum allowed message size
   * @private
   */
  setSocket(socket, head, maxPayload) {
    //建構receiver: writable stream
    const receiver = new Receiver(
      this.binaryType, //buffer type
      this._extensions, //handshaked extensions
      this._isServer, //server or client, for mask
      maxPayload //maxpayload, default 100MB, 另外雖然RFC可以支援到64bit的長度,實際上的最大限制會被語言或是runtime限制住,例如js的max integer 2^53-1
    );
    //建構sender
    this._sender = new Sender(socket, this._extensions);
    this._receiver = receiver;
    this._socket = socket;

    receiver[kWebSocket] = this; //kWebSocket = Symbol('websocket')
    socket[kWebSocket] = this;

    receiver.on('conclude', receiverOnConclude); //conclude是結束的意思
    receiver.on('drain', receiverOnDrain); //writable stream event
    receiver.on('error', receiverOnError); //writable stream event
    receiver.on('message', receiverOnMessage); //receiver event
    receiver.on('ping', receiverOnPing); //receiver event
    receiver.on('pong', receiverOnPong); //receiver event

    socket.setTimeout(0); //turn off idle timeout
    socket.setNoDelay();  //disable the use of Nagle's algorithm.

    //這一行比較特別,他把head放回去,後面說明
    if (head.length > 0) socket.unshift(head);

    //register socket event listener
    socket.on('close', socketOnClose);
    socket.on('data', socketOnData);
    socket.on('end', socketOnEnd);
    socket.on('error', socketOnError);

    //finally, change to OPEN state, emit 'open'
    this._readyState = WebSocket.OPEN;
    this.emit('open');
  }

上面有一行 if (head.length > 0) socket.unshift(head); 是將head的data放回socket buffer,這邊的head不是指http header之類的,他是在http client upgrade時parse超過的部分,因為socket data來的時候有可能在http header後body也一起帶過來了,畢竟對server來說,當client request合格後,server side的websocket connection在送完open handshake response就是OPEN狀態了,後面可以繼續送body,所以從client角度來看,有可能同時收到server handshake response + websocket data(body),可參考:

If the server finishes these steps without aborting the WebSocket handshake, the server considers the WebSocket connection to be established and that the WebSocket connection is in the OPEN state.

RFC 6455 p25

在initAsClient裡建連線使用opts.createConnection變數,傳入http.get,opts.createConnection = isSecure ? tlsConnect : netConnect;

/**
 * Create a `net.Socket` and initiate a connection.
 *
 * @param {Object} options Connection options
 * @return {net.Socket} The newly created socket used to start the connection
 * @private
 */
function netConnect(options) {
  options.path = options.socketPath;
  return net.connect(options); //這裡的net.connect是 net.createConnection()返回一個socket
}

/**
 * Create a `tls.TLSSocket` and initiate a connection.
 *
 * @param {Object} options Connection options
 * @return {tls.TLSSocket} The newly created socket used to start the connection
 * @private
 */
function tlsConnect(options) {
  options.path = undefined;

  //這邊是設定SNI (Server Name Indication) extension,因為tls connection先於http request,所以對於virtual host來說,建TLS需要再加上SNI
  if (!options.servername && options.servername !== '') {
    options.servername = net.isIP(options.host) ? '' : options.host;
  }

  return tls.connect(options); //tls.connect返回tls.TLSSocket
}

ws透過createConnection參數省掉了custom http agent,可以理解為直接透過net.connect的raw connection,而避開的http.Agent的一些機制的實現,例如keep alive connection,事實上websocket的連線也不需要。

接下來整理有關close connection的部分,close大概分幾種狀態

  • abort handshake
  • 使用者下close()
  • tcp socket收到close
  • 收到對方送來close

abortHandshake發生的時機點主要是在websocket open handshake完成之前

/**
 * Abort the handshake and emit an error.
 *
 * @param {WebSocket} websocket The WebSocket instance
 * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the
 *     socket to destroy
 * @param {String} message The error message
 * @private
 */
function abortHandshake(websocket, stream, message) {
  //更改狀態CLOSING
  websocket._readyState = WebSocket.CLOSING;
  //在err物件加上call此function的stack trace
  const err = new Error(message);
  Error.captureStackTrace(err, abortHandshake);
  
  //這裡用stream.setHeader判斷是否可以response header
  //以client來說 當收到server upgrade header時,就代表無法再response header
  //如果此時要abort 就是立即斷線
  if (stream.setHeader) {
    stream.abort();
    stream.once('abort', websocket.emitClose.bind(websocket));
    websocket.emit('error', err);
  } else {
    stream.destroy(err);
    stream.once('error', websocket.emit.bind(websocket, 'error'));
    stream.once('close', websocket.emitClose.bind(websocket));
  }
}

在ws中abortHandshake分成req 或是 socket,在程式碼內是用stream.setHeader來判斷,req.abort裡面也會做socket.destroy,唯一的差別是在req.abort時,socket不一定存在(http req.abort有做相關檢查),兩個API都是等同於直接斷線,因為從client的角度來看,在open handshake完成之前(before ‘open’ state),關閉連線就是直接斷線,沒有其他做法(沒有辦法做close handshake,也沒有辦法像server可以response status code)

再來整理使用者下close()的情形,須注意此public method – close() 在內部的關閉連線也是統一呼叫此method,所以不僅只是外部使用者呼叫而已

 /**
   * Start a closing handshake.
   *
   *          +----------+   +-----------+   +----------+
   *     - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
   *    |     +----------+   +-----------+   +----------+     |
   *          +----------+   +-----------+         |
   * CLOSING  |ws.close()|<--|close frame|<--+-----+       CLOSING
   *          +----------+   +-----------+   |
   *    |           |                        |   +---+        |
   *                +------------------------+-->|fin| - - - -
   *    |         +---+                      |   +---+
   *     - - - - -|fin|<---------------------+
   *              +---+
   *
   * @param {Number} [code] Status code explaining why the connection is closing
   * @param {String} [data] A string explaining why the connection is closing
   * @public
   */

  close(code, data) {
    //close handshake要在state OPEN後才有意義
    //CLOSED狀態:已關閉連線 不需要處理
    if (this.readyState === WebSocket.CLOSED) return;
    //CONNETING: 在open之前,使用abortHandshake處理
    if (this.readyState === WebSocket.CONNECTING) {
      const msg = 'WebSocket was closed before the connection was established';
      return abortHandshake(this, this._req, msg);
    }
    //CLOSING狀態: 這個狀態比較複雜,因為很多情況會跑到此狀態
    //包括: req.on('error' event listener
    //      abortHandshake
    //這裡比較需要注意的是: 下面開始就是走 close handshake 這邊需要檢查是否有機會讓未open handshake完成的連線走到下面的步驟,這邊看不太出來,還需要再檢查確認,後面再補充說明

    //RFC要求,close handshake要收到close + 送出一次 close算完成
    //下面會要判斷 已sent & recv close再socket.end()是因為
    //receiverOnConclude把關閉socket連線的工作交給close() 可參考websocket.js L779
    //以下處理先送close再收到close然後斷線的情形 CASE 1
    if (this.readyState === WebSocket.CLOSING) {
      if (this._closeFrameSent && this._closeFrameReceived) this._socket.end();
      //如果已經是CLOSING狀態,代表呼叫已經send close,並且設定timeout timer,所以後面不用再處理了
      return;
    }

    this._readyState = WebSocket.CLOSING;
    this._sender.close(code, data, !this._isServer, (err) => {
      //
      // This error is handled by the `'error'` listener on the socket. We only
      // want to know if the close frame has been sent here.
      //
      //socket error,不能確定close frame是否成功送出
      if (err) return;

      this._closeFrameSent = true;

      //以下處理收到close並已送出close然後斷線的情形 CASE 2
      if (this._closeFrameReceived) this._socket.end();
    });

    //
    // Specify a timeout for the closing handshake to complete.
    //
    //這邊可以理解為送出close後,timeout後強制將socket destroy,清除資源
    this._closeTimer = setTimeout(
      this._socket.destroy.bind(this._socket),
      closeTimeout //預設 30 seconds
    );
  }

在上方有個地方判斷closeFrameSent + closeFrameReceived就做socket.end,socket.end是half-close,也就是送出FIN,ws統一在close()裡判斷是否要關閉連線,close handshake無論是 recv close + send close -> 斷線 or send close + recv close ->斷線 這兩種斷線的case分別在close()處理了

另外是sender.close()收到 socket error時,因為不確定close frame是否正確送出,所以就直接返回,此時_closeFrameSent = false,流程上有以下可能

  1. 先收到close,回覆close,進入CLOSING,送出可能失敗了,對方收不到close等到timeout後就TCP斷線,本地端timeout也會強制destroy socket
  2. 先送出close,但失敗了,已進入CLOSING狀態,本地端timeout後會強制destroy socket

其實還有其他組合,例如兩邊同時送close frame,送失敗的那一方會收到close frame,但是因為進入CLOSING狀態而closeFrameSent是false,代表closeFrameSent不可能再被設定了(CLOSING狀態之後的close()就是return),這邊就依賴timeout timer處理

除了使用者主動下close()外,以及abortHandshake情形,其他的case都是被動close,無論是網路斷線,或是收到close frame後進行close handshake,以下整理出所有被動引發close的條件

receiver.on('conclude', receiverOnConclude); //收到close frame
receiver.on('error', receiverOnError); //parse frame error
socket.on('close', socketOnClose); //Emitted once the socket is fully closed. The argument hadError is a boolean which says if the socket was closed due to a transmission error.
socket.on('end', socketOnEnd); //Emitted when the other end of the socket sends a FIN packet, thus ending the readable side of the socket.
socket.on('error', socketOnError); //Emitted when an error occurs. The 'close' event will be called directly following this event.

以下分別註解對應的event listener

以下是收到close frame時

/**
 * The listener of the `Receiver` `'conclude'` event.
 *
 * @param {Number} code The status code
 * @param {String} reason The reason for closing
 * @private
 */
function receiverOnConclude(code, reason) {
  const websocket = this[kWebSocket];

  //收到close frame,停止後續的data event接收
  websocket._socket.removeListener('data', socketOnData);  
  websocket._socket.resume();//**這裡resume()是socket.pause()暫停'data'後想要繼續接收'data' event時需要的,這行可能不必要呼叫
  //收到close frame設定相關資訊
  websocket._closeFrameReceived = true;
  websocket._closeMessage = reason;
  websocket._closeCode = code;
  //如果有帶close status code/reason,就回覆同樣的內容
  if (code === 1005) websocket.close();
  else websocket.close(code, reason);
}

以下是receiver parse error時

/**
 * The listener of the `Receiver` `'error'` event.
 *
 * @param {(RangeError|Error)} err The emitted error
 * @private
 */
function receiverOnError(err) {
  const websocket = this[kWebSocket];
  //停止繼續收data event
  websocket._socket.removeListener('data', socketOnData);

  websocket._readyState = WebSocket.CLOSING;
  websocket._closeCode = err[kStatusCode]; //這邊的err[kStatusCode]是由receiver所設定的,根據parse error的結果設定不同的code
  websocket.emit('error', err);
  //直接socket destroy
  websocket._socket.destroy();
}

以下是socket斷線時,這邊的錯誤處理比較多細節須注意: socket ‘close’ event: Emitted once the socket is fully closed. The argument hadError is a boolean which says if the socket was closed due to a transmission error

/**
 * The listener of the `net.Socket` `'close'` event.
 *
 * @private
 */
function socketOnClose() {
  const websocket = this[kWebSocket];

  this.removeListener('close', socketOnClose);
  this.removeListener('end', socketOnEnd);

  //這邊需特別注意是否重複走入CLOSING狀態或是是否有機會與CLOSED衝突,主要的關鍵點在socket close event可能發生的時間點,待之後另文分析
  websocket._readyState = WebSocket.CLOSING;

  //
  // The close frame might not have been received or the `'end'` event emitted,
  // for example, if the socket was destroyed due to an error. Ensure that the
  // `receiver` stream is closed after writing any remaining buffered data to
  // it. If the readable side of the socket is in flowing mode then there is no
  // buffered data as everything has been already written and `readable.read()`
  // will return `null`. If instead, the socket is paused, any possible buffered
  // data will be read as a single chunk and emitted synchronously in a single
  // `'data'` event.
  //
  websocket._socket.read(); //這裡的目的主要是清read buffer給receiver的writable stream,是synchronous call to 'data' event
  websocket._receiver.end(); //no more data

  this.removeListener('data', socketOnData); //移除data event listener
  this[kWebSocket] = undefined;

  clearTimeout(websocket._closeTimer); //關掉timeout timer,因為socket收到close會正常關閉,不須再destroy
  //什麼時候writableState finished or errorEmitted?
  //errorEmitted: 當stream writer送出error event, 發生於receiver _write 返回error時,例如receiver parse frame error
  //finished: 當stream writer送出finish event
  if (
    websocket._receiver._writableState.finished ||
    websocket._receiver._writableState.errorEmitted
  ) {
    websocket.emitClose(); //emit close!
  } else {
   //否則就等待 error or finish event,等到後就emit close
    websocket._receiver.on('error', receiverOnFinish); //will emit close
    websocket._receiver.on('finish', receiverOnFinish); //will emit close
  }
}

/**
 * The listener of the `Receiver` `'finish'` event.
 *
 * @private
 */
function receiverOnFinish() {
  this[kWebSocket].emitClose();
}

上面因為牽涉到writable stream和socket本身的error handling,有點錯綜複雜,最主要的原因在於使用者必須要很清楚每個event發生的原因和時間點,以及呼叫了close destroy end之類的function的流程細節是什麼、會收到什麼event、順序是什麼,不然使用上一定會漏掉很多情形

socket ‘end’ event是收到TCP FIN

/**
 * The listener of the `net.Socket` `'end'` event.
 *
 * @private
 */
function socketOnEnd() {
  const websocket = this[kWebSocket];

  websocket._readyState = WebSocket.CLOSING;
  websocket._receiver.end();
  this.end();
}

下面是當發生socket error時的處理,需要注意的是程式為了只處理一次error event,移除了如果沒有listener listen ‘error’ event會導致process exit: If an EventEmitter does not have at least one listener registered for the 'error' event, and an 'error' event is emitted, the error is thrown, a stack trace is printed, and the Node.js process exits.

/**
 * The listener of the `net.Socket` `'error'` event.
 *
 * @private
 */
function socketOnError() {
  const websocket = this[kWebSocket];

  this.removeListener('error', socketOnError);
  //上面解除socketOnError要特別小心,因為nodejs的eventemitter預設的行為是如果沒有任何'error' listener,會直接process exit
  this.on('error', NOOP);
  //socket都error了,直接destroy
  if (websocket) {
    websocket._readyState = WebSocket.CLOSING;
    this.destroy();
  }
}

close部分最後會進入CLOSED狀態,這部分是透過emitClose來更新,也就是當WebSocket發出’close’ event時

  /**
   * Emit the `'close'` event.
   *
   * @private
   */
  emitClose() {
    if (!this._socket) {
      this._readyState = WebSocket.CLOSED;
      this.emit('close', this._closeCode, this._closeMessage);
      return;
    }

    if (this._extensions[PerMessageDeflate.extensionName]) {
      this._extensions[PerMessageDeflate.extensionName].cleanup();
    }

    this._receiver.removeAllListeners();
    this._readyState = WebSocket.CLOSED;
    this.emit('close', this._closeCode, this._closeMessage);
  }

close error handling還有一種情況是當使用者主動呼叫send()或其他會送出data的method,但是readyState不是OPEN,這部分在sendAfterClose處理

/**
 * Handle cases where the `ping()`, `pong()`, or `send()` methods are called
 * when the `readyState` attribute is `CLOSING` or `CLOSED`.
 *
 * @param {WebSocket} websocket The WebSocket instance
 * @param {*} [data] The data to send
 * @param {Function} [cb] Callback
 * @private
 */
function sendAfterClose(websocket, data, cb) {
  if (data) {
    const length = toBuffer(data).length;
    
    //
    // The `_bufferedAmount` property is used only when the peer is a client and
    // the opening handshake fails. Under these circumstances, in fact, the
    // `setSocket()` method is not called, so the `_socket` and `_sender`
    // properties are set to `null`.
    //
    //更新bufferAmount, ws內部有分handshake完前後,參考不同的變數,可參考下面 bufferedAmount()的實作
    if (websocket._socket) websocket._sender._bufferedBytes += length;
    else websocket._bufferedAmount += length;
  }

  if (cb) {
    const err = new Error(
      `WebSocket is not open: readyState ${websocket.readyState} ` +
        `(${readyStates[websocket.readyState]})`
    );
    cb(err);
  }
}

//這邊順便註記一下bufferedAmount,handshake完成之前(socket = null)是記在一個內部變數
  /**
   * @type {Number}
   */
  get bufferedAmount() {
    if (!this._socket) return this._bufferedAmount;

    return this._socket._writableState.length + this._sender._bufferedBytes;
  }

WebSocket還提供發送訊息的method如send ping pong,這邊介紹send

  /**
   * Send a data message.
   *
   * @param {*} data The message to send
   * @param {Object} [options] Options object
   * @param {Boolean} [options.compress] Specifies whether or not to compress
   *     `data`
   * @param {Boolean} [options.binary] Specifies whether `data` is binary or
   *     text
   * @param {Boolean} [options.fin=true] Specifies whether the fragment is the
   *     last one
   * @param {Boolean} [options.mask] Specifies whether or not to mask `data`
   * @param {Function} [cb] Callback which is executed when data is written out
   * @public
   */
  send(data, options, cb) {    
    if (this.readyState === WebSocket.CONNECTING) {
      throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
    }
    //options is optional
    if (typeof options === 'function') {
      cb = options;
      options = {};
    }
    //這邊只有判斷data是number時,轉成string
    if (typeof data === 'number') data = data.toString();

    //如果狀態是CLOSING CLOSED
    if (this.readyState !== WebSocket.OPEN) {
      sendAfterClose(this, data, cb);
      return;
    }

    const opts = {
      binary: typeof data !== 'string',
      mask: !this._isServer,
      compress: true,
      fin: true,
      ...options
    };

    if (!this._extensions[PerMessageDeflate.extensionName]) {
      opts.compress = false;
    }
    //交給sender處理,這邊的細節再留到解析sender.js再說明
    this._sender.send(data || EMPTY_BUFFER, opts, cb);
  }

}

Posted in nodejs | Leave a comment

Nodejs Events 整理

Nodejs Events module是一個很重要的元件,主要原因是在asynchronous programming,Node.js在Javascript的語言層面提供了如callback、Promise、async/await等機制,這些機制都是類似於request-response的概念,也就是先發起一個function call,等待function call回應,可以對於notification類型,特別是event driver programming,就必須使用callback方式。在browser的DOM規範中,也有定義類似的介面EventTarget

https://dom.spec.whatwg.org/#interface-eventtarget

interface EventTarget {
  constructor();

  undefined addEventListener(DOMString type, EventListener? callback, optional (AddEventListenerOptions or boolean) options = {});
  undefined removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options = {});
  boolean dispatchEvent(Event event);
};

主要就是定義註冊管理使用callback function的介面

event driven programming的特色就是將event透過handler/listener function來處理,並且event的時間點不確定,所以handler/listener callback function的角色是被動的,有什麼event過來就處理什麼event,event可以是新的資料,譬如說網路socket接收到的內容,也可能是狀態的改變,譬如說XMLHttpRequest的progress event。

在瀏覽器的javascript,因為UI有各種IO原始事件(滑鼠、鍵盤),進而在瀏覽器產生對應出衍生的UI事件。在Nodejs更多的IO操作也是透過event傳達狀態改變,例如’open’, ‘ready’等。或是如Readable stream的’data’

在Node.js中,按照習慣,會發出event的object繼承於EventEmitter,透過.on(eventName, eventHandler/Listener) 來接收event,並且eventName建議是以camel case的方式命名,只是Nodejs大部分的event不需要用到第二個字,所以常見的都只是小寫event name

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
  console.log('an event occurred!');
  //this === myEmitter 
});
myEmitter.emit('event', 'a', 'b');

這裡節錄一段官方文件的範例,透過extends EventEmitter方式,使物件具有收發event的能力,emit透過 function argument,傳遞eventName之外的其他資訊。

.on呼叫的callback不應該是async function,並且callback被呼叫的順序是按照註冊的順序被 synchronously呼叫,另外也有一個.once 的API,主要用於handler/listener只一次性處理event,可參考另一篇文的說明

在Node.js中,錯誤處理有兩類,一種是js exception機制,synchronous function call內部透過throw,可以將錯誤傳遞出來,另外一種是callback中帶err argument,但這些都是function call流程很明確的時候。event driven programming中,因為event本身的特性不是線性流程的處理,不能透過上述兩種方式將錯誤帶出,事實上,也可以想成錯誤是不定期(不預期)的時間點,因此也是一種event。

在Node.js中使用event name為’error’的convention,並且Node.js本身有提供’error’ 的default listener: print call stack + exit

另外需注意的是每個event name的event listener預設上限是10個,可在setMaxListeners設定

對於EventEmitter,常用的用法如下

  • class XYZ extends EventEmitter
  • xyz.on(‘event name’, callback) / xyz.addListener,同一個一樣”callback”可以加多次
  • xyz.off / xyz.removeListener(‘event name’, callback),移除最近一次加的”callback”
  • xyz.emit(‘event name’, arg1, arg2…)

其中 on = addListener, off = removeListener

以上簡介Node.js events最基本的用法,在後續版本有多一些處理promise async function的API(events.once v11.13.0、captureRejection v12.16.0),以及增加EventTarget interface(v14.5.0)等,考量到相容性,一般就比較少用。

Posted in nodejs | 1 Comment

WebSocket protocol整理

WebSocket作為在HTTP下一個很重要的雙向通信延伸的protocol,HTTP 1.0/1.1的設計是half-duplex,也就是同一個時間只會有單向傳輸(request->response),雖然HTTP 1.1有支援http pipelining,但本質上他還是單向傳輸的的架構(第二個request不用等到第一個response收到就可以發出,但是response order還是根據request),並且這項功能在大部分的client/server實作不完整

WebSocket出現於2009年左右(並於Chrome 4開始實作支援),在此之前要在Web上透過HTTP protocol進行雙向通信,需要用一些技巧模擬出來

因為HTTP 通信是half-duplex,從web browser client的角度,browser可以知道什麼時候要send資料,隨時可以由browser發起http request,而recv則是由server傳來的資料,但是在http request-response的架構下,因為不知道server什麼時候有資料,最簡單的做法就是要定期去polling,這個在早期的聊天室系統都是這樣處理,每隔幾秒鐘就發一次http request更新最新的內容,但是這樣做有個缺點 – 不即時

即時性的問題在後來發展出hidden iframe以及long polling兩種做法,以兩種做法解決問題主要是因為browser的限制(browser只支援單純的http request-response),雖然在HTTP協定中其實有定義可以做雙向通信的,像是HTTP/1.1 CONNECT(主要用在tunnel proxy情境),或是HTTP/1.1 Upgrade header,但是tunnel或是upgrade完的通信方式要由app決定。

hidden iframe的做法算是巧妙地利用browser load javascript的行為,他透過inline iframe建立起一個隱藏的iframe,在iframe裡面load一個特別的網頁,那個網頁會一直傳<script>,將要server通知的內容即時透過<script>結合javascript,在裡面嵌code和data,因為browser收到script會立即執行,並且按順序執行,透過巧妙地安排script內容,將訊息傳出來給parent page,因為網頁還沒load完,連線就會一直持續

long polling則是透過http XMLHttpRequest方式,連上server後,在沒有新的event data情況下,server就掛起連線,等有資料再response,當client收完資料後連線結束,再馬上發起新的long polling request,這個好處是可以即時的收到event data,缺點是XMLHttpRequest browser有一些限制(例如cross origin等問題),以及每次HTTP request的header overhead。

以上兩種方式都是因為既有的HTTP限制下產生的做法,HTTP的連線甚至在browser還有其他的限制,像是同一個時間對同一個site最多有幾條連線(concurrent connections)等,如果掛太多long polling連線,會占住正常的使用

WebSocket的出現讓Web環境下雙向通信變得容易,也不再需要用特別的方式處理了,整體來說,WebSocket的設計是基於既有HTTP的架構下,並且考慮到一些既有HTTP component的相容性(如proxy)來設計,並且在某種程度上考量到舊有component的相容性(如proxy),以及加上framing的機制,使得可以在TCP streaming的傳輸上以不限制大小的message方式來進行,另外也有限度的考慮了一些安全性(特別是針對XMLHttpRequest)

WebSocket的規格歷經多代演進,最後在RFC 6455標準化

https://en.wikipedia.org/wiki/WebSocket

在使用library時,我們可以看到所support的版本

例如: https://github.com/zaphoyd/websocketpp

  • Full support for RFC6455
  • Partial support for Hixie 76 / Hybi 00, 07-17 draft specs (server only)

在server的實作常會見到同時支援hixie, hybi第幾版之類(hixie這邊的命名是與原始起草規範Ian Hickson有關,hybi的bi應是指bidirection。另可參考: https://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 )的,主要的原因就是瀏覽器作為client連線時,因為browser的版本不同,支援的websocket版本也不同,所以如果考量到不確定target使用者瀏覽器版本時,常需要對相容性做處理,相對來說,如果websocket僅是作為app之間的通信或是client版本可控時,server只要實作RFC就足夠了。

以下整理一下RFC 6455中提到的資訊

protocol分成兩部分

  • handshake (open handshake、close handshake)
  • data transfer

Open Handshake (主要整理Section 1.3)

open handshake基本上就是透過http request – response的方式在header內傳遞相關資訊,以下從RFC節錄

The handshake from the client looks as follows:

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

The handshake from the server looks as follows:

        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat

實測的範例,client side使用nodejs ws,server side使用websocketpp

GET / HTTP/1.1
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: vDJK2hgGUHeVDSXRDYeAEw==
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Host: 127.0.0.1:80

HTTP/1.1 101 Switching Protocols
Connection: upgrade
Sec-WebSocket-Accept: tjloVFK8w9BH5RxPicVb03W3Ams=
Server: WebSocket++/0.7.0
Upgrade: websocket

在request、response header中沒有Content-Length的欄位,在RFC 7230 3.3.2有說明

A server MUST NOT send a Content-Length header field in any response with a status code of 1xx (Informational) or 204 (No Content).

對於client則是因為GET本身不帶body,也是method不預期有body,所以不需要帶Content-Length欄位

A user agent SHOULD NOT send a Content-Length header field when the request message does not contain a payload body and the method semantics do not anticipate such a body.

附帶一提的是原來HTTP常見的應用如Cookie、Authorization等欄位也可使用,使得既有HTTP框架下的驗證機制可以reuse, 除了一般HTTP request既有的header外,WebSocket 另外定義了Sec-WebSocket-Protocol、Sec-WebSocket-Extensions作為protocol的擴展

Sec-WebSocket-Protocol是subprotocol selector,client如果提供支援的subprotocol(可能多筆),而server就必須回應一個支援的subprotol,類似的設計在SSL handshake也可以看到,ClientHello會送出多組支援的cipher suite,server只會回應一組選擇的cipher suite

subprotocol指的是application-level protocols layered over the WebSocket Protocol,也就是在WebSocket protocol架構下的application protocol,可以想成WebSocket提供了基礎的通信收發,而應用端具體地定義message的內容與回應方式,相關資訊可以參考IANA的列表

https://www.iana.org/assignments/websocket/websocket.xhtml#subprotocol-name

這邊可以看到message broker常見的protocol如amqp、mqtt都有各自定義websocket subprotocol

Sec-WebSocket-Extensions 則是定義在protocol level的extension,如如permessage-deflate可參考 https://tools.ietf.org/html/rfc7692 Compression Extensions for WebSocket

上面handshake中有兩個欄位: Sec-WebSocket-Key 和Sec-WebSocket-Accept是一串亂碼,這是base64 encoding,內容並不重要, 這邊的 Sec-WebSocket-Key 跟security或是key沒什麼關係(Sec-名稱的原因後面詳述), 他主要的目的是讓client確定server是一個WebSocket server,計算方式如下

Sec-WebSocket-Accept = base64(sha1( Sec-WebSocket-Key + “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”))

以上面的例子就是 把 dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 這串字做sha1然後將hex octect做base64

從上面可以看到這邊的計算完全是公開的,單純只是讓client確認server看得懂 Sec-WebSocket-Key 並且回應正確的 Sec-WebSocket-Accept,是一個WebSocket server,事實上這步應該是多餘的,但如果從防止XMLHttpRequest request模擬WebSocket client連接WebSocket Server的角度來看,又有其用處。因為browser會阻擋 javascript設置Sec-WebSocket-Key, Sec-開頭的是forbidden name之一, 這也是為什麼欄位名稱是Sec-開頭,在瀏覽器中不可被programmatically修改設置,可參考 https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name

另外client side random Sec-WebSocket-Key 也可以避開老舊的proxy cache機制

Sec-WebSocket-Version在RFC版本中 = 13

在handshake中,成功的handshake,server一定是回傳status code 101(switch protocol),如果是其他的status code就按照原有的意義去解讀,譬如說404 not found, 403 forbidden, 302 redirect等,如果不是101,client不應進行後續的websocket傳輸

server要回傳的欄位還包括

  • Upgrade: websocket
  • Connection: Upgrade
  • Sec-WebSocket-Accept:

而其他如Set-Cookie,在RFC中也允許server handshake response回傳。

Closing Handshake(主要整理Section 1.4)

當要連線結束時,一方先送出close control frame,另一方收到close要求時,也送出close frame,在RFC中同時也關注simultaneous close的處理,也就是雙方都同時送出close,基本的原則就是

  • 送出close之後不再送任何data,並且等待收到close,再斷線
  • 收到close之後不再收任何data,並且如果收到close時還沒回應close,要送出close,再斷線

事實上要斷線應該可以靠TCP的close handshake(4-way handshaking)就好,在RFC提到需要在WebSocket層加上close的handshake主要是因為可能在整體架構上有proxy或是其他intermediaries,沒有完整處理好TCP斷線,透過在application layer做end-to-end的close handshake可以避開這個問題(當然這個前提是TCP連線正常時),或是說可以很明確的讓雙方知道意圖,主動將連線中斷送出close之後不再送任何data,並且等待收到close,再斷線

Data transfer 主要整理 Section 5 與 Section 6

WebSocket介面是常以message為單位,而protocol傳輸是以frame為單位,這裡的frame跟網路transport如tcp或是更底層的ethernet無關,純粹是application層的架構上的定義,並且有些通用的概念也不同,雖然這邊說以frame為單位,但是他不像switch一樣,把ethernet frame完整的接收才store and forward,在RFC 6455 page 35有提到,可以在frame還沒接收完時,邊收邊處理。

RFC 6455總共定義了6種frame type(在Section 5: binary、text、continuation、close、ping、pong),而message由frame組成,在application 層只需要關注 兩種message type: text(UTF8)、binary。

當handshake完成後,就進入data transfer的狀態,這時候雙方可以各自傳送資料, frame在WebSocket算是滿重要的部分,他可以將大的message切割成很多小的chunk傳輸,使得在protocol設計上可以透過multiplex機制來支援同時傳輸兩個大的message成為可能,並且在protocol設計可以做更彈性的控制,例如在frame加入checksum,當發生錯誤時,可以做recover或要求重傳或斷線,如果checksum是做在message level,會導致要等到整個message傳完後,才知道是否有錯。上面提的是framing的重要性,在WebSocket中他的frame沒有檢查正確性的機制,依賴的是底層TCP的checksum或是在AP層設計的檢查機制。在RFC 6455也沒有定義multiplex的機制,而是保留在extension去擴展。 (可參考 Section 5.4)

另外需要注意的是,在RFC並沒有定義frame的大小或是切割message的規則,這部分是由傳輸的各方自行處理,像是chromium是以131KB來切割(參考: https://bugs.chromium.org/p/chromium/issues/detail?id=517090)

FIN: 代表此frame是message的最後一筆frame (frame-fin)

RSV1 RSV2 RSV3是保留flag

opcode 是 frame type,共4個bit 可代表16種不同的frame type,RFC定義了6種,分別是

  • continuation frame(0x0)
  • text frame(0x1)
  • binary frame(0x2)
  • connection close(0x8) – control frame
  • ping(0x9) – control frame
  • pong(0xA) – control frame

MASK 是傳輸的payload內容需不需要做mask,在RFC中,client->server要mask,server->client不用mask,mask是透過Masking key進行xor運算

Payload length: Extension data + Application data的長度。編碼採用為可變長度的encoding、network byte order,7 bits, 7+16 bits, or 7+64 bits。如果payload length是0-125就只用7bit,如果payload length 是126 – 65535就將第一個7bits指定為126,接下來的16bits指定為對應的值,如果payload length是65536 – (2^64 – 1),就將第一個7bits指定為127,接下來的64bits指定為對應的值。另外規範要求值的表示法唯一,這邊可以看到frame定義的payload length可以大到2^64-1(16EB-1),所以理論上任何大小的message都可以塞在一個data frame內,只是缺點就如前面所提的:

  • protocol設計上會少了很多彈性
  • 在實作上,在有限的buffer size,過大的message無法一次放進buffer中
  • payload length需要是一個已知的payload大小

Masking-key (0 or 4 bytes),client到server的傳輸才會帶masking key

從上面可以看到,data frame並沒有指定原始message的大小,在RFC規範中,甚至可以在一開始message不知道大小的情況進行傳輸,只要fragment payload length知道即可,在fragmented messages裡面描述的data frame行為是

  • 第一個data frame會有op code (例如 text /binary) + FIN=0
  • 後續的data frame會是 op code = 0(continuation) + FIN=0
  • 最後一個data frame是 op code = 0 + FIN =1

這種設計需要資料流是連續且按照順序的(沒有serial number),並且很明顯是無法支援multiplex的,除非另外紀錄message sequence number之類的資訊。當然,額外的資訊可透過extension來定義,extension提供了方式可以解讀payload中 extension data的意義並做對應的處理,5.4也有明確提到:

The fragments of one message MUST NOT be interleaved between the fragments of another message unless an extension has been negotiated that can interpret the interleaving.

雖然前面提到fragmented message的data frame是連續的,但在RFC有定義例外,就是control frame可以插隊在fragmented message中間,當然control frame本身不能再切割,這邊主要的設計應該是為了讓像close handshake可以立即被處理,或是keep alive的機制可以立即被回應,所以在RFC中也作了對應的要求

An endpoint MUST be capable of handling control frames in the middle of a fragmented message.

control frame共有 close、ping、pong,並且規範中要求 control frame的payload length <= 125

close frame: 可以帶payload body [2 byte status code – uint16 + [reason – utf8]],這邊的payload也要符合mask rule,如果沒有帶status code,則對於API層來說,close status code = 1005,如果沒有帶reason,則對於API層來說 ,reason為empty string

規範也提到close frame response的原則: 收到close frame時,如果先前還沒有送出close frame,才要送出close frame,並且建議回覆相同的close code

When sending a Close frame in response, the endpoint typically echos the status code it received.

當送出close並且收到close後(或是收到close回覆close後),在WebSocket protocol層就算close了,但是底層的TCP連線,在規範中也定義的建議作法

  • server立即斷線(此時在TCP層會送出 FIN給client)
  • client等到收到斷線(recv() = 0, FIN packet),或是自訂一個timeout值後斷線

ping frame、pong frame: 作為keep alive的機制,或是確認對方是否responsive,可帶application data,ping、pong的application data要相同(echo ping),pong可以只選擇最近一筆的ping回應

data frame 包括 text frame、binary frame

text frame: payload UTF8 text,在8.1. Handling Errors in UTF-8-Encoded Data提到,如果收到的text不是utf8要做Fail the WebSocket Connection (fail the websocket connection要做的事可參考7.1.7)

binary frame: 內容是application自行解釋

接收端收到data frame,將payload的application data連接組成為一個message,直到frame-fin,frame-fin後面的data frame就是新的message

Extension 主要整理5.8

在frame的欄位中,op code = 0x3-0x7, 0xB-0xF保留未用,以及RSV1, RSV2, RSV3的flag,payload的Extension data區域都提供了protocol擴展的可能性,在RFC中,上述的保留op code、flags、或是以payload的內容都可拿來進行擴展,

目前有廣泛使用的extension只有 permessage-deflate ,相關資訊也可看IANA的registry https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name

在nodejs ws 套件中的 permessage-deflate extension,server side預設是關起來的 (https://github.com/websockets/ws 參考WebSocket compression一節)

上面的紅框處是從chrome截下來的,extension list是以 逗號 , 分隔,分號; 後面是extension的參數,例如 client_max_window_bits是parameter,可參考 rfc7692 7.1.2.2 (https://tools.ietf.org/html/rfc7692#section-7.1.2.2)

另外是當指定多個extension時處理的方式,在RFC 6455 9.1 p.49也有說明,有先後的處理順序

if there are two extensions “foo” and “bar” and if the header field |Sec-WebSocket-Extensions| sent by the server has the value “foo, bar“, then operations on the data will be made as bar(foo(data)), be those changes to the data itself (such as compression) or changes to the framing that may “stack”.

Closing The Connection 主要整理Section 7

在RFC裡,明確的描述一些操作的細節,特別是closing the connection這一個Section,因為如handshake或是傳輸中有一些不預期的內容時,必須作錯誤處理,最常見的方式就是關閉連線,在RFC中,client主動關閉連線只有兩種情形會發生,1.錯誤處理 2. application api主動呼叫close (參考p44 7.2.1)

錯誤處理的關閉連線又有分直接斷線或是經過Closing Handshake的方式,Section 7定義了一些操作或狀態,以下做一些整理

Start the WebSocket Closing Handshake

送出close control frame,當送出close frame也收到close frame,就進行Close the WebSocket Connection

The WebSocket Closing Handshake is Started

當收到或是送出close frame,就進入The WebSocket Closing Handshake is Started的狀態,此時WebSocket connection是CLOSING狀態

The WebSocket Connection is Closed

當TCP連線關閉時,就是 The WebSocket Connection is Closed的狀態,WebSocket connection是CLOSED狀態,如果TCP連線關閉時,已經完成closing handshake,則是close cleanly,如果TCP連線關閉時沒有收到close frame,則傳給API層的close status code = 1006

Close the WebSocket Connection

當提到 Close the WebSocket Connection 是指close TCP connection,在TCP連線以建立下,對於server side是指 立即關掉,對於client side是指等待server close在關閉

Fail the WebSocket Connection

Fail the WebSocket Connection 大部分定義在open handshake操作,並且是client的行為(因為server如果再open handshake發生問題,可以透過http status code回應),而當text不是UTF8時,也會進行此操作,如果WebSocket connection還沒有established,就進行Close the WebSocket Connection,如果已經established,就送出close frame + Close the WebSocket Connection(不處理後續的frame,包括接收close frame)。這裡RFC描述得比較簡單,整理一下RFC內所有提到 Fail the WebSocket Connection 的case

  1. p15. Open handshake時,client發現URI invalid (before WebSocket connection established)
  2. p17. client建立TCP連線失敗或是proxy return error (before WebSocket connection established)
  3. p19. Open handshake,client接收的handshake response Upgrade != websocket (before WebSocket connection established)
  4. p19. Open handshake,client接收的handshake response Connection != Upgrade (before WebSocket connection established)
  5. p19. Open handshake,client接收的handshake response Sec-WebSocket-Extensions不預期 (before WebSocket connection established)
  6. p20. Open handshake,client接收的handshake response 內容不符4.2.2的描述 (before WebSocket connection established)
  7. p44. client發生TCP連線中斷underlying transport layer connection is unexpectedly lost, the client MUST Fail the WebSocket Connection.
  8. p.48. client/server 預期收到UTF8但不是UTF8的encoding sequence時

不過RFC對於操作並沒有完全的描述清楚,例如7.2.2 提到的Abort the WebSocket Connection並沒有在該文件內出現,另外是還有一些case沒有明確定義是否歸類在fail the websocket connection, 例如 5.1 p27

server MUST NOT mask any frames that it sends to the client. A client MUST close a connection if it detects a masked frame. In this case, it MAY use the status code 1002 (protocol error) as defined in Section 7.4.1.

這邊只提到說要送close code 1002,但沒有提到要使用fail connection還是start close handshake。當然如果像收到不預期(mask錯誤)應該歸類在 Fail the WebSocket Connection

Predefined close status code 整理7.4.1,這邊只列出nodejs ws 有在使用的status code或是常見的code

  • 1000 normal closure
  • 1002 protocol error
  • 1005 close frame without status code (for API)
  • 1006 reserved value. It is designated for use in applications expecting a status code to indicate that the connection was closed abnormally, e.g., without sending or receiving a Close control frame. 這個在nodejs ws是WebSocket物件的預設值 (for API)
  • 1007 invalid UTF-8 sequence
  • 1009 frame payload size limit exceeds,在nodejs ws是2^53-1,跟Javascript integer MAX_SAFE_INTERGER有關

在nodejs ws的status code validation可以清楚的看到control frame可以帶的status code,像是1005、1006不應出現在control frame中,它是直接由程式內部判斷狀態產生的

exports.isValidStatusCode = (code) => {
  return (
    (code >= 1000 &&
      code <= 1014 &&
      code !== 1004 &&
      code !== 1005 &&
      code !== 1006) ||
    (code >= 3000 && code <= 4999)
  );
};

Security Considerations 主要整理Section 10

在RFC提到檢查Origin的機制,這段是做在server site,對於client side,原本有一些如cross origin 的阻擋機制,很可惜他主要是針對XMLHttpRequest,沒有規範WebSocket(參考https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)。所以要防止惡意的網頁存取必須要在server side加上Origin機制的檢查,需要注意的是cross origin的機制是保護browser的使用者,當他瀏覽到惡意的網頁,惡意的網頁可能可以建立起WebSocket連線到指定的site取得相關的資料,正常來說,browser可以將這類的連線阻斷,不過在CORS並沒有限制WebSocket,所以相關的限制必須做在server side(透過檢查Origin,這個header在網頁中是無法被修改的)。

其他security機制的幾個重點:

masking: masking的用途主要是防止proxy cache poisoning,在連線過程中可能會經過多個proxy,而有機會中間的proxy實作有缺陷,導致惡意的client-server安排可以將它的cache代換其他內容,透過在payload裡面送出如HTTP GET的request,安排好的server或被控制的server在WebSocket返回HTTP GET response,雖然理論上WebSocket還有frame的機制,payload前面有包含一些frame的header bytes,資料流的pattern不應該會被判斷成HTTP,但是可能有些實作是直接掃連線內容作切割(應該跟HTTP/1.1的Keep-alive connection機制有關),而沒有檢查所有完整資料內容的正確性 (也可參考 https://security.stackexchange.com/questions/36930/how-does-websocket-frame-masking-protect-against-cache-poisoning 的回答),所以透過client-server的mask,可以讓client的request內容不被有問題的proxy所看懂。至於server到client就不一定需要mask了,因為proxy更新cache主要是根據http request做對應,如果看不懂request,拿到response也就無從更新了

但是client -> server payload加上mask還不夠,RFC中還提到要求mask key的選取要足夠亂數,雖然在browser下透過javascript是拿不到mask key的,但這個主要是防止惡意的使用者猜到key(譬如說總是用同樣的key),直接在控制的client頁面建立WebSocket連線送出binary message,而message的內容mask完剛好是一個HTTP GET request,這樣就有可能導致proxy cache poisoning。

當然,這邊所有的防護都是針對,被控制的server + 網頁的使用者來思考的,被控制的server: 代表可能可以在上面修改WebSocket server的回傳,並且讓使用者load到惡意的網頁程式,使得使用者瀏覽網頁時,惡意的javascript程式執行,透過連到被控制的server,在WebSocket的傳輸內容動手腳,進而有機會汙染proxy cache(需要那台proxy實作有問題 + cache miss or expired)。

如果是非網頁的application,那以上的限制更是無用了。事實上,以上的機制對於惡意網頁程式,也只能防止一部分的攻擊,因為如果server被控制了,mask key也就可以即時的從WebSocket server取得再傳給client端,接下來惡意的程式一樣可以產生想要的data pattern。不過至少mask key的機制透過簡易的設計,大幅減少意外汙染proxy cache的可能性。

其他補充:

4.1 提到header沒有 Sec-WebSocket-Protocol 代表沒有 subprotocol時,在一些程式的API是透過指定subprotocol: null來描述

The Subprotocol In Use is defined to be the value of the |Sec-WebSocket-Protocol| header field in the server’s handshake or the null value if that header field was not present in the server’s handshake.

4.2.2對於subprotocol server如果無法支援client的要求,就是只支援null,也就是回應不帶 Sec-WebSocket-Protocol header field

另外4.2.1提到一些server端對於open handshake request的要求

  • GET verb
  • header field Host
  • header field Upgrade = websocket (case-insensitive value)
  • header field Connection = Upgrade (case-insensitive value)
  • header field Sec-WebSocket-Key
  • header field Sec-WebSocket-Version = 13
  • header field Origin (optional)
  • header field Sec-WebSocket-Protocol (optional)
  • header field Sec-WebSocket-Extensions (optional)
  • other optional header fields

4.2.2 (5) 描述server端對於open handshake response(accepted)要求的header欄位

  • status line 101
  • header field Upgrade = websocket
  • header field Connection = Upgrade
  • header field Sec-WebSocket-Accept
  • header field Sec-WebSocket-Protocol (optional, only one is selected)
  • header field Sec-WebSocket-Extensions (optional, can have multiple value or split in to multiple header field instances)

open handshake階段,在server side,送出handshake response後,此連線狀態為OPEN,server可以開始send/recv data,對於client side,收到server response並驗證成功後,連線狀態也是OPEN,至於client side要驗證的server response有哪些呢?在4.1的後半段有描述 The client MUST validate the server’s response as follows:

  • status code = 101
  • check header field Upgrade = websocket (value case-insensitive)
  • check header field Connection = Upgrade (value case-insensitive)
  • check header field Sec-WebSocket-Accept value
  • check header field (if exists) Sec-WebSocket-Extensions value is in the requested handshake
  • check header field (if exists) Sec-WebSocket-Protocol value is in the requested handshake

4.3 4.4 Sec-WebSocket-Version-Server = 1#version 在ABNF描述中,server response可返回多筆version number主要是用在當client handshake request要求的version server不支援時,server可返回所支援的version list

client handshake request example from RFC

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade

Sec-WebSocket-Version: 25

server response example from RFC

HTTP/1.1 400 Bad Request

Sec-WebSocket-Version: 13, 8, 7

HTTP/1.1 400 Bad Request

Sec-WebSocket-Version: 13
Sec-WebSocket-Version: 8, 7

5.4 有關 fragmentation,裡面多次提到intermediaries,他可以根據需要重組重切message,但是對於看不懂的extension或是不確定該條WebSocket連線所使用的handshake,就不能隨便更動fragmentation

參考:

Posted in Network | 1 Comment

linux ls -l

整理一下 ls -l 相關的內容意義

ls -l 預設顯示第一欄是file type/ permissions / attributes, 第一各字元是file type,d代表 directory,l代表 symbolic link, 在linux中總共有7種file type,分別是

  •  : regular file
  • d : directory
  • l : symbolic link
  • c : character device file
  • b : block device file
  • s : local socket file
  • p : named pipe

前三個是一般檔案系統檔案相關,device file是io device相關,socket、pipe是IPC相關

socket檔案可以在程式有使用unix domain socket 時看到,例如docker在 /run/docker.sock

再來是rwx分別代表 owner、group、other的存取、執行權限permission,這部分就不多提,沒有set時為 ,另外當x在directory時代表可被存取(否則cd 進該directory會permission denied)

最後是 setuid/setgid/sticky的flag,這個flag是跟執行權限有關。setuid、setgid代表執行時以file user/group owner身分執行。如果是flag設定在directory時,setuid不作用,setgid代表在該目錄下建立的檔案group id會設定為該目錄的group id

sticky bit 最早是用來設定執行檔執行結束後仍駐留在記憶體中,目前大多數的UNIX系統已經不這樣使用,目前sticky bit的作用是限制只有owner可以delete or rename的操作,對於directory則是限制該目錄下的file必須是owner才能刪除或是rename

drwxrwxrwt. 10 root root 135 Feb 19 15:07 tmp

如上面 /tmp 在 other項目的 x 處顯示 t 代表設定sticky,因此/tmp下的檔案只有 該檔案的owner可刪或rename(注意是/tmp下的檔案,而不是/tmp本身,/tmp本身的rename, delete 權限由/的permission決定),因為/tmp要讓所有人可存取,所以設定為rwxrwxrwx,如果沒有設定stickybit,rwxrwxrwx會讓所有人都可以刪除/tmp下的所有(第一層)檔案

當然,如果刪除的 第一層 檔案是不為空的目錄時,該子目錄的下的檔案存取權限是由該子目錄的permission決定

在ls -l中 顯示 s 在 user,代表setuid +x ,如上面的su,代表一般的使用者執行su會以root的身分來執行身分切換,而s顯示在group區,代表setgid + x,如screen是以screen group身分執行,顯示在others的t代表sticky+x

值得注意的是sudo的權限是設定成 4111 (set uid + x),只有x沒有rw,這樣還是可以執行的,因為binary執行不需要讀取權限(https://unix.stackexchange.com/questions/214274/purpose-of-permissions-such-as-0111-or-0333)

chmod octet setuid = 4,setgid = 2,sticky = 1

第一欄的最後面有個dot,這是指有設定selinux security context,可用 ls -Z查看

再來ls -l 第2欄是link count,對regular file,是inode hard link數

對目錄來說,link count是指link到該目錄inode的數目,空的目錄為2 如上面的/srv,其中一個是目錄下 . 指向自己,另一個是來自parent directory的指向

當有subdirectories時,因為subdirectory下的 .. 會指向parent inode,所以一個subdirectory會貢獻一個link count,因此如 /var 顯示20,代表它下面有18個subdirectories

/var下有18個subdirectory,link count 20,差異的2個,其中一個是 . 指向自己,另一個是parent directory / 指向/var inode

參考:

Posted in System Administration | Leave a comment

sudo note

在Linux中,使用者取得root權限執行可以透過sudo 或是 su root (直接切換成root),差別是sudo 是不需要知道root密碼的,並且可以透過定義sudoer限制可使用的命令。修改/etc/sudoers,透過visudo command

在CentOS中, 定義了 wheel group有sudo權限,可將使用者直接加入wheel群組

usermod –aG wheel username

上面除了定義wheel群組,也定義了root使用者,當然root本身就具有最高權限了,會在sudoers定義最主要是因為讓root不會因為用sudo命令被擋下來

例如將 root那行註解後,(以root身分) 輸入 sudo ls會失敗

在Ubuntu中,則是定義了admin 和 sudo group,不過admin group是早期Ubuntu 11及之前在使用的,在Ubuntu 12後不使用

如過要讓使用者sudo 時不需再輸入密碼,可以在檔案的最後面加上

user ALL=(ALL) NOPASSWD:ALL

如果不在最後面加上,則會以最後一筆match的設定apply,以ubuntu預設建立的使用者來說,會自動加進sudo group,所以會導致上面的設定被override

When multiple entries match for a user, they are applied in order. Where there are multiple matches, the last match is used (which is not necessarily the most specific match).

https://askubuntu.com/questions/100051/why-is-sudoers-nopasswd-option-not-working

另外是上面列的 %sudo ALL=(ALL:ALL) ALL 依照順序指的是

%sudo 群組 在 ALL 不限制host上,可以以 ALL 不限制run as user,ALL不限制run as group,ALL不限制執行command

ALL不限制host通常是有類是ldap的架構下才會有意義,不然一般檔案都是在單機local上設定,sudoers也只會作用在該台機器上

以上可參考 https://linux.die.net/man/5/sudoers Runas_Spec與Tag_Spec的說明

sudoers可以設定限制使用者執行的身分、群組,使用者在sudo時,可以指定-u or -g ,不指定-u 預設是root,指定-u不指定-g時預設是指定user的primary group,不指定-u 但指定-g時會用原有user的身份但group切換執行

參考:

Posted in System Administration | Leave a comment

docker-compose project name

docker-compose透過docker-compose.yml檔案描述的方式,操作docker container,特別是multi containers的情境,讓使用和管理上變得很容易,不過需要注意的是 docker compose是透過project name來識別要操作的container,例如啟動、停止等。

一般來說,啟動就只要下docker-compose up就可以,不同的container是isolated,但是在某些情境,對於不同的docker-compose.yml下 docker-compose up有可能會互相干擾,例如以下,起始一個container name為testm2 docker instance

version: "3.9" # optional since v1.27.0
services:
  app2:
    container_name: testm2
    image: centos:7
    command: sleep 1d

在另一個資料夾下的啟動testm3

version: "3.9" # optional since v1.27.0
services:
  app2:
    container_name: testm3
    image: centos:7
    command: sleep 1d

上面出現了recreating testm2,查看container list,testm2消失,取代的是testm3

而原來testm2也顯示被踢掉了

這表示testm2被停掉,主要的原因是project name相同

直覺上會覺得container name不同應該就會不會衝突,事實上docker compose是透過project name來管理,參考: https://docs.docker.com/compose/

The default project name is the basename of the project directory. You can set a custom project name by using the -p command line option or the COMPOSE_PROJECT_NAME environment variable.

因為testm2 testm3都是放在config資料夾(雖然絕對路徑不同),所以project name都是資料夾名稱config,也可以透過 docker inspect test3 看到 docker compose在docker container object記下的labels metadata如下

改成command line加上project name


再查看兩個container都被建立起來

docker-compose是相當方便的工具,但是在使用上project name這個小細節需要特別小心,因為他可能造成使用者不預期停掉docker container,這對於在線環境是特別危險的。

Posted in System Administration | Leave a comment

yum repo sync to local

要複製remote yum repo到local有兩種方式

  • rsync
  • reposync

但不是所有的remote repo都提供rsync存取權限,例如vault.centos.org就無法使用(https://lists.centos.org/pipermail/centos-mirror/2013-April/007069.html 裡面提到將rsync關掉了)

https://vault.centos.org/readme.txt

The Following External Vault mirrors (not monitored by the CentOS Infra team !) also provide direct downloads for all content, including isos and rsync access:

USA: 

http://archive.kernel.org/centos-vault/
rsync://archive.kernel.org::centos-vault/

Europe: 

http://mirror.nsc.liu.se/centos-store/
rsync://mirror.nsc.liu.se::centos-store/

當遠端可使用rsync時,可透過以下命令

rsync -avP rsync://linuxsoft.cern.ch/centos-vault/6.10/updates/x86_64/ /data/centos/6/updates/x86_64

分別將os, updates, extras等同步到local dir

也可以直接rsync linuxsoft.cern.ch/centos-vault/6.10/ 排除掉一些pattern如i386、Source

rsync -avP –exclude=i386 rsync://linuxsoft.cern.ch/centos-vault/6.10/ /data/centos/6

另外的方式是透過reposync,他會讀取/etc/yum.conf,例如以下要同步updates repo

yum -y install yum-utils createrepo reposync
reposync –repoid=updates –arch=x86_64 -l -g

關於CentOS 6 repo sync要注意的是目前很多mirror site都是直接mirror官方的內容,但官方CentOS 6的repo都只有一個readme顯示不再提供

This directory (and version of CentOS) is deprecated. Please see this FAQ concerning the CentOS release scheme: https://wiki.centos.org/FAQ/General Please keep in mind that 6.0, 6.1, 6.2, 6.3, 6.4 , 6.5, 6.6, 6.7, 6.8 , 6.9 and 6.10 no longer get any updates, nor any security fix’s. The whole CentOS 6 is *dead* and *shouldn’t* be used anywhere at *all*

http://mirror.centos.org/centos/6/readme

CentOS6 可用的repo備份目前只有如vault.centos.org或是其他linuxsoft.cern.ch、archive.kernel.org有提供,但是cern的速度很慢,kernel.org常常會顯示max connection reached,備份時可以透過reposync先備份Packages資料夾(vault.centos.org的速度還算快),再用rsync將其他drpms、repodata等資料夾同步

local yum repo只需要設定好httpd,client端將yum.repos.d下的.repo指向對應的http路徑即可

參考:

Posted in System Administration | Leave a comment

CentOS 6 docker yum更新

在centos 7環境下跑centos 6 docker image

docker run -it centos:6

下 yum update 報錯,顯示無法retrieve repomd.xml

確認一下原因,主要是因為 centos 6在2020 Nov就停止支援了,參考https://forums.centos.org/viewtopic.php?t=72710

Red Hat have pulled the plug on RHEL 6.x as of Nov 30th 2020 and as a result CentOS 6 is now a dead version. The online yum repos for CentOS 6 have been archived to vault.centos.org and there will be no more updates to it at all, ever.

yum repo archive到 vault.centos.org ,所以須將yum相關config修改,直接修改/etc/yum.repos.d/CentOS-Base.repo

將所有mirrorlist註解掉,改成baseurl指向vault.centos.org 就可以了

內容類似在/etc/yum.repos.d的CentOS-Vault.repo

不過 CentOS-Vault.repo 主要是archive packages obsoleted by current release,所以以CentOS 6.10來說 它裡面只包含到CentOS 6.9 repo

如果是內部要長期維護的話,最好還是建一個local yum repo mirror,避免網路上的archive repo失效

Posted in System Administration | Leave a comment

rsyslog in docker environment

在docker中,預設沒有啟用systemd所需要的 CAP_SYS_ADMIN  privileged capabilities,因此在執行服務時,一般採用直接執行process的方式而不透過systemd控制(systemctl)

不過rsyslogd在CentOS預設是透過journald讀取syslog,journald在systemd的架構下主要處理來自systemd service的stdout/stderr、以及syslog(透過/run/systemd/journal/dev-log,並將/dev/log symlink到前述位置),以及處理systemd forward log到 /run/systemd/journal/syslog (須設定ForwardToSyslog開啟,新版本的rsyslogd已經直接讀取journal,不再使用此方式)

如果要跑journald則需要對docker另外開一些capabilites(可參考systemd的service裡面的CapabilityBoundingSet設定,雖然部分應該是處理kern log),另外要處理listen unix socket的設定(描述在/lib/systemd/system/systemd-journald.socket、/lib/systemd/system/syslog.socket) ,因此最簡單的方式就是直接讓rsyslog listen unix socket /dev/log,並且如果不用systemd,單純使用syslog的話沒有理由需要透過journald來處理。

在/etc/rsyslog.conf 將所有journald相關的設定註解,並且開啟imuxsock

#### MODULES ####
#for DOCKER add load imuxsock
module(load="imuxsock")
# The imjournal module bellow is now used as a message source instead of imuxsock.
#$ModLoad imuxsock # provides support for local system logging (e.g. via logger command)

#for DOCKER remove imjournal module
#$ModLoad imjournal # provides access to the systemd journal

#...

# Turn off message reception via local log socket;
# local messages are retrieved through imjournal now.
# for DOCKER
#$OmitLocalLogging on

# File to store the position in the journal
#for DOCKER
#$IMJournalStateFile imjournal.state

並且刪掉 /etc/rsyslog.d/listen.conf

啟動rsyslogd就可以正常運行將syslog寫入到對應的log檔

以 logger測試

值得一提的是 因為透過imuxsock 模組讀取log,是透過 /dev/log unix socket 讀取log

而這個socket是由 rsyslogd產生的,logger預設也是寫入此socket

可參考 util-linux/logger.c

另外docker執行時,一般習慣上是用foreground執行,rsyslogd預設是會跑在背景,並且會做fork,這對於在整合一些process manager如supervisor或是chapterone的工具無法正確追蹤process是否正確執行,或是要設定專門的rsyslog server放在docker的entrypoint,因此在執行rsyslogd時,加上-n選項

/usr/sbin/rsyslogd -n

Posted in System Administration | Leave a comment