Web

AMP曾经是被Google完全垄断的, 这引起很大的争议和抵制, 后来转为由OpenJS基金会主持.
AMP组件表面上是Web Components中的Custom Elements, 但事实上Bento AMP是由Preact构建的.

AMP页面曾经在Google的搜索结果里享有更高权重, 这一情况于2021年6月结束.
作为替代, Google使用新的页面体验排名算法分析每个网站的页面载入性能.

  • AMP的JavaScript文件事实上有70KB之巨.
    因此精心设计的页面会比没有缓存的AMP页面更快.
  • 有很多硬性限制, 例如CSS和JavaScript文件都被要求在一定尺寸以内.
  • 对元素事件的支持是一种DSL, 看起来简直像是重新发明了HTML的内联事件.
    这与开发人员熟知的最佳实践截然相反, 编写AMP本身就令人生畏.

可以在非AMP页面使用的AMP组件.
于2021年1月推出预览版.

参考: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

CSRF令牌是一种随页面下载得到的具有不可预测性的令牌(token), 只有包含此令牌的请求会被服务器接受.
使用CSRF令牌是为了防止通过其他域名发起CSRF攻击.

常见的CSRF令牌位置:

  • 作为不可见字段隐藏在表单里, 提交表单时会一并提交令牌.
  • 通过 Set-Cookie 保存为客户端Cookie, 发送带有凭据的请求时会一并发送令牌.
    存储令牌的Cookie必须设置正确的SameSite属性, 否则仍然会留下CSRF攻击漏洞,
    因为伪造的请求能够在发送凭据时一并发送令牌.
  • 在请求中返回, 由客户端保存, 客户端之后用自定义 X-CSRF-Token 头发送令牌.

跨站请求如果可以通过CORS检查, 则意味着CSRF令牌也可以被客户端脚本读取, 客户端脚本就可能利用CSRF令牌伪造请求.

SameSite是Cookie的一个属性, 正确设置SameSite可以防止构造带有Cookie的CSRF请求.

  • Lax: Cookie不会在跨站请求中发送, 只有在点击链接导航至网站时会发送.
    在最近的浏览器版本里, 如果未明确指定SameSite属性, 则Lax会作为默认值.
  • Strict: Cookie只会在相应网站上发送.
    使用Strict意味着当用户是从其他网站导航至目标网站时, 本次请求将不会发送Cookie.
  • None: Cookie将在所有上下文中发送.
    在旧的浏览器版本里, None是SameSite的默认值.
    如果SameSite被设置为None, 但却没有设置Secure属性(要求此Cookie只能在HTTPS请求中发送),
    控制台会打印警告, 因为Cookie可能被中间人窃取.

对CORS的一种常见误解是认为它可以阻止CSRF, 但事实并非如此,
CORS只是一种可以防止响应被客户端读取的跨站资源共享策略而已.

方法为GET, HEAD, POST, 且不包含自定义请求头的请求(有少数几个请求头是例外), 不会触发预检.
详见 https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests

因此不触发预检意味着这些请求会完整发送到目标服务器:
浏览器之后可能因为不满足CORS而在客户端层面上拒绝掉相关响应, 但请求确实被服务器接受了,
这些请求可以被用于CSRF攻击.

img元素会无视CORS, 对它src属性里的url发出的GET请求.

form提交会无视CORS, 可以用于发送GET请求和POST请求.

重点在于用form触发请求并不需要人工点击提交按钮, 仅仅只是由JavaScript调用 form.submit() 就会发出请求.

用form构造JSON请求是可能的,
enctype="text/plain" 时, 可以通过拼接input元素的 namevalue 属性构造出JSON文本.

这种攻击之所以成立的原因之一, 是API服务器没有限制客户端的 content-type 请求头.

自HTTP 1.0可用的永久重定向.
使用该状态码的重定向可能会将非GET方法的请求重定向为GET方法, 当使用GET以外的方法时, 不建议使用它.

自HTTP 1.0可用的临时重定向.
使用该状态码的重定向可能会将非GET方法的请求重定向为GET方法, 当使用GET以外的方法时, 不建议使用它.

自HTTP 1.1可用的临时重定向, 用来取代302.
重定向时会将请求改为GET方法.

自HTTP 1.1可用的临时重定向, 用来取代302.
使用该状态码的重定向不会改变请求的方法.

自HTTP 1.1可用的永久重定向, 用来取代301.
使用该状态码的重定向不会改变请求的方法.

注意: 虽然与TCP Keepalive具有类似的名称, 但两者完全没有关系.

HTTP Keppalive的功能是使接下来的HTTP请求复用上一个连接, 而不是每次请求都打开一个新连接.
当连接已结束或超时(一定时间内不再有数据包), 连接才会关闭.

需要包含请求和响应都包含 Connection: Keep-alive.

默认启用, 根据平台的不同, 具有不同的默认超时时间.

在HTTP/2里, Keepalive已经没有作用, 因为HTTP/2自身实现了多路复用.

单工, 协议本身基于UTF-8编码的文本, 所以无法传输二进制数据.
基于HTTP实现(因此可享受到HTTP/2的多路复用优势和Node.js的事件循环模式).
是HTTP Comet技术的精神继承者.
支持使用event字段添加事件类型, 但由于所有文本数据都会完整发送至客户端, 在很多场景下聊胜于无.

SSE的客户端具有自动重连机制, 在得到连接断开的消息时会自动重连(例如TCP超时错误),
在重连时若得到不符合 SSE 的 HTTP 响应时会放弃重连(视作被服务器拒绝).

服务端可以使用retry字段设置客户端重连前需要等待的时间(毫秒单位, 浏览器默认值一般为3~5秒).

SSE草案提到每15秒发送一次心跳包, 这个频率可能超出了实际需求, 但也不会对服务器带来太大负担.

有几种不同的心跳包形式:

  • 在默认event下发送注释(推荐):
    以冒号开头的空白行, 即 :\n\n
    (我不清楚第二个 \n 是否有必要, 但我见到的实现都使用了两个 \n).
    这种心跳包的缺陷在于, 原生EventSource不可能使用它实现客户端心跳检测, 因为注释对客户端来说是透明的.
  • event: hbevent: heartbeatevent: ping 发送空 data 或带有时间戳的 data
    带有时间戳的好处是当服务端和客户端中间存在奇怪的延迟问题时, 时间戳可以作为判断两次发送心跳间隔的依据.
    这种心跳包比仅发送注释需要更多的数据量, 但原生EventSource可以检测到它.

客户端支持基于id的自动重连.
尽管协议本身支持HTTP头, 但浏览器原生的EventSource API没有提供相应的选项.

https://bugzilla.mozilla.org/show_bug.cgi?id=444328

由于SSE在应用层只提供数据的单向发送, 因此由服务端主动向客户端发送心跳包.
只能让服务器断开TCP已死的连接, 这会导致客户端的连接仍然处于半开状态.

在客户端未配置TCP Keepalive的情况下,
客户端的EventSource连接 最多需要2个小时才能发现服务器端的连接已经断开.
这使得自动重连机制在绝大多数场景下都 聊胜于无 (客户端未能配置合适的TCP Keepalive).

客户端需要能够识别服务器发送的心跳包, 在一定时间未收到心跳包时主动断开实际已经死亡的半开连接.

双工, 二进制数据或UTF-8文本数据, 基于纯TCP实现(每一个标签页都会导致一个新的TCP连接).

无法使用HTTP头, 客户端需要自行协商一些基本功能(例如身份验证).

WebSocket协程内置了ping/pong机制, 在底层ping包和pong包是作为控制帧发送的, ping为 0x9, pong为 0xA.

通常来说最佳实践是由服务端发送ping包, 以检测客户端是否存在.

浏览器端的WebSocket API没有提供与ping/pong机制有关的方法, 对于应用程序来说, 心跳机制完全是透明的.
这是非常遗憾的, 这意味着客户端没有能力主动发起ping包来自己检测与服务器的连通性.
为了实现客户端最小数据量的心跳检测, 只能定期向服务器发送空白数据.

如果客户端与服务端之间是直接连接的, 则TCP连接即使在没有数据包的情况下也会继续维持.
但假如客户端与服务端之间有防火墙等设备, 则TCP连接有可能因为长时间没有数据包经过而被中间设备删除,
这会导致下一次发包时发现TCP连接已断开.

一端发送心跳包并等待另一端的回应, 这时可以用超时来检测无效连接.

注意: 虽然与HTTP Keepalive具有类似的名称, 但两者完全没有关系.

TCP Keepalive的功能是使TCP保持活动状态, 这意味着可以检查已连接的TCP Socket, 以确认连接是否正常.

建立TCP连接时, 会将连接与计时器相关联.
当计时器归零时, 会发送Keepalive数据包, 利用TCP连接的双向性对连接进行确认(ACK).

按照公式, 一个TCP连接会在x秒后丢失:
x = KEEPALIVE_TIME + (KEEPALIVE_PROBES + 1) * KEEPALIVE_INTERVAL

从技术角度看, 这是最完美的心跳检测方式, 在任何主流TCP实现上都已实现, 但存在以下问题导致它难以使用:

  • TCP连接默认不启用Keepalive, 必须由应用调用相应的API才能支持Keepalive.
  • Keepalive的相关选项在一些平台上不受应用层的编程控制.
    例如在Windows上只能全局配置, Java不支持应用级配置.
  • 有着脱离时代的奇怪默认值, 例如默认的超时检测时间为2小时, 这是在RFC 1122里定义的.

因此, 若要使用TCP Keepalive, 则意味着:

  • 必须有配置操作系统的权限.
  • 必须修改应用以启用TCP Keepalive.
  • Keepalive选项必须适用于该操作系统上的所有应用程序.

TCP Keepalive仅适用于服务器端的半开连接检测, 如果配置TCP Keepalive, 则可以省去应用层的心跳机制.
对于客户端来说, 必须在应用层自行实现ping/pong心跳机制.

对Node.js程序来说, http和https模块可以在Agent对象中启用TCP Keepalive,
但与大多数编程平台一样, 无法在应用层提供TCP Keepalive的选项.

\HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\TCPIP\Parameters
修改/创建相应的注册表项:

  • KeepAlivetime, 单位为毫秒, 默认为7200秒.
  • KeepAliveInterval, 单位为秒, 默认为1秒.
  • TCPMaxDataRetransmissions, 默认为5.

Windows需要重新启动以使配置生效.

注1: 似乎只有ipv4具有可配置的keepalive属性.
注2: Docker容器不会继承宿主机的keepalive设置.

# 读取/写入当前keepalive超时检测时间(秒), 默认为2小时, 建议值为5分钟
cat /proc/sys/net/ipv4/tcp_keepalive_time
echo 300 > /proc/sys/net/ipv4/tcp_keepalive_time
# 使用sysctl的等价方式
sysctl net.ipv4.tcp_keepalive_time
sysctl -w net.ipv4.tcp_keepalive_time=300 # 永久修改, 在重启后仍然有效
# 读取/写入当前keepalive心跳包发送间隔(秒), 默认为75秒
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
echo 30 > /proc/sys/net/ipv4/tcp_keepalive_intvl
# 使用sysctl的等价方式
sysctl net.ipv4.tcp_keepalive_intvl
sysctl -w net.ipv4.tcp_keepalive_intvl=30 # 永久修改, 在重启后仍然有效
# 读取/写入当前keepalive 未收到响应的容忍次数, 默认为9
cat /proc/sys/net/ipv4/tcp_keepalive_probes
echo 10 > /proc/sys/net/ipv4/tcp_keepalive_probes
# 使用sysctl的等价方式
sysctl net.ipv4.tcp_keepalive_probes
sysctl -w net.ipv4.tcp_keepalive_probes=10 # 永久修改, 在重启后仍然有效