惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
雷峰网
雷峰网
Recent Announcements
Recent Announcements
月光博客
月光博客
G
Google Developers Blog
腾讯CDC
S
Secure Thoughts
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
云风的 BLOG
云风的 BLOG
W
WeLiveSecurity
博客园 - 【当耐特】
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 聂微东
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
MyScale Blog
MyScale Blog
K
Kaspersky official blog
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
aimingoo的专栏
aimingoo的专栏
I
Intezer
Vercel News
Vercel News
小众软件
小众软件
Simon Willison's Weblog
Simon Willison's Weblog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
N
Netflix TechBlog - Medium
P
Proofpoint News Feed
Latest news
Latest news
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tor Project blog
S
Security Affairs
P
Proofpoint News Feed
博客园 - 三生石上(FineUI控件)
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
美团技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Security @ Cisco Blogs
L
LINUX DO - 热门话题
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
D
Docker
L
Lohrmann on Cybersecurity
F
Full Disclosure

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
Workerman 源码分析:文件上传
她和她的猫 · 2021-05-08 · via 文章列表 - 她和她的猫

前言

在 Nginx 中 HTTP 数据是一边接收一边进行解析的,如果解析过程中发现收到的数据有问题就会停止解析,并且停止接收数据。

而 Workerman 将解析协议这一步进行后置,当程序需要用到 HTTP 协议携带的信息时才会解析相应的数据,并把解析结果缓存起来,下次获取信息时就直接从缓存中读取即可,避免多次解析。

两种方式各有自己的优点,前者的优点就是可以及时的检查数据是否有问题,后者的优点是在接收数据的逻辑相对要简单一点。

解析上传文件的逻辑在 Request::parseUploadFiles 方法中,下面是它的调用栈。

// source: Protocols\Http\Request.php
post()/file()
    -> parsePost()
        -> parseUploadFiles()

解析 POST 数据

当调用 post 或 file 方法获取 POST 参数或上传的文件时,程序会检查是否已经解析过了,如果已经解析过了则直接返回对应的结果,否则就会调用 parsePost 方法解析数据。

protected function parsePost()
{
    // rawBody() 返回的就是请求体的内容
    $body_buffer = $this->rawBody();
    // 初始化用于保存解析数据的缓存
    $this->_data['post'] = $this->_data['files'] = array();
    if ($body_buffer === '') {
        return;
    }
    // 尝试从缓存中读取
    $cacheable = static::$_enableCache && !isset($body_buffer[1024]);
    if ($cacheable && isset(static::$_postCache[$body_buffer])) {
        $this->_data['post'] = static::$_postCache[$body_buffer];
        return;
    }
    // 读取请求头中的 content-type 字段,如果请求头没有被解析过,那么也会进行解析并缓存
    $content_type = $this->header('content-type', '');
    // 尝试解析 boundary 字段,如果获取到了说明是 multipart/form-data 类型的,可能会上传文件
    if (\preg_match('/boundary="?(\S+)"?/', $content_type, $match)) {
        // $match[1] 中就是我们上面说的边界值
        // 加上 -- 得到请求体中的边界值
        $http_post_boundary = '--' . $match[1];
        $this->parseUploadFiles($http_post_boundary);
        return;
    }
    // 如果从 content-type 中匹配到了 json,比如 application/json
    // 说明请求体的数据是 JSON 格式的
    if (\preg_match('/\bjson\b/i', $content_type)) {
        $this->_data['post'] = (array) json_decode($body_buffer, true);
    } else {
        // 否则就是 application/x-www-form-urlencoded
            \parse_str($body_buffer, $this->_data['post']);
    }
    // 如果开启了缓存,就把解析结果缓存起来
    if ($cacheable) {
        static::$_postCache[$body_buffer] = $this->_data['post'];
        if (\count(static::$_postCache) > 256) {
            unset(static::$_postCache[key(static::$_postCache)]);
        }
    }
}

解析请求体的内容

接下来就是 parseUploadFiles 方法的内容了。

首先处理请求体,删掉末尾的结束边界值,然后通过边界值得到数据块数组。

// 先获取请求体的内容
$http_body = $this->rawBody();
//删除末尾的结束边界值
$http_body = \substr($http_body, 0, \strlen($http_body) - (\strlen($http_post_boundary) + 4))

// 通过边界值 + \r\n 分割请求体,得到数据块数组
$boundary_data_array = \explode($http_post_boundary . "\r\n", $http_body);
if ($boundary_data_array[0] === '') {
    unset($boundary_data_array[0]);
};

为什么计算 substr 结束位置最后要 +4 呢?

因为 结束边界值 = 边界值 + -- + \r\n

接下来用两个 foreach 和一个 switch case 来解析请求体的内容。

遍历所有的数据块

通过 \r\n\r\n 分割数据块,得到数据块的头部信息数据块的值,然后去除数据块的值末尾的 \r\n

foreach ($boundary_data_array as $boundary_data_buffer) {
    list($boundary_header_buffer, $boundary_value) = \explode("\r\n\r\n", $boundary_data_buffer, 2);
    // 去除 $boundary_value 末尾的 \r\n
    $boundary_value = \substr($boundary_value, 0, -2);
    $key++;
}

解析数据块的头部信息

数据块的头部信息可能存在多行,所以需要通过 \r\n 分割头部信息字符串得到头部信息的数组。

然后遍历该数组,在循环中通过 : 分割每行的头部信息,得到字段名 $header_key 和字段值 $header_value 。

foreach (\explode("\r\n", $boundary_header_buffer) as $item) {
    list($header_key, $header_value) = \explode(": ", $item);
    $header_key = \strtolower($header_key);

    switch ($header_key) {
        case "content-disposition":
            // 匹配到了 filename 说明是文件数据
            if (\preg_match('/name="(.*?)"; filename="(.*?)"/i', $header_value, $match)) {
                $error = 0;
                $tmp_file = '';
                // 获取文件大小
                $size = \strlen($boundary_value);
                // 获取上传临时目录
                $tmp_upload_dir = HTTP::uploadTmpDir();
                if (!$tmp_upload_dir) {
                    $error = UPLOAD_ERR_NO_TMP_DIR;
                } else {
                    // 使用 tempnam 函数在临时目录下创建一个唯一文件名的临时文件
                    $tmp_file = \tempnam($tmp_upload_dir, 'workerman.upload.');
                    // 文件创建成功后,将数据块的值写入到文件中
                    if ($tmp_file === false || false == \file_put_contents($tmp_file, $boundary_value)) {
                        $error = UPLOAD_ERR_CANT_WRITE;
                    }
                }
                // 格式化上传的文件信息
                $files[$key] = array(
                    'key' => $match[1], // 表单中的字段名
                    'name' => $match[2], // 文件名称
                    'tmp_name' => $tmp_file, // 临时文件的完整路径
                    'size' => $size, // 文件大小
                    'error' => $error // 错误
                );
                break;
            } else {
                // 未匹配到 filename 说明是 POST 字段,需要解析 $_POST.
                if (\preg_match('/name="(.*?)"$/', $header_value, $match)) {
                    $this->_data['post'][$match[1]] = $boundary_value;
                }
            }
            break;
        case "content-type":
            // 添加文件类型
            $files[$key]['type'] = \trim($header_value);
            break;
    }
}

switch 中的逻辑就是判断 $header_key 的值,然后执行相应的操作。

  • content-disposition: 通过正则判断是否为文件数据,如果是文件就将数据块的值写入临时文件中,否则将字段和值保存到存放 POST 数据的数组中。
  • content-type: 记录文件的类型。