gtxyzz

我掌握了少数人才知道持续集成系统的日志密码

gtxyzz 安全防护 2022-12-25 303浏览 0

我掌握了少数人才知道持续集成系统的日志密码

前言

前段时间在使用 Travis CI 的时候发现它的部署日志包含了很多带色彩的日志。

我掌握了少数人才知道持续集成系统的日志密码

我掌握了少数人才知道持续集成系统的日志密码

并且我们知道,在使用命令行终端的时候也会出现这些可爱的色彩。

当然我不是为了吹它而吹它,它是有实际的作用的,能够帮助我们快速定位问题!

对此我就产生了好奇,Travis CI 是怎么把这些彩色日志搬到浏览器的?

我猜想肯定不是通过对关键字词特征识别来做的,因为那样太 low 了。

进行了查询后,查到了一个终于查到了关键词,它就是 ANSI escape sequences。

ANSI转义序列是带内信令的标准,用于控制终端和终端仿真器上的光标位置,颜色和一些其他选项。–维基百科

通俗地讲,就是那些在终端输出彩色的文字中包含了一些转义序列字符,只不过我们看不到,被终端进行了解析。然后终端将这些字符解析成了我们现在看到的形形色色多彩的日志(包括一些颜色、下划线、粗体等)。

例如,我们在终端进行npm 的安装,git 分支的切换,包括运行报错的时候都能看到。

正是有了这些色彩,让我们的调试工作效率大大提高,一眼便能看到哪些命令出错了,以及如何解决的方案。

现在我们要做的就是如何将这些色彩日志输出到浏览器端。而进行这个步骤之前,我们得先知道,这些ANSI转义序列的形态是什么样子的?

根据wiki我们可以知道 ANSI 转义序列可以操作很多功能,例如光标位置、颜色、下划线和其他选项。下面我们就 颜色部分 来进行讲解。

ANSI 转义序列

ANSI 转义序列 也是跟随着终端的发展而发展,颜色的规范也是随着设备的不同有所区别。例如在早期的设备只支持 3 / 4 Bit ,支持的颜色分别为 8 / 16 种。

ANSI 转义序列大多数以 ESC 和'[‘开头嵌入到文本中,终端会查找并解释为命令,而不是字符串。

ESC 的 ANSI 值为 27 ,8进制表示为 \033 ,16进制表示为 \u001B。

3/4 bit

原始规格只有 8/16 种颜色。

比如ESC[30;47m 它是以 ESC[ 开头 m 结束,中间为code码,以分号进行分割。

color 取值为30-37,background 取值为 40-47。例如 :

echo-e"\u001B[31mhello"

(如果想要清除颜色就需要使用 ESC [39;49m(某些终端不支持) 或者ESC[0m )

后来的终端增加了直接指定 90-97 和 100-107 的“明亮”颜色的能力。

效果如下:

我掌握了少数人才知道持续集成系统的日志密码

以下是其色彩对照表:

8-bit

后来由于256色在显卡上很常见,因此添加了转义序列以从预定义的256种颜色中进行选择,也就是说在原来的书写方式上增加了新的一位来代表更多的颜色。

ESC[38;5;<n>m//设置字体颜色
ESC[48;5;<n>m//设置背景颜色
0-7:standardcolors(asinESC[30–37m)
8-15:highintensitycolors(asinESC[90–97m)
16-231:6×6×6cube(216colors):16+36×r+6×g+b(0≤r,g,b≤5)
232-255:grayscalefromblacktowhitein24steps

在支持更多色彩的终端中,例如:

echo-e"\u001B[38;5;11mhello"

代表输出黄色字体。

echo-e"\u001B[48;5;14;38;5;13mhello"

代表输出蓝色背景,粉红色字体。

我掌握了少数人才知道持续集成系统的日志密码

以下是其色彩对照表:

我掌握了少数人才知道持续集成系统的日志密码

24-bit

再往后发展就是支持 24 位真彩的显卡,Xterm, KDE 的Konsole,以及所有基于 libvte 的终端(包括GNOME终端)支持24位前景和背景颜色设置。

ESC[38;2;<r>;<g>;<b>m//前景色
ESC[48;2;<r>;<g>;<b>m//背景色

例如:

echo-e"\u001B[38;2;100;228;75mhello"

输出绿色的字体代表 rgb(100,228,75)。

我掌握了少数人才知道持续集成系统的日志密码

解析工具

我们知道了转义的规范后,那么我们需要将 ANSI 字符进行解析。

由于规范比较多,因此我们先调研一下在 js 中常用的色彩库,来进行一个小小的探索。

由于 3 / 4bit 的兼容性更好,大多数工具(如chalk)会采用这 8 / 16 色来做高亮,因此我们先实现一个 8 / 16 色的解析。

这里参考了 ansiparse 这个解析库:

核心思路为:

我掌握了少数人才知道持续集成系统的日志密码

我掌握了少数人才知道持续集成系统的日志密码

constansiparse=require('ansiparse')

constansiStr="\u001B[34mHello\u001B[39mWorld\u001B[31m!\u001B[39m"

constjson=ansiparse(ansiStr)
console.log(json)

//json输出如下:
[
{foreground:'blue',text:'Hello'},
{text:'World'},
{foreground:'red',text:'!'}
]

然后我们可以写一个函数来遍历上面解析得到的 JSON数组,输出 HTML。

functioncreateHtml(ansiList,wrap=''){
lethtml='';
for(leti=0;i<ansiList.length;i++){
consthtmlFrame=ansiList[i];

const{background='',text,foreground=''}=htmlFrame;
if(background&&foreground){
if(text.includes('\n')){
html+=wrap;
continue;
}
html+=fontBgCode(text,foreground,background);
continue;
}
if(background||foreground){
constcolor=background?`bg-${background}`:foreground;
lettextColor=bgCode(text,color);

textColor=textColor.replace(/\n/g,wrap);

html+=textColor;
continue;
}
if(text.includes('\n')){
consttextColor=text.replace(/\n/g,wrap);
html+=textColor;
continue;
}
html+=singleCode(text);
}
html+=''
returnhtml;
}

functionfontBgCode(value,color,bgColor){
return`<spanclass="${color}bg-${bgColor}">${value}</span>`
}

functionbgCode(value,color){
return`<spanclass="${color}">${value}</span>`
}

functionsingleCode(value){
return`<span>${value}</span>`
}

使用示例如下:

conststr="\u001B[34mHello\u001B[39mWorld\u001B[31m!\u001B[39m";

console.log(createHtml(parseAnsi(str)));

//<spanclass="blue">Hello</span><span>World</span><spanclass="red">!</span>

部署实战

有了上面的部分我们就来用一个简单的demo实际演示一下部署日志吧!

//项目目录结构
demo
|-package.json
|-index.html
|-webpack.config.js
|-/src
|-index.js
index.js
build.sh

我们在 index.js 中启动一个 build 脚本,来模拟一下我们真实的部署场景。

const{spawn}=require('child_process');
constcmd=spawn('sh',['build.sh']);

cmd.stdout.on('data',(data)=>{
console.log(`stdout:${data}`);
});

cmd.stderr.on('data',(data)=>{
console.log(`stderr:${data}`);
});

cmd.on('close',(code)=>{
console.log(`childprocessexitedwithcode${code}`);
});
//build.sh

cddemo

npxwebpack

我们在终端尝试一下,控制台输入 node index.js

我掌握了少数人才知道持续集成系统的日志密码

发现在输出的日志中,并没有看到对应的色彩。

为什么从 child_process 为什么无法输出色彩,而我们如果在终端中直接打包项目却能够输出色彩呢?

Why?

第一反应就是去查找根源,也就是使用频率最高的几个色彩输出的库。

以简单的方式给控制台的输出标记颜色。

https://github.com/Marak/colors.js

https://github.com/chalk/chalk

在看了webpack-cli的源码后,查到它是用了colorette作为色彩输出库的。

那么我们就来查看一下colorette的源码一探究竟。

在入口文件的开头就看到一个变量isColorSupported来判断是否支持色彩输出。

https://github.com/jorgebucaran/colorette/blob/main/index.js#L17

//colorette/index.js
import*asttyfrom"tty"

constenv=process.env||{}
constargv=process.argv||[]

constisDisabled="NO_COLOR"inenv||argv.includes("--no-color")

constisForced="FORCE_COLOR"inenv||argv.includes("--color")
constisWindows=process.platform==="win32"
constisCompatibleTerminal=tty&&tty.isatty&&tty.isatty(1)&&env.TERM&&env.TERM!=="dumb"
constisCI="CI"inenv&&("GITHUB_ACTIONS"inenv||"GITLAB_CI"inenv||"CIRCLECI"inenv)

exportconstisColorSupported=!isDisabled&&(isForced||isWindows||isCompatibleTerminal||isCI)

可以看到这种工具判断了很多条件,来对我们的输出流进行处理。

在以上条件成立下,才会输出 ANSI 日志。在不满足以上情况的条件下,就会切换输出更容易解析的方式。

const isWindows = process.platform === "win32"

继续浏览有关 安全 的文章
发表评论