← 中文资源

简体中文版本

QUIC 0-RTT 恢复

QUIC 0-RTT 让回访客户端用恢复密钥在第一个飞行就发送加密应用数据,省掉握手往返,代价是该数据可被重放、需受约束。

阅读英文原文

概念定义

全新 QUIC 或 TLS 1.3 握手要花一个往返客户端才能发请求字节。0-RTT 用先前签发的票据立即派生密钥,第一包就携带加密请求。服务器只有在反重放状态与策略允许时才会处理它。

工作机制

上一条连接的票据携带恢复密钥;客户端派生早期密钥,把数据与握手其余部分一起发出。服务器验证票据、查询重放缓存,或处理早期数据,或拒绝并回退完整握手。后续飞行一律使用全新密钥。

使用场景

高往返路径上的回访延迟:移动应用重复调用 API、浏览器访问已知源、服务网格重连。收益是一个 RTT,在 150 毫秒路径上就是首字节时间里肉眼可见的一截。

常见误区

加密不等于防重放:攻击者可以原样复制第 0 飞行。0-RTT 应限于幂等读操作,或叠加每端点反重放与幂等键。负载均衡器必须共享重放状态或保持路由一致,否则会静默扩大重放窗口。拒绝 0-RTT 是设计内的正常回退,不是错误路径。

代码示例

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

.animate-fade {
  animation: fadeIn 0.3s ease-out forwards;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.05); }
}
export function parseContentDisposition(header) {
  if (typeof header !== "string" || header.trim() === "") {
    throw new TypeError("header must be a non-empty string");
  }
  const segments = splitParameters(header);
  const type = segments.shift().trim().toLowerCase();
  if (type !== "inline" && type !== "attachment") {
    throw new Error(`unsupported disposition type "${type}"`);
  }
  const parameters = new Map();
  for (const segment of segments) {
    const equals = segment.indexOf("=");
    if (equals === -1) continue;
    const name = segment.slice(0, equals).trim().toLowerCase();
    if (name === "") continue;
    parameters.set(name, unquote(segment.slice(equals + 1).trim()));
  }
  return { type, filename: resolveFilename(parameters), parameters };
}

function splitParameters(header) {
  const segments = [];
  let current = "";
  let quoted = false;
  for (const character of header) {
    if (character === '"') quoted = !quoted;
    if (character === ";" && !quoted) {
      segments.push(current);
      current = "";
      continue;
    }
    current += character;
  }
  segments.push(current);
  return segments;
}

function unquote(value) {
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
    return value.slice(1, -1).replace(/\\(.)/g, "$1");
  }
  return value;
}

function resolveFilename(parameters) {
  const extended = parameters.get("filename*");
  if (extended !== undefined) {
    const match = /^([\w-]+)'([\w-]*)'(.+)$/.exec(extended);
    if (match !== null) {
      try {
        return sanitize(decodeURIComponent(match[3]));
      } catch {
        return sanitize(extended);
      }
    }
  }
  const plain = parameters.get("filename");
  return plain === undefined ? null : sanitize(plain);
}

function sanitize(filename) {
  return filename.split(/[\\/]/).pop() || null;
}

console.log(parseContentDisposition('attachment; filename="report.pdf"'));
console.log(parseContentDisposition("attachment; filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf"));
console.log(parseContentDisposition('inline; filename="../../etc/passwd"'));