mirror of
https://github.com/tnb-labs/panel.git
synced 2026-10-16 07:21:35 +08:00
fix(lsapi): 修审查发现的协议与接入问题
- lsphp 在 prefork 下借 stderr 通道上报 worker pid,这 8 字节让 stderr 恒非空, 错误判断失效且报错被二进制前缀污染,现在识别并丢弃 - 正常收尾一定有 RESP_END,原先把 EOF 当完整响应会把子进程异常当成功 - 请求包超过 lsphp 的 readReq 上限时自己拦下,否则只能收到一个没头没尾的 EOF - 补响应体总量上限与兜底超时,调用方没给期限时不再无限等 - 解析 RESP_HEADER 的状态码,PHP 报错且无输出时有线索 - 状态接口漏改成按协议选套接字,LSAPI 下读状态这条路此前根本没接上 - 标记在但 lsphp 不在时当作没开,否则外部应用指向不存在的二进制且 fpm 已停 - 探针文件改成先写临时文件再改名,并发下不会被读到半截 测试覆盖请求包布局与响应循环,另配真实 lsphp 8.3 验证。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"resty.dev/v3"
|
||||
@@ -587,7 +588,8 @@ func (s *EnvironmentPHPService) Opcache(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = os.Stat(s.phpFPMSocket(req.Version)); err != nil {
|
||||
socket, _ := s.probeSocket(req.Version)
|
||||
if _, err = os.Stat(socket); err != nil {
|
||||
Success(w, types.EnvironmentPHPOpcache{Enabled: false})
|
||||
return
|
||||
}
|
||||
@@ -989,8 +991,14 @@ if (($_GET['action'] ?? '') === 'reset') {
|
||||
}
|
||||
echo json_encode(function_exists('opcache_get_status') ? opcache_get_status(false) : false);
|
||||
`
|
||||
// FPM 以 www 用户执行,探针放在 /tmp 保证可读,每次覆盖写入保证内容正确
|
||||
if err := io.Write(probePath, probe, 0644); err != nil {
|
||||
// PHP 以 www 用户执行,探针放在 /tmp 保证可读;先写临时文件再改名,避免并发读到半截。
|
||||
// 路径固定,每次换名字会让 OPcache 把每一次探针都缓存成一条脚本
|
||||
tmpPath := probePath + "." + str.Random(8)
|
||||
if err := io.Write(tmpPath, probe, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.Rename(tmpPath, probePath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+78
-20
@@ -4,11 +4,13 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"runtime"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,8 +26,22 @@ const (
|
||||
headerIndexLen = 25*2 + 2 + 25*4
|
||||
|
||||
maxPacketLen = 16 << 20
|
||||
|
||||
// lsphp 的 readReq 超过这个长度直接拒绝并关连接,自己先拦住才能给出像样的错误
|
||||
maxRequestLen = 256 << 10
|
||||
// 响应体总量上限,异常的 lsphp 可能一直吐数据
|
||||
maxBodyLen = 32 << 20
|
||||
// 调用方没给期限时的兜底,否则读循环会一直等
|
||||
defaultTimeout = 30 * time.Second
|
||||
|
||||
// 环境变量的键值长度各用两字节表示,含结尾的 0;数量上限取自 lsphp 的 parseEnv
|
||||
maxEnvLen = 65534
|
||||
maxEnvCount = 8192
|
||||
)
|
||||
|
||||
// fixedKeys 这四项的值会被请求头的偏移字段引用,必须存在且排在最前
|
||||
var fixedKeys = []string{"SCRIPT_FILENAME", "SCRIPT_NAME", "QUERY_STRING", "REQUEST_METHOD"}
|
||||
|
||||
// endianFlag lsapidef.h 的字节序判断只认 x86,其它架构一律标成大端;两端一致时都不转换,各自按本机顺序读写
|
||||
var endianFlag = func() byte {
|
||||
if runtime.GOARCH == "amd64" || runtime.GOARCH == "386" {
|
||||
@@ -36,27 +52,35 @@ var endianFlag = func() byte {
|
||||
|
||||
// Request 发起一次请求,返回的响应体不含响应头
|
||||
func Request(ctx context.Context, network, address string, params map[string]string) ([]byte, error) {
|
||||
req, err := buildRequest(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
deadline = time.Now().Add(defaultTimeout)
|
||||
}
|
||||
_ = conn.SetDeadline(deadline)
|
||||
|
||||
if _, err = conn.Write(buildRequest(params)); err != nil {
|
||||
if _, err = conn.Write(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var body, stderr bytes.Buffer
|
||||
var status uint32
|
||||
header := make([]byte, packetHeaderLen)
|
||||
for {
|
||||
if _, err = io.ReadFull(conn, header); err != nil {
|
||||
// lsphp 处理完可能直接关连接,已经收到的就是完整响应
|
||||
if errors.Is(err, io.EOF) && body.Len() > 0 {
|
||||
return body.Bytes(), nil
|
||||
// 正常收尾一定有 RESP_END,走到这里多半是子进程挂了,stderr 里有线索
|
||||
if stderr.Len() > 0 {
|
||||
return nil, fmt.Errorf("lsapi stderr: %s", stderr.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,9 +88,10 @@ func Request(ctx context.Context, network, address string, params map[string]str
|
||||
return nil, fmt.Errorf("lsapi: bad packet signature %q", header[:2])
|
||||
}
|
||||
|
||||
length := int(binary.NativeEndian.Uint32(header[4:8])) - packetHeaderLen
|
||||
size := binary.NativeEndian.Uint32(header[4:8])
|
||||
length := int(size) - packetHeaderLen
|
||||
if length < 0 || length > maxPacketLen {
|
||||
return nil, fmt.Errorf("lsapi: bad packet length %d", length)
|
||||
return nil, fmt.Errorf("lsapi: bad packet length %d", size)
|
||||
}
|
||||
payload := make([]byte, length)
|
||||
if _, err = io.ReadFull(conn, payload); err != nil {
|
||||
@@ -75,40 +100,69 @@ func Request(ctx context.Context, network, address string, params map[string]str
|
||||
|
||||
switch header[2] {
|
||||
case typeRespStream:
|
||||
if body.Len()+len(payload) > maxBodyLen {
|
||||
return nil, fmt.Errorf("lsapi: response exceeds %d bytes", maxBodyLen)
|
||||
}
|
||||
body.Write(payload)
|
||||
case typeStderrStream:
|
||||
// prefork 出来的子进程借 stderr 通道上报自己的 pid,不是错误输出
|
||||
if bytes.HasPrefix(payload, []byte("\x00PID")) {
|
||||
continue
|
||||
}
|
||||
stderr.Write(payload)
|
||||
case typeRespHeader:
|
||||
if len(payload) >= 8 {
|
||||
status = binary.NativeEndian.Uint32(payload[4:8])
|
||||
}
|
||||
case typeRespEnd:
|
||||
if stderr.Len() > 0 && body.Len() == 0 {
|
||||
if body.Len() > 0 {
|
||||
return body.Bytes(), nil
|
||||
}
|
||||
if stderr.Len() > 0 {
|
||||
return nil, fmt.Errorf("lsapi stderr: %s", stderr.String())
|
||||
}
|
||||
return body.Bytes(), nil
|
||||
if status >= 400 {
|
||||
return nil, fmt.Errorf("lsapi: php returned status %d", status)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildRequest 定长头 + 环境变量区 + 8 字节对齐后的已知头索引表。
|
||||
// 头里的四个偏移指向环境变量区中的值,所以这四项要排在最前
|
||||
func buildRequest(params map[string]string) []byte {
|
||||
keys := []string{"SCRIPT_FILENAME", "SCRIPT_NAME", "QUERY_STRING", "REQUEST_METHOD"}
|
||||
for name := range params {
|
||||
switch name {
|
||||
case "SCRIPT_FILENAME", "SCRIPT_NAME", "QUERY_STRING", "REQUEST_METHOD":
|
||||
default:
|
||||
keys = append(keys, name)
|
||||
func buildRequest(params map[string]string) ([]byte, error) {
|
||||
keys := make([]string, len(fixedKeys), len(params)+len(fixedKeys))
|
||||
copy(keys, fixedKeys)
|
||||
for _, name := range slices.Sorted(maps.Keys(params)) {
|
||||
// 超长的可选项直接丢掉,写进去只会让包错位
|
||||
if slices.Contains(fixedKeys, name) || len(name) > maxEnvLen || len(params[name]) > maxEnvLen {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, name)
|
||||
}
|
||||
if len(keys) > maxEnvCount {
|
||||
keys = keys[:maxEnvCount]
|
||||
}
|
||||
|
||||
reqHeaderLen := packetHeaderLen + 4*9
|
||||
envLen := 4 + 4 // 两个环境变量区各有 4 字节结束符,特殊变量区只有结束符
|
||||
for _, name := range keys {
|
||||
envLen += 4 + len(name) + 1 + len(params[name]) + 1
|
||||
value := params[name]
|
||||
if len(value) > maxEnvLen {
|
||||
value = ""
|
||||
}
|
||||
envLen += 4 + len(name) + 1 + len(value) + 1
|
||||
}
|
||||
|
||||
total := reqHeaderLen + envLen
|
||||
pad := (8 - total%8) % 8
|
||||
total += pad + headerIndexLen
|
||||
|
||||
if total > maxRequestLen {
|
||||
return nil, fmt.Errorf("lsapi: request packet too large (%d bytes)", total)
|
||||
}
|
||||
|
||||
buf := make([]byte, total)
|
||||
copy(buf, []byte{'L', 'S', typeBeginRequest, endianFlag})
|
||||
binary.NativeEndian.PutUint32(buf[4:8], uint32(total))
|
||||
@@ -118,6 +172,10 @@ func buildRequest(params map[string]string) []byte {
|
||||
offsets := make([]uint32, 4)
|
||||
for i, name := range keys {
|
||||
value := params[name]
|
||||
// 固定项不能丢,超长时退化成空值
|
||||
if len(value) > maxEnvLen {
|
||||
value = ""
|
||||
}
|
||||
binary.BigEndian.PutUint16(buf[pos:], uint16(len(name)+1)) //nolint:gosec
|
||||
binary.BigEndian.PutUint16(buf[pos+2:], uint16(len(value)+1)) //nolint:gosec
|
||||
pos += 4
|
||||
@@ -141,5 +199,5 @@ func buildRequest(params map[string]string) []byte {
|
||||
binary.NativeEndian.PutUint32(buf[packetHeaderLen+i*4:], v)
|
||||
}
|
||||
|
||||
return buf
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
package lsapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/libtnb/assert/check"
|
||||
"github.com/libtnb/assert/must"
|
||||
)
|
||||
|
||||
func mustBuild(t *testing.T, params map[string]string) []byte {
|
||||
t.Helper()
|
||||
buf, err := buildRequest(params)
|
||||
must.NoError(t, err)
|
||||
return buf
|
||||
}
|
||||
|
||||
// parsed 按 lsphp 侧 parseRequest 的步骤还原出来的请求包内容
|
||||
type parsed struct {
|
||||
total int
|
||||
env map[string]string
|
||||
scriptFile string
|
||||
scriptName string
|
||||
queryStr string
|
||||
method string
|
||||
}
|
||||
|
||||
// parseAsServer 复刻 lsapilib.c 的 parseRequest:依次是定长头、特殊环境变量区、
|
||||
// 环境变量区、8 字节对齐的已知头索引表,最后位置必须正好落在包尾
|
||||
func parseAsServer(t *testing.T, buf []byte) parsed {
|
||||
t.Helper()
|
||||
must.True(t, len(buf) > packetHeaderLen)
|
||||
check.Equal(t, string(buf[:2]), "LS")
|
||||
check.Equal(t, int(buf[2]), typeBeginRequest)
|
||||
check.Equal(t, int(buf[3]), int(endianFlag))
|
||||
|
||||
total := int(binary.NativeEndian.Uint32(buf[4:8]))
|
||||
must.Equal(t, total, len(buf))
|
||||
|
||||
field := func(i int) int {
|
||||
return int(binary.NativeEndian.Uint32(buf[packetHeaderLen+i*4:]))
|
||||
}
|
||||
httpHeaderLen, cntUnknown := field(0), field(6)
|
||||
cntEnv, cntSpecial := field(7), field(8)
|
||||
check.Equal(t, httpHeaderLen, 0)
|
||||
check.Equal(t, cntUnknown, 0)
|
||||
check.Equal(t, cntSpecial, 0)
|
||||
|
||||
pos := packetHeaderLen + 4*9
|
||||
readEnv := func(count int) map[string]string {
|
||||
out := make(map[string]string, count)
|
||||
for range count {
|
||||
keyLen := int(binary.BigEndian.Uint16(buf[pos:]))
|
||||
valLen := int(binary.BigEndian.Uint16(buf[pos+2:]))
|
||||
must.True(t, keyLen > 0 && valLen > 0)
|
||||
pos += 4
|
||||
key := string(buf[pos : pos+keyLen-1])
|
||||
check.Equal(t, int(buf[pos+keyLen-1]), 0)
|
||||
pos += keyLen
|
||||
value := string(buf[pos : pos+valLen-1])
|
||||
check.Equal(t, int(buf[pos+valLen-1]), 0)
|
||||
pos += valLen
|
||||
out[key] = value
|
||||
}
|
||||
// 每个区以 4 个 0 收尾
|
||||
check.Equal(t, binary.NativeEndian.Uint32(buf[pos:]), uint32(0))
|
||||
pos += 4
|
||||
return out
|
||||
}
|
||||
|
||||
readEnv(cntSpecial)
|
||||
env := readEnv(cntEnv)
|
||||
|
||||
pos = (pos + 7) &^ 7
|
||||
pos += headerIndexLen
|
||||
pos += cntUnknown * 16
|
||||
pos += httpHeaderLen
|
||||
// parseRequest 最后要求位置正好等于包尾,差一个字节就整包被拒
|
||||
must.Equal(t, pos, total)
|
||||
|
||||
str := func(off int) string {
|
||||
end := off
|
||||
for buf[end] != 0 {
|
||||
end++
|
||||
}
|
||||
return string(buf[off:end])
|
||||
}
|
||||
|
||||
return parsed{
|
||||
total: total,
|
||||
env: env,
|
||||
scriptFile: str(field(2)),
|
||||
scriptName: str(field(3)),
|
||||
queryStr: str(field(4)),
|
||||
method: str(field(5)),
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRequest(t *testing.T) {
|
||||
params := map[string]string{
|
||||
"SCRIPT_FILENAME": "/tmp/probe.php",
|
||||
"SCRIPT_NAME": "/probe.php",
|
||||
"QUERY_STRING": "action=reset",
|
||||
"REQUEST_METHOD": "GET",
|
||||
"SERVER_PROTOCOL": "HTTP/1.1",
|
||||
"HTTP_X_TEST": "hello",
|
||||
}
|
||||
|
||||
got := parseAsServer(t, mustBuild(t, params))
|
||||
|
||||
check.DeepEqual(t, got.env, params)
|
||||
check.Equal(t, got.scriptFile, "/tmp/probe.php")
|
||||
check.Equal(t, got.scriptName, "/probe.php")
|
||||
check.Equal(t, got.queryStr, "action=reset")
|
||||
check.Equal(t, got.method, "GET")
|
||||
check.Equal(t, got.total%8, headerIndexLen%8)
|
||||
}
|
||||
|
||||
// 四个固定键即使调用方没传也要占位,它们的偏移被请求头引用
|
||||
func TestBuildRequestMissingFixedKeys(t *testing.T) {
|
||||
got := parseAsServer(t, mustBuild(t, map[string]string{"SERVER_NAME": "localhost"}))
|
||||
|
||||
check.Len(t, got.env, 5)
|
||||
check.Equal(t, got.scriptFile, "")
|
||||
check.Equal(t, got.method, "")
|
||||
check.Equal(t, got.env["SERVER_NAME"], "localhost")
|
||||
}
|
||||
|
||||
func TestBuildRequestEmptyParams(t *testing.T) {
|
||||
got := parseAsServer(t, mustBuild(t, nil))
|
||||
|
||||
check.Len(t, got.env, 4)
|
||||
}
|
||||
|
||||
// 长度字段只有两字节,超长的值必须被挡住而不是截断成错位的包
|
||||
func TestBuildRequestOversizedValue(t *testing.T) {
|
||||
long := make([]byte, maxEnvLen+1)
|
||||
for i := range long {
|
||||
long[i] = 'a'
|
||||
}
|
||||
|
||||
got := parseAsServer(t, mustBuild(t, map[string]string{
|
||||
"HTTP_LONG": string(long),
|
||||
"SERVER_NAME": "localhost",
|
||||
}))
|
||||
|
||||
check.Equal(t, got.env["SERVER_NAME"], "localhost")
|
||||
_, ok := got.env["HTTP_LONG"]
|
||||
check.False(t, ok)
|
||||
}
|
||||
|
||||
// 超过 lsphp 的 readReq 上限时要自己拦下,否则只会收到一个没头没尾的 EOF
|
||||
func TestBuildRequestTooLarge(t *testing.T) {
|
||||
params := make(map[string]string, 3000)
|
||||
value := string(make([]byte, 100))
|
||||
for i := range 3000 {
|
||||
params["HTTP_X_"+strconv.Itoa(i)] = value
|
||||
}
|
||||
|
||||
_, err := buildRequest(params)
|
||||
check.Error(t, err)
|
||||
}
|
||||
|
||||
// fakeLSPHP 起一个假 lsphp,按给定的包序列应答一次请求
|
||||
func fakeLSPHP(t *testing.T, packets [][]byte, closeEarly bool) string {
|
||||
t.Helper()
|
||||
socket := filepath.Join(t.TempDir(), "s.sock")
|
||||
ln, err := net.Listen("unix", socket)
|
||||
must.NoError(t, err)
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
_, _ = io.CopyN(io.Discard, conn, 8) // 请求包头
|
||||
for _, p := range packets {
|
||||
_, _ = conn.Write(p)
|
||||
}
|
||||
if !closeEarly {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
return socket
|
||||
}
|
||||
|
||||
func packet(typ byte, payload []byte) []byte {
|
||||
buf := make([]byte, packetHeaderLen+len(payload))
|
||||
copy(buf, []byte{'L', 'S', typ, endianFlag})
|
||||
binary.NativeEndian.PutUint32(buf[4:8], uint32(packetHeaderLen+len(payload))) //nolint:gosec // 测试数据长度可控
|
||||
copy(buf[packetHeaderLen:], payload)
|
||||
return buf
|
||||
}
|
||||
|
||||
func respHeader(status uint32) []byte {
|
||||
payload := make([]byte, 8)
|
||||
binary.NativeEndian.PutUint32(payload[4:], status)
|
||||
return packet(typeRespHeader, payload)
|
||||
}
|
||||
|
||||
// pidPacket lsphp fork 后借 stderr 通道上报 pid,客户端必须忽略它
|
||||
func pidPacket() []byte {
|
||||
return packet(typeStderrStream, append([]byte("\x00PID"), 1, 2, 3, 4))
|
||||
}
|
||||
|
||||
func request(t *testing.T, socket string) ([]byte, error) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
return Request(ctx, "unix", socket, map[string]string{"SCRIPT_FILENAME": "/tmp/x.php"})
|
||||
}
|
||||
|
||||
func TestRequestBody(t *testing.T) {
|
||||
socket := fakeLSPHP(t, [][]byte{
|
||||
pidPacket(),
|
||||
respHeader(200),
|
||||
packet(typeRespStream, []byte("hello ")),
|
||||
packet(typeRespStream, []byte("world")),
|
||||
packet(typeRespEnd, nil),
|
||||
}, false)
|
||||
|
||||
body, err := request(t, socket)
|
||||
must.NoError(t, err)
|
||||
check.Equal(t, string(body), "hello world")
|
||||
}
|
||||
|
||||
// pid 通知包不能被当成错误输出,否则每个请求的 stderr 都非空
|
||||
func TestRequestIgnoresPIDNotify(t *testing.T) {
|
||||
socket := fakeLSPHP(t, [][]byte{pidPacket(), respHeader(200), packet(typeRespEnd, nil)}, false)
|
||||
|
||||
body, err := request(t, socket)
|
||||
check.NoError(t, err)
|
||||
check.Empty(t, body)
|
||||
}
|
||||
|
||||
func TestRequestStderr(t *testing.T) {
|
||||
socket := fakeLSPHP(t, [][]byte{
|
||||
pidPacket(),
|
||||
packet(typeStderrStream, []byte("PHP Fatal error: boom")),
|
||||
packet(typeRespEnd, nil),
|
||||
}, false)
|
||||
|
||||
_, err := request(t, socket)
|
||||
must.Error(t, err)
|
||||
check.Contains(t, err.Error(), "PHP Fatal error: boom")
|
||||
check.NotContains(t, err.Error(), "PID")
|
||||
}
|
||||
|
||||
// 有正文时 stderr 里的 warning 不该让整个请求失败
|
||||
func TestRequestStderrWithBody(t *testing.T) {
|
||||
socket := fakeLSPHP(t, [][]byte{
|
||||
packet(typeStderrStream, []byte("PHP Warning: deprecated")),
|
||||
packet(typeRespStream, []byte("ok")),
|
||||
packet(typeRespEnd, nil),
|
||||
}, false)
|
||||
|
||||
body, err := request(t, socket)
|
||||
must.NoError(t, err)
|
||||
check.Equal(t, string(body), "ok")
|
||||
}
|
||||
|
||||
// 没等到 RESP_END 就断开是子进程异常,不能把半截正文当成功
|
||||
func TestRequestTruncated(t *testing.T) {
|
||||
socket := fakeLSPHP(t, [][]byte{packet(typeRespStream, []byte("half"))}, true)
|
||||
|
||||
_, err := request(t, socket)
|
||||
check.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRequestBadSignature(t *testing.T) {
|
||||
socket := fakeLSPHP(t, [][]byte{{'X', 'X', 0, 0, 0, 0, 0, 8}}, true)
|
||||
|
||||
_, err := request(t, socket)
|
||||
must.Error(t, err)
|
||||
check.Contains(t, err.Error(), "bad packet signature")
|
||||
}
|
||||
|
||||
func TestRequestBadLength(t *testing.T) {
|
||||
bad := make([]byte, packetHeaderLen)
|
||||
copy(bad, []byte{'L', 'S', typeRespStream, endianFlag})
|
||||
binary.NativeEndian.PutUint32(bad[4:8], 3) // 比包头还短
|
||||
socket := fakeLSPHP(t, [][]byte{bad}, true)
|
||||
|
||||
_, err := request(t, socket)
|
||||
must.Error(t, err)
|
||||
check.Contains(t, err.Error(), "bad packet length 3")
|
||||
}
|
||||
|
||||
// PHP 报 500 且没有任何输出时,状态码是唯一线索
|
||||
func TestRequestErrorStatus(t *testing.T) {
|
||||
socket := fakeLSPHP(t, [][]byte{respHeader(500), packet(typeRespEnd, nil)}, false)
|
||||
|
||||
_, err := request(t, socket)
|
||||
must.Error(t, err)
|
||||
check.Contains(t, err.Error(), "status 500")
|
||||
}
|
||||
|
||||
func TestRequestDialError(t *testing.T) {
|
||||
_, err := request(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
check.Error(t, err)
|
||||
}
|
||||
@@ -32,8 +32,13 @@ func LSPHPPath(version uint) string {
|
||||
return fmt.Sprintf("/opt/ace/server/php/%d/bin/lsphp", version)
|
||||
}
|
||||
|
||||
// LSAPIEnabled 标记在但 lsphp 不在时要当作没开,否则外部应用会指向不存在的二进制,
|
||||
// 而 php-fpm 又被停掉,该版本 PHP 就整体不可用了
|
||||
func LSAPIEnabled(version uint) bool {
|
||||
_, err := os.Stat(filepath.Join(lsapiDir, strconv.FormatUint(uint64(version), 10)))
|
||||
if _, err := os.Stat(filepath.Join(lsapiDir, strconv.FormatUint(uint64(version), 10))); err != nil {
|
||||
return false
|
||||
}
|
||||
_, err := os.Stat(LSPHPPath(version))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user