mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
71 Commits
dev-test-m
...
v1.12.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4e422c1c | ||
|
|
148cbd8606 | ||
|
|
ee86d58a3e | ||
|
|
63759861d4 | ||
|
|
dd402fb350 | ||
|
|
434d6f83bc | ||
|
|
6d6b7316db | ||
|
|
a68ab97855 | ||
|
|
ca6a3ee6c5 | ||
|
|
d8376282d5 | ||
|
|
dca786c0bb | ||
|
|
a785709637 | ||
|
|
b1478ecf77 | ||
|
|
9d86465ed4 | ||
|
|
d2acfa9947 | ||
|
|
1c7bb1b339 | ||
|
|
afa43d1c00 | ||
|
|
d054978121 | ||
|
|
9abde46e71 | ||
|
|
d89ffcbf6c | ||
|
|
b8757a2ed6 | ||
|
|
0cbe502a6d | ||
|
|
119218dc40 | ||
|
|
9eee8b71a7 | ||
|
|
04949486da | ||
|
|
7ea560548e | ||
|
|
b1aff3a9fc | ||
|
|
5235e770dc | ||
|
|
bcadf7d1b7 | ||
|
|
c80cc10623 | ||
|
|
0ad13fac52 | ||
|
|
310226c6b9 | ||
|
|
7305a72fd2 | ||
|
|
d15caee966 | ||
|
|
23fc7e738c | ||
|
|
3a97a9e61d | ||
|
|
88e57b5ea9 | ||
|
|
27ad613700 | ||
|
|
bf8a1160b0 | ||
|
|
6156fdddf7 | ||
|
|
3fff679a9e | ||
|
|
fb2d71f356 | ||
|
|
f93821488b | ||
|
|
8bcea4caef | ||
|
|
c2f0802648 | ||
|
|
ddf2f569de | ||
|
|
1311bc794d | ||
|
|
be20d225f8 | ||
|
|
4ad0189e35 | ||
|
|
d85235a156 | ||
|
|
24213194b5 | ||
|
|
1c10684ba2 | ||
|
|
51c2789abf | ||
|
|
f2c870e1c3 | ||
|
|
aa9e6581c7 | ||
|
|
c15e2d529e | ||
|
|
58d49055c1 | ||
|
|
a52fbfd80d | ||
|
|
33b3210bf7 | ||
|
|
75f4e6534c | ||
|
|
1bc2580d51 | ||
|
|
9774a659b0 | ||
|
|
2e4a6de4e7 | ||
|
|
a530e424e9 | ||
|
|
0bfd487ee9 | ||
|
|
6aae834493 | ||
|
|
f56131f38e | ||
|
|
273a11d550 | ||
|
|
ae8ce75e41 | ||
|
|
d6d94b689f | ||
|
|
30d785f1ee |
19
.fpm
Normal file
19
.fpm
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-s dir
|
||||||
|
--name sing-box
|
||||||
|
--category net
|
||||||
|
--license GPLv3-or-later
|
||||||
|
--description "The universal proxy platform."
|
||||||
|
--url "https://sing-box.sagernet.org/"
|
||||||
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
|
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||||
|
|
||||||
|
release/config/config.json=/etc/sing-box/config.json
|
||||||
|
|
||||||
|
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||||
|
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||||
|
|
||||||
|
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
|
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
||||||
3
.github/goreleaser/README.md
vendored
3
.github/goreleaser/README.md
vendored
@@ -1,3 +0,0 @@
|
|||||||
# goreleaser
|
|
||||||
|
|
||||||
I'm sorry for this, but I can't afford to renew for now because the subscription is too expensive for an open source project.
|
|
||||||
BIN
.github/goreleaser/ca.crt
vendored
BIN
.github/goreleaser/ca.crt
vendored
Binary file not shown.
87
.github/goreleaser/config.json
vendored
87
.github/goreleaser/config.json
vendored
@@ -1,87 +0,0 @@
|
|||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"type": "tls",
|
|
||||||
"server": "8.8.8.8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"inbounds": [
|
|
||||||
{
|
|
||||||
"type": "tun",
|
|
||||||
"address": [
|
|
||||||
"172.19.0.1/30",
|
|
||||||
"fdfe:dcba:9876::1/126"
|
|
||||||
],
|
|
||||||
"auto_route": true,
|
|
||||||
"auto_redirect": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"certificate": {
|
|
||||||
"tls_decryption": {
|
|
||||||
"enabled": true,
|
|
||||||
"key_pair_p12": "MIIKYQIBAzCCChEGCSqGSIb3DQEHAaCCCgIEggn+MIIJ+jCCBGgGCSqGSIb3DQEHBqCCBFkwggRVAgEAMIIETgYJKoZIhvcNAQcBMF0GCSqGSIb3DQEFDTBQMC8GCSqGSIb3DQEFDDAiBBBxLjkB6wrMHpRNPnq8KUnXAgIIADAKBggqhkiG9w0CCTAdBglghkgBZQMEASoEEHFou8IR0ZPb9O4NaLDC5LKAggPgL/7EoJRMEx5ZDVm2ZUQRuGyjS+lMB4JDZiykYfvfzMtQ2LZ+aO90rLxYFh4uBpbu+mmA0WDF/HU3GbE0nyY9beo0RAh0/u2Ak2kkfDSntRPVTl5zNBrT9hEtH9oSlN7tok9SMhWEJlsoIRhGinJwsDnDbXcIqkIj/oqtXlSJc6gA7CYf6AJRrVjP1Wtk80GMrMfYNvQw9bich5fs4biddf0xtR13YFV80rCPb+HtTT4KYa7Rzo5qR/cNHsMP/3v5BT2UszpaSIokPoW8ta1RWcQNXuH3OHjG4GMjg88w6xtyudIKrTyP0BTRfIJ2S2EtsWGHU2Gmr/MUY0a7abbtG+LVdSCRTgDoNeiY4C7lkQEOpefoZHWa3+jeGu17812YZHxfCZuhFy33rZgqngWRN1cdxoAbhozChtKmn0Uhdox7jqUw5M/Sj4DWHm0RNB8Ffvf39i/zvlfORzljIiwAKiB26FwpcKKRfx7rrjx4xRLkTLWl0DnJKxOcVz/oXSjglpHJvUSMgbpzXEHHQ7+d+K/WTnoj+dONifxiWBt1hQA8qoPiQceYWGY37oeWvGZI/Qv3ZSO5Mm/yVAuAFyOzJdpW5aC3Kq3gwNVbKNeeV5fWDtvP2K9XcgZFv8OqpNnvLmaL+iWHTPg5wYGvf0iWPr8NVU6OQpSZCOTodwOGfcpQ2YlCnkBgkjkJFLNuM4mi1U9kyTZWAYyZ6zVort0eezJcBoQGBBV2/GkFmwDNa9Q8mT8S7QTf7ZqAtyMnM9rBch7zIscBk6swG/KhgFRtUmDLpY6tpMb6vHHueu4duaUvIXvdjgTe4oE2Ou36VZ1+dC+RswmGCMwFlHqsZiIfU26SDiC3G9wH0iIg6th3LrDJYYD57l5Ps0pVjS7RAYYzu1lA2d2wGEFBJ3UEpJp257Wv2I6foeoTYXSX/XM1JUuFv1516qSqwPk4a1E6N6J+d+iWvM7BBcwakMG1XSUT4zhHrBzPPxXCCBeJHTcOoiaqXwaqsBBButSxViysvGZcBbyAxZNtmXCDh33a760XF4tb1f0mb2jW13CMARGOeubM3Z21eoc16tFkoKSD3wlzT2VlxVuUIgBT+wx4GOWgldngn2aXWInOkaEFdwABBLh5egxNBAI2tzirk6ijpRCq+gquTbEhxIwJavCfdYc0lqMevsEiZxqjoZHEf1EoId/rd3TEdclRf5OzLjbSbDICtFI82S5A+wDMXltVmB+Rw1mBZZUhvbUUOC7ARQ0pkE8DfwgPviFo2z8/i++3Mb02D57V1Mz1k6PB56QzlEOTJrmaBXSQs7U8Aiuln6CA+McwggWKBgkqhkiG9w0BBwGgggV7BIIFdzCCBXMwggVvBgsqhkiG9w0BDAoBAqCCBTcwggUzMF0GCSqGSIb3DQEFDTBQMC8GCSqGSIb3DQEFDDAiBBCl+G6epsuiNjP2afUFOwazAgIIADAKBggqhkiG9w0CCTAdBglghkgBZQMEASoEEG2FLRo+Ud+dbzCVbrer71YEggTQ81fiT0+gLnYWZpNq0MV/kPma4P+sws4wRd5CVG5rCMwmmr3JUCVk66uYLZTBXqHJ0qy3CPE2K1siImQJNS3DMD1q9WVCLPFEPLbO1ycsV73AOMc2UNJMkY7AgGCMpK+u/afMewsnAk/fmwjTw5qOm21TeesahwVvIMb3pQrkFu8FSIWK9IPRX7VCiYSa/KajiFKi0/lWEk9/LJEfikqGOB3FWYQkrV4jhhh+SNMm5LATgNgZ3FyhleruJZup0PN25W2IrpjcEBr9gHVU6gsCyB4PTTrVfopLq7goDWnOQeeAa4Y98QN6nT0EyqkfKU678/JeLz0gW8zijgdqzLwwucLg6cGE379d/2igE7/SJO8qa/JAjD3RDe88N97ysKW7vOOvIH6DnmkgQc8Cq/KKOyVlrDNx65YEft1oqVE3L5IfnmHT5ycbzyMJpdB6uL6OT9KqVLB2bHWDH47XfI8I8z56mzmKSXrWGm93beYV8u908Rokj82LHGEf9th6ttBZykWZgS+hQjc3jIU8xpa2/7mpPVFBCTiphBtp3+fCEVKmnubiiwe28Lw+xEvX8oAEGXhi5fNIGrAXvMk/rgpoh44wQwET6WnyiO8Ad8hOxvPtwgGD0m0FNFlv+yIGzY1PZeevquLKEwtvllo/A3g0OUbeGC2qC5s8VGkv11FRQPdUnOV2oXvosAWqxh6SnVrG8xbxc5L2xjJuUH8b70ne4iXzcfXo5FubtLuuJ6WNFWO9UasmvKaMqFZDlMK8FMcNTq6X0m8ilRZf056C3FDQAMxIa9mKyWebm3+4+LfxjgWo1dxvXR5HnMpzCbcoz/TIbSiUzSTaihxpzMi7Cvkc/JqPTTSkqjR+jLw7tOZucP8VtpQmQvqg2fd1hBgqam37qVC45D2765/V5v74+gtn5nc6HrGOEwpLlqcy5kojrjhQkNUkS7x5vg1KOFP/9uoC67qRFaGH5EM0XAdTVAyt1gn+StXVCXsNKvX93BPaNwL7we/zYZPpERFHaVD9R1Fw2Bz0+RzcdNQqP9yiq3mmmGNZHS0KSAKP3cmA3pwt9gPjpt/L1VNFgkVti2/YIDF37c3yuU9ZBI6kA7LhkcH5j0APr1ppS+Zxw6UKhsZDSGySqPyz3C2k4wy+R2+8mO1dN9haRW0smWWnziHWh8OFGhG+ghvc2HiX1tg2dTrByIFr9wixs4Kn9wDg0Qc1mS0+2+KacO8todl3jVYsLhcSTt5d2b/ZHuyAx4UPFtWPPF1vFdRMnf1jq83q/OEcTSfqkiEpEzs6NXpDEy1E0neq+LVHXi37IHzTGjjIvBnE2KZUoUdiFYitfDoUQpdhSpWKZTsmpqVXi/b7TZ20scvt5Qb6nfEWNds7hyGhnzAGQIV64xaDhKDB1p3QpDYxsJHvAGC1Yj9CY5w+sYpOjsfUo0qKeaFmu0fWX44s388GjZbid92/UvIxN9Lt/jri2xq+XPjJR194hc2ITUDrZvaaqeZ4odH4HXUC7FMqL6NVeX6MIv4g2QQkrt9DO35LokztOQCeuaA4rOZiM7mR3JJZIXf2jFNwElU0bvUJY2eYcQwTSQPXBzMz0AvhXxJiOFx3IQHHl55j4KMpab/NNbHChDUWJ7ptLX0/x8R3scJjCqwxJTAjBgkqhkiG9w0BCRUxFgQU6W6dFe0wnwEJqyaK7H5cUfUvxzQwRzAvMAsGCWCGSAFlAwQCAQQghQoDpo4gS46c+xoCeAykL69ZRT3zYrgNkgvL6s2UIVsEEHkNJl2cMvSFmZ8gi+cS/vQCAggA",
|
|
||||||
"key_pair_p12_password": "D173A3D9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mitm": {
|
|
||||||
"enabled": true,
|
|
||||||
"http2_enabled": true
|
|
||||||
},
|
|
||||||
"outbounds": [
|
|
||||||
{
|
|
||||||
"type": "direct",
|
|
||||||
"tag": "direct"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"route": {
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"action": "sniff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "logical",
|
|
||||||
"mode": "or",
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"network": "udp",
|
|
||||||
"port": 53
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"protocol": "dns"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"action": "hijack-dns"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ip_is_private": true,
|
|
||||||
"outbound": "direct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "resolve"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"domain": "goreleaser.com",
|
|
||||||
"action": "route-options",
|
|
||||||
"mitm": {
|
|
||||||
"enabled": true,
|
|
||||||
"surge_map_local": [
|
|
||||||
"^https://goreleaser\\.com/static/latest-pro data-type=text data=\"(update check disabled)\""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"domain": "api.gumroad.com",
|
|
||||||
"action": "route-options",
|
|
||||||
"mitm": {
|
|
||||||
"enabled": true,
|
|
||||||
"surge_map_local": [
|
|
||||||
"^https://api\\.gumroad\\.com/v2/licenses/verify data-type=file data=.github/goreleaser/response.json header=\"Content-Type:application/json\""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"auto_detect_interface": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
.github/goreleaser/configure.sh
vendored
11
.github/goreleaser/configure.sh
vendored
@@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e -o pipefail
|
|
||||||
|
|
||||||
release/local/install_minimal.sh
|
|
||||||
sudo cp .github/goreleaser/config.json /usr/local/etc/sing-box/config.json
|
|
||||||
sudo mkdir -p /var/lib/sing-box/.github/goreleaser
|
|
||||||
sudo cp .github/goreleaser/response.json /var/lib/sing-box/.github/goreleaser/response.json
|
|
||||||
go run -v ./cmd/sing-box tools install-ca .github/goreleaser/ca.crt
|
|
||||||
sudo systemctl start sing-box
|
|
||||||
sleep 5
|
|
||||||
12
.github/goreleaser/response.json
vendored
12
.github/goreleaser/response.json
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"success": true,
|
|
||||||
"purchase": {
|
|
||||||
"license_key": "fake-key",
|
|
||||||
"subscription_id": "fake-id",
|
|
||||||
"product_id": "7ev6hHL7RZc753daE5bRNw==",
|
|
||||||
"product_permalink": "https:\/\/beckersoft.gumroad.com\/l\/goreleaser",
|
|
||||||
"seller_id": "A2wDalJj66fJdFU_jwy_oA==",
|
|
||||||
"short_product_id": "CadfZ",
|
|
||||||
"permalink": "goreleaser"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
.github/setup_legacy_go.sh
vendored
7
.github/setup_legacy_go.sh
vendored
@@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
VERSION="1.23.6"
|
VERSION="1.23.6"
|
||||||
|
|
||||||
|
mkdir -p $HOME/go
|
||||||
|
cd $HOME/go
|
||||||
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||||
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||||
mv go $HOME/go/go_legacy
|
mv go go_legacy
|
||||||
cd $HOME/go/go_legacy
|
cd go_legacy
|
||||||
|
|
||||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||||
# this patch file only works on golang1.23.x
|
# this patch file only works on golang1.23.x
|
||||||
|
|||||||
284
.github/workflows/build.yml
vendored
284
.github/workflows/build.yml
vendored
@@ -50,12 +50,12 @@ jobs:
|
|||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
echo "version=${{ inputs.version }}"
|
echo "version=${{ inputs.version }}"
|
||||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Calculate version
|
- name: Calculate version
|
||||||
if: github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/read_tag --nightly
|
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
id: outputs
|
id: outputs
|
||||||
run: |-
|
run: |-
|
||||||
@@ -68,73 +68,42 @@ jobs:
|
|||||||
- calculate_version
|
- calculate_version
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
os: [ linux, windows, darwin, android ]
|
||||||
|
arch: [ "386", amd64, arm64 ]
|
||||||
|
legacy_go: [ false ]
|
||||||
include:
|
include:
|
||||||
- name: linux_386
|
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||||
goos: linux
|
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||||
goarch: 386
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||||
- name: linux_amd64
|
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||||
goos: linux
|
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||||
goarch: amd64
|
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||||
- name: linux_arm64
|
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||||
goos: linux
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
goarch: arm64
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
- name: linux_arm
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||||
goos: linux
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
- { os: windows, arch: "386", legacy_go: true }
|
||||||
- name: linux_arm_v7
|
- { os: windows, arch: amd64, legacy_go: true }
|
||||||
goos: linux
|
|
||||||
goarch: arm
|
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||||
goarm: 7
|
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||||
- name: linux_s390x
|
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||||
goos: linux
|
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||||
goarch: s390x
|
exclude:
|
||||||
- name: linux_riscv64
|
- { os: darwin, arch: "386" }
|
||||||
goos: linux
|
|
||||||
goarch: riscv64
|
|
||||||
- name: linux_mips64le
|
|
||||||
goos: linux
|
|
||||||
goarch: mips64le
|
|
||||||
- name: windows_amd64
|
|
||||||
goos: windows
|
|
||||||
goarch: amd64
|
|
||||||
require_legacy_go: true
|
|
||||||
- name: windows_386
|
|
||||||
goos: windows
|
|
||||||
goarch: 386
|
|
||||||
require_legacy_go: true
|
|
||||||
- name: windows_arm64
|
|
||||||
goos: windows
|
|
||||||
goarch: arm64
|
|
||||||
- name: darwin_arm64
|
|
||||||
goos: darwin
|
|
||||||
goarch: arm64
|
|
||||||
- name: darwin_amd64
|
|
||||||
goos: darwin
|
|
||||||
goarch: amd64
|
|
||||||
- name: android_arm64
|
|
||||||
goos: android
|
|
||||||
goarch: arm64
|
|
||||||
- name: android_arm
|
|
||||||
goos: android
|
|
||||||
goarch: arm
|
|
||||||
goarm: 7
|
|
||||||
- name: android_amd64
|
|
||||||
goos: android
|
|
||||||
goarch: amd64
|
|
||||||
- name: android_386
|
|
||||||
goos: android
|
|
||||||
goarch: 386
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
|
if: ${{ ! matrix.legacy_go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.24
|
||||||
- name: Cache legacy Go
|
- name: Cache Legacy Go
|
||||||
if: matrix.require_legacy_go
|
if: matrix.require_legacy_go
|
||||||
id: cache-legacy-go
|
id: cache-legacy-go
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -142,64 +111,142 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
~/go/go_legacy
|
~/go/go_legacy
|
||||||
key: go_legacy_1236
|
key: go_legacy_1236
|
||||||
- name: Setup legacy Go
|
- name: Setup Legacy Go
|
||||||
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||||
run: bash .github/setup_legacy_go.sh
|
run: |-
|
||||||
|
.github/setup_legacy_go.sh
|
||||||
|
- name: Setup Legacy Go 2
|
||||||
|
if: matrix.legacy_go
|
||||||
|
run: |-
|
||||||
|
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
|
||||||
|
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
if: matrix.goos == 'android'
|
if: matrix.os == 'android'
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28
|
ndk-version: r28
|
||||||
local-cache: true
|
local-cache: true
|
||||||
- name: Setup Goreleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
distribution: goreleaser-pro
|
|
||||||
version: '~> v2'
|
|
||||||
install-only: true
|
|
||||||
- name: Extract signing key
|
|
||||||
run: |-
|
|
||||||
mkdir -p $HOME/.gnupg
|
|
||||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
|
||||||
${{ secrets.GPG_KEY }}
|
|
||||||
EOF
|
|
||||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
|
||||||
|
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
|
||||||
|
TAGS="${TAGS},with_ech"
|
||||||
|
fi
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.goos != 'android'
|
if: matrix.os != 'android'
|
||||||
run: |-
|
run: |
|
||||||
goreleaser release --clean --split
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||||
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
CGO_ENABLED: "0"
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOOS: ${{ matrix.os }}
|
||||||
GOPATH: ${{ env.HOME }}/go
|
GOARCH: ${{ matrix.arch }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
|
||||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
|
||||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
|
||||||
- name: Build Android
|
- name: Build Android
|
||||||
if: matrix.goos == 'android'
|
if: matrix.os == 'android'
|
||||||
run: |-
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
go install -v ./cmd/internal/build
|
go install -v ./cmd/internal/build
|
||||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
|
export CC='${{ matrix.ndk }}-clang'
|
||||||
|
export CXX="${CC}++"
|
||||||
|
mkdir -p dist
|
||||||
|
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||||
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
BUILD_GOOS: ${{ matrix.goos }}
|
CGO_ENABLED: "1"
|
||||||
BUILD_GOARCH: ${{ matrix.goarch }}
|
BUILD_GOOS: ${{ matrix.os }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
BUILD_GOARCH: ${{ matrix.arch }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
- name: Set name
|
||||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
run: |-
|
||||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
ARM_VERSION=$([ -n '${{ matrix.goarm}}' ] && echo 'v${{ matrix.goarm}}' || true)
|
||||||
|
LEGACY=$([ '${{ matrix.legacy_go }}' = 'true' ] && echo "-legacy" || true)
|
||||||
|
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}${ARM_VERSION}${LEGACY}"
|
||||||
|
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
|
||||||
|
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||||
|
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Package DEB
|
||||||
|
if: matrix.debian != ''
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get install -y debsigs
|
||||||
|
fpm -t deb \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${PKG_NAME}.deb" \
|
||||||
|
--architecture ${{ matrix.debian }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||||
|
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||||
|
rm -rf $HOME/.gnupg
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||||
|
- name: Package RPM
|
||||||
|
if: matrix.rpm != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
fpm -t rpm \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${PKG_NAME}.rpm" \
|
||||||
|
--architecture ${{ matrix.rpm }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
cat > $HOME/.rpmmacros <<EOF
|
||||||
|
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||||
|
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
EOF
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
rpmsign --addsign dist/*.rpm
|
||||||
|
- name: Package Pacman
|
||||||
|
if: matrix.pacman != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get install -y libarchive-tools
|
||||||
|
fpm -t pacman \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
||||||
|
--architecture ${{ matrix.pacman }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
- name: Archive
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
cd dist
|
||||||
|
mkdir -p "${DIR_NAME}"
|
||||||
|
cp ../LICENSE "${DIR_NAME}"
|
||||||
|
if [ '${{ matrix.os }}' = 'windows' ]; then
|
||||||
|
cp sing-box "${DIR_NAME}/sing-box.exe"
|
||||||
|
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||||
|
else
|
||||||
|
cp sing-box "${DIR_NAME}"
|
||||||
|
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||||
|
fi
|
||||||
|
rm -r "${DIR_NAME}"
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: binary-${{ matrix.name }}
|
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||||
path: 'dist'
|
path: "dist"
|
||||||
build_android:
|
build_android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||||
@@ -271,13 +318,11 @@ jobs:
|
|||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
- name: Prepare upload
|
- name: Prepare upload
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
run: |-
|
run: |-
|
||||||
mkdir -p dist/release
|
mkdir -p dist
|
||||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
|
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
||||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
|
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: binary-android-apks
|
name: binary-android-apks
|
||||||
@@ -435,19 +480,19 @@ jobs:
|
|||||||
|
|
||||||
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
||||||
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
||||||
|
|
||||||
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
mkdir -p "$PROFILES_PATH"
|
mkdir -p "$PROFILES_PATH"
|
||||||
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||||
|
|
||||||
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
||||||
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||||
|
|
||||||
xcrun notarytool store-credentials "notarytool-password" \
|
xcrun notarytool store-credentials "notarytool-password" \
|
||||||
--key $ASC_KEY_PATH \
|
--key $ASC_KEY_PATH \
|
||||||
--key-id $ASC_KEY_ID \
|
--key-id $ASC_KEY_ID \
|
||||||
--issuer $ASC_KEY_ISSUER_ID
|
--issuer $ASC_KEY_ISSUER_ID
|
||||||
|
|
||||||
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
||||||
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
||||||
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
||||||
@@ -523,10 +568,10 @@ jobs:
|
|||||||
cd "${{ matrix.archive }}"
|
cd "${{ matrix.archive }}"
|
||||||
zip -r SFM.dSYMs.zip dSYMs
|
zip -r SFM.dSYMs.zip dSYMs
|
||||||
popd
|
popd
|
||||||
|
|
||||||
mkdir -p dist/release
|
mkdir -p dist
|
||||||
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
|
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
||||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
|
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
||||||
- name: Upload image
|
- name: Upload image
|
||||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -547,12 +592,6 @@ jobs:
|
|||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Goreleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
distribution: goreleaser-pro
|
|
||||||
version: '~> v2'
|
|
||||||
install-only: true
|
|
||||||
- name: Cache ghr
|
- name: Cache ghr
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
id: cache-ghr
|
id: cache-ghr
|
||||||
@@ -577,26 +616,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Merge builds
|
|
||||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
|
||||||
run: |-
|
|
||||||
goreleaser continue --merge --skip publish
|
|
||||||
mkdir -p dist/release
|
|
||||||
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
|
||||||
- name: Upload builds
|
- name: Upload builds
|
||||||
if: ${{ env.PUBLISHED == 'false' }}
|
if: ${{ env.PUBLISHED == 'false' }}
|
||||||
run: |-
|
run: |-
|
||||||
export PATH="$PATH:$HOME/go/bin"
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Replace builds
|
- name: Replace builds
|
||||||
if: ${{ env.PUBLISHED != 'false' }}
|
if: ${{ env.PUBLISHED != 'false' }}
|
||||||
run: |-
|
run: |-
|
||||||
export PATH="$PATH:$HOME/go/bin"
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
ghr --replace -p 5 "v${VERSION}" dist/release
|
ghr --replace -p 5 "v${VERSION}" dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
@@ -34,4 +34,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
|
verify: false
|
||||||
|
|||||||
181
.github/workflows/linux.yml
vendored
181
.github/workflows/linux.yml
vendored
@@ -1,13 +1,22 @@
|
|||||||
name: Release to Linux repository
|
name: Build Linux Packages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version name"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
calculate_version:
|
||||||
|
name: Calculate version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
@@ -17,22 +26,160 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.24
|
||||||
- name: Extract signing key
|
- name: Check input version
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
mkdir -p $HOME/.gnupg
|
echo "version=${{ inputs.version }}"
|
||||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Calculate version
|
||||||
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||||
|
- name: Set outputs
|
||||||
|
id: outputs
|
||||||
|
run: |-
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
build:
|
||||||
|
name: Build binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||||
|
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||||
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||||
|
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||||
|
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||||
|
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||||
|
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||||
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||||
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24
|
||||||
|
- name: Setup Android NDK
|
||||||
|
if: matrix.os == 'android'
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r28
|
||||||
|
local-cache: true
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
|
||||||
|
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
|
||||||
|
TAGS="${TAGS},with_ech"
|
||||||
|
fi
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
GOOS: ${{ matrix.os }}
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set mtime
|
||||||
|
run: |-
|
||||||
|
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||||
|
- name: Set name
|
||||||
|
if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||||
|
- name: Set beta name
|
||||||
|
if: contains(needs.calculate_version.outputs.version, '-')
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||||
|
- name: Set version
|
||||||
|
run: |-
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Package DEB
|
||||||
|
if: matrix.debian != ''
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get install -y debsigs
|
||||||
|
fpm -t deb \
|
||||||
|
--name "${NAME}" \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||||
|
--architecture ${{ matrix.debian }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||||
|
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||||
|
rm -rf $HOME/.gnupg
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
${{ secrets.GPG_KEY }}
|
${{ secrets.GPG_KEY }}
|
||||||
EOF
|
EOF
|
||||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||||
- name: Publish release
|
- name: Package RPM
|
||||||
uses: goreleaser/goreleaser-action@v6
|
if: matrix.rpm != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
fpm -t rpm \
|
||||||
|
--name "${NAME}" \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||||
|
--architecture ${{ matrix.rpm }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
cat > $HOME/.rpmmacros <<EOF
|
||||||
|
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||||
|
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
EOF
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
rpmsign --addsign dist/*.rpm
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||||
version: '~> v2'
|
path: "dist"
|
||||||
args: release -f .goreleaser.fury.yaml --clean
|
upload:
|
||||||
env:
|
name: Upload builds
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
runs-on: ubuntu-latest
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
needs:
|
||||||
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
|
- calculate_version
|
||||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
- build
|
||||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Download builds
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Publish packages
|
||||||
|
run: |-
|
||||||
|
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
/*.js
|
|
||||||
/*.srs
|
/*.srs
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ run:
|
|||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
- with_script
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-dirs:
|
exclude-dirs:
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ builds:
|
|||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
- with_tailscale
|
- with_tailscale
|
||||||
- with_script
|
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOTOOLCHAIN=local
|
- GOTOOLCHAIN=local
|
||||||
@@ -52,7 +51,6 @@ builds:
|
|||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
- with_tailscale
|
- with_tailscale
|
||||||
- with_script
|
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||||
@@ -128,8 +126,8 @@ nfpms:
|
|||||||
- deb
|
- deb
|
||||||
- rpm
|
- rpm
|
||||||
- archlinux
|
- archlinux
|
||||||
# - apk
|
# - apk
|
||||||
# - ipk
|
# - ipk
|
||||||
priority: extra
|
priority: extra
|
||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ RUN set -ex \
|
|||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api" \
|
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api,with_tailscale" \
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
|
|||||||
9
Makefile
9
Makefile
@@ -1,8 +1,6 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_tailscale
|
||||||
TAGS_GO123 = with_tailscale,with_script
|
|
||||||
TAGS ?= $(TAGS_GO120),$(TAGS_GO123)
|
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
|
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
@@ -20,11 +18,6 @@ build:
|
|||||||
export GOTOOLCHAIN=local && \
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
ci_build_go120:
|
|
||||||
export GOTOOLCHAIN=local && \
|
|
||||||
go build $(PARAMS) $(MAIN) && \
|
|
||||||
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
|
||||||
|
|
||||||
ci_build:
|
ci_build:
|
||||||
export GOTOOLCHAIN=local && \
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(PARAMS) $(MAIN) && \
|
go build $(PARAMS) $(MAIN) && \
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import (
|
|||||||
type CertificateStore interface {
|
type CertificateStore interface {
|
||||||
LifecycleService
|
LifecycleService
|
||||||
Pool() *x509.CertPool
|
Pool() *x509.CertPool
|
||||||
TLSDecryptionEnabled() bool
|
|
||||||
TLSDecryptionCertificate() *x509.Certificate
|
|
||||||
TLSDecryptionPrivateKey() any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||||
|
|||||||
@@ -52,10 +52,6 @@ type CacheFile interface {
|
|||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedBinary
|
LoadRuleSet(tag string) *SavedBinary
|
||||||
SaveRuleSet(tag string, set *SavedBinary) error
|
SaveRuleSet(tag string, set *SavedBinary) error
|
||||||
LoadScript(tag string) *SavedBinary
|
|
||||||
SaveScript(tag string, script *SavedBinary) error
|
|
||||||
SurgePersistentStoreRead(key string) string
|
|
||||||
SurgePersistentStoreWrite(key string, value string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedBinary struct {
|
type SavedBinary struct {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -60,8 +58,6 @@ type InboundContext struct {
|
|||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
PacketSniffError error
|
PacketSniffError error
|
||||||
HTTPRequest *http.Request
|
|
||||||
ClientHello *tls.ClientHelloInfo
|
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
@@ -78,7 +74,6 @@ type InboundContext struct {
|
|||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
TLSFragment bool
|
TLSFragment bool
|
||||||
TLSFragmentFallbackDelay time.Duration
|
TLSFragmentFallbackDelay time.Duration
|
||||||
MITM *option.MITMRouteOptions
|
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy *C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import E "github.com/sagernet/sing/common/exceptions"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StartStage uint8
|
type StartStage uint8
|
||||||
|
|
||||||
@@ -47,9 +45,6 @@ type LifecycleService interface {
|
|||||||
|
|
||||||
func Start(stage StartStage, services ...Lifecycle) error {
|
func Start(stage StartStage, services ...Lifecycle) error {
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
if service == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := service.Start(stage)
|
err := service.Start(stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MITMEngine interface {
|
|
||||||
Lifecycle
|
|
||||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ScriptManager interface {
|
|
||||||
Lifecycle
|
|
||||||
Scripts() []Script
|
|
||||||
Script(name string) (Script, bool)
|
|
||||||
SurgeCache() *SurgeInMemoryCache
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeInMemoryCache struct {
|
|
||||||
sync.RWMutex
|
|
||||||
Data map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Script interface {
|
|
||||||
Type() string
|
|
||||||
Tag() string
|
|
||||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
|
||||||
PostStart() error
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeScript interface {
|
|
||||||
Script
|
|
||||||
ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error
|
|
||||||
ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*HTTPRequestScriptResult, error)
|
|
||||||
ExecuteHTTPResponse(ctx context.Context, timeout time.Duration, request *http.Request, response *http.Response, body []byte, binaryBody bool, arguments []string) (*HTTPResponseScriptResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPRequestScriptResult struct {
|
|
||||||
URL string
|
|
||||||
Headers http.Header
|
|
||||||
Body []byte
|
|
||||||
Response *HTTPRequestScriptResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPRequestScriptResponse struct {
|
|
||||||
Status int
|
|
||||||
Headers http.Header
|
|
||||||
Body []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPResponseScriptResult struct {
|
|
||||||
Status int
|
|
||||||
Headers http.Header
|
|
||||||
Body []byte
|
|
||||||
}
|
|
||||||
51
box.go
51
box.go
@@ -23,11 +23,9 @@ import (
|
|||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/mitm"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/protocol/direct"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
"github.com/sagernet/sing-box/script"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
@@ -50,8 +48,6 @@ type Box struct {
|
|||||||
dnsRouter *dns.Router
|
dnsRouter *dns.Router
|
||||||
connection *route.ConnectionManager
|
connection *route.ConnectionManager
|
||||||
router *route.Router
|
router *route.Router
|
||||||
script *script.Manager
|
|
||||||
mitm adapter.MITMEngine //*mitm.Engine
|
|
||||||
services []adapter.LifecycleService
|
services []adapter.LifecycleService
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
@@ -147,12 +143,18 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var services []adapter.LifecycleService
|
var services []adapter.LifecycleService
|
||||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
|
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||||
if err != nil {
|
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||||
return nil, err
|
len(certificateOptions.Certificate) > 0 ||
|
||||||
|
len(certificateOptions.CertificatePath) > 0 ||
|
||||||
|
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||||
|
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
|
services = append(services, certificateStore)
|
||||||
}
|
}
|
||||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
|
||||||
services = append(services, certificateStore)
|
|
||||||
|
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
@@ -171,7 +173,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
}
|
}
|
||||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
|
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||||
service.MustRegister[adapter.Router](ctx, router)
|
service.MustRegister[adapter.Router](ctx, router)
|
||||||
@@ -179,8 +181,8 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize router")
|
return nil, E.Cause(err, "initialize router")
|
||||||
}
|
}
|
||||||
var timeService *tls.TimeServiceWrapper
|
|
||||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||||
|
var timeService *tls.TimeServiceWrapper
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
timeService = new(tls.TimeServiceWrapper)
|
timeService = new(tls.TimeServiceWrapper)
|
||||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
@@ -294,11 +296,6 @@ func New(options Options) (*Box, error) {
|
|||||||
"local",
|
"local",
|
||||||
option.LocalDNSServerOptions{},
|
option.LocalDNSServerOptions{},
|
||||||
)))
|
)))
|
||||||
scriptManager, err := script.NewManager(ctx, logFactory, options.Scripts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize script manager")
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.ScriptManager](ctx, scriptManager)
|
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -348,16 +345,6 @@ func New(options Options) (*Box, error) {
|
|||||||
timeService.TimeService = ntpService
|
timeService.TimeService = ntpService
|
||||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
mitmOptions := common.PtrValueOrDefault(options.MITM)
|
|
||||||
var mitmEngine adapter.MITMEngine
|
|
||||||
if mitmOptions.Enabled {
|
|
||||||
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create MITM engine")
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.MITMEngine](ctx, engine)
|
|
||||||
mitmEngine = engine
|
|
||||||
}
|
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
@@ -367,8 +354,6 @@ func New(options Options) (*Box, error) {
|
|||||||
dnsRouter: dnsRouter,
|
dnsRouter: dnsRouter,
|
||||||
connection: connectionManager,
|
connection: connectionManager,
|
||||||
router: router,
|
router: router,
|
||||||
script: scriptManager,
|
|
||||||
mitm: mitmEngine,
|
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.Logger(),
|
||||||
@@ -427,11 +412,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
|
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -455,7 +440,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -463,7 +448,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -482,7 +467,7 @@ func (s *Box) Close() error {
|
|||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
err := common.Close(
|
err := common.Close(
|
||||||
s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||||
)
|
)
|
||||||
for _, lifecycleService := range s.services {
|
for _, lifecycleService := range s.services {
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
|
|||||||
Submodule clients/android updated: aefe3c0290...5659088bb3
@@ -59,7 +59,7 @@ func init() {
|
|||||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_script")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api")
|
||||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||||
memcTags = append(memcTags, "with_tailscale")
|
memcTags = append(memcTags, "with_tailscale")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
|
|||||||
@@ -5,40 +5,49 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
|
"github.com/sagernet/sing-box/common/badversion"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var nightly bool
|
var (
|
||||||
|
flagRunInCI bool
|
||||||
|
flagRunNightly bool
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
|
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||||
|
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if nightly {
|
var (
|
||||||
version, err := build_shared.ReadTagVersionRev()
|
versionStr string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if flagRunNightly {
|
||||||
|
var version badversion.Version
|
||||||
|
version, err = build_shared.ReadTagVersion()
|
||||||
|
if err == nil {
|
||||||
|
versionStr = version.String()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
versionStr, err = build_shared.ReadTag()
|
||||||
|
}
|
||||||
|
if flagRunInCI {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
var versionStr string
|
|
||||||
if version.PreReleaseIdentifier != "" {
|
|
||||||
versionStr = version.VersionString() + "-nightly"
|
|
||||||
} else {
|
|
||||||
version.Patch++
|
|
||||||
versionStr = version.VersionString() + "-nightly"
|
|
||||||
}
|
|
||||||
err = setGitHubEnv("version", versionStr)
|
err = setGitHubEnv("version", versionStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
os.Stdout.WriteString("unknown\n")
|
os.Stdout.WriteString("unknown\n")
|
||||||
} else {
|
} else {
|
||||||
os.Stdout.WriteString(tag + "\n")
|
os.Stdout.WriteString(versionStr + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"software.sslmate.com/src/go-pkcs12"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagGenerateCAName string
|
|
||||||
flagGenerateCAPKCS12Password string
|
|
||||||
flagGenerateOutput string
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGenerateCAKeyPair = &cobra.Command{
|
|
||||||
Use: "ca-keypair",
|
|
||||||
Short: "Generate CA key pair",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := generateCAKeyPair()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAName, "name", "n", "", "Set custom CA name")
|
|
||||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAPKCS12Password, "p12-password", "p", "", "Set custom PKCS12 password")
|
|
||||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateOutput, "output", "o", ".", "Set output directory")
|
|
||||||
commandGenerate.AddCommand(commandGenerateCAKeyPair)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCAKeyPair() error {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
spkiASN1, err := x509.MarshalPKIXPublicKey(privateKey.Public())
|
|
||||||
var spki struct {
|
|
||||||
Algorithm pkix.AlgorithmIdentifier
|
|
||||||
SubjectPublicKey asn1.BitString
|
|
||||||
}
|
|
||||||
_, err = asn1.Unmarshal(spkiASN1, &spki)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
|
|
||||||
var caName string
|
|
||||||
if flagGenerateCAName != "" {
|
|
||||||
caName = flagGenerateCAName
|
|
||||||
} else {
|
|
||||||
caName = "sing-box Generated CA " + strings.ToUpper(hex.EncodeToString(skid[:4]))
|
|
||||||
}
|
|
||||||
caTpl := &x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{caName},
|
|
||||||
CommonName: caName,
|
|
||||||
},
|
|
||||||
SubjectKeyId: skid[:],
|
|
||||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
KeyUsage: x509.KeyUsageCertSign,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
IsCA: true,
|
|
||||||
MaxPathLenZero: true,
|
|
||||||
}
|
|
||||||
publicDer, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, privateKey.Public(), privateKey)
|
|
||||||
var caPassword string
|
|
||||||
if flagGenerateCAPKCS12Password != "" {
|
|
||||||
caPassword = flagGenerateCAPKCS12Password
|
|
||||||
} else {
|
|
||||||
caPassword = strings.ToUpper(hex.EncodeToString(skid[:4]))
|
|
||||||
}
|
|
||||||
caTpl.Raw = publicDer
|
|
||||||
p12Bytes, err := pkcs12.Modern.Encode(privateKey, caTpl, nil, caPassword)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
privateDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
|
||||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".private.pem"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}), 0o644)
|
|
||||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".crt"), publicDer, 0o644)
|
|
||||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".p12"), p12Bytes, 0o644)
|
|
||||||
var tlsDecryptionOptions option.TLSDecryptionOptions
|
|
||||||
tlsDecryptionOptions.Enabled = true
|
|
||||||
tlsDecryptionOptions.KeyPair = base64.StdEncoding.EncodeToString(p12Bytes)
|
|
||||||
tlsDecryptionOptions.KeyPairPassword = caPassword
|
|
||||||
var certificateOptions option.CertificateOptions
|
|
||||||
certificateOptions.TLSDecryption = &tlsDecryptionOptions
|
|
||||||
encoder := json.NewEncoder(os.Stdout)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
return encoder.Encode(certificateOptions)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,5 +19,36 @@ var commandTools = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
|
||||||
mainCommand.AddCommand(commandTools)
|
mainCommand.AddCommand(commandTools)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createPreStartedClient() (*box.Box, error) {
|
||||||
|
options, err := readConfigAndMerge()
|
||||||
|
if err != nil {
|
||||||
|
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create service")
|
||||||
|
}
|
||||||
|
err = instance.PreStart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "start service")
|
||||||
|
}
|
||||||
|
return instance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
|
||||||
|
if outboundTag == "" {
|
||||||
|
return instance.Outbound().Default(), nil
|
||||||
|
} else {
|
||||||
|
outbound, loaded := instance.Outbound().Outbound(outboundTag)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound not found: ", outboundTag)
|
||||||
|
}
|
||||||
|
return outbound, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
73
cmd/sing-box/cmd_tools_connect.go
Normal file
73
cmd/sing-box/cmd_tools_connect.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/task"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandConnectFlagNetwork string
|
||||||
|
|
||||||
|
var commandConnect = &cobra.Command{
|
||||||
|
Use: "connect <address>",
|
||||||
|
Short: "Connect to an address",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := connect(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type")
|
||||||
|
commandTools.AddCommand(commandConnect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(address string) error {
|
||||||
|
switch N.NetworkName(commandConnectFlagNetwork) {
|
||||||
|
case N.NetworkTCP, N.NetworkUDP:
|
||||||
|
default:
|
||||||
|
return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork)
|
||||||
|
}
|
||||||
|
instance, err := createPreStartedClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer instance.Close()
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address))
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "connect to server")
|
||||||
|
}
|
||||||
|
var group task.Group
|
||||||
|
group.Append("upload", func(ctx context.Context) error {
|
||||||
|
return common.Error(bufio.Copy(conn, os.Stdin))
|
||||||
|
})
|
||||||
|
group.Append("download", func(ctx context.Context) error {
|
||||||
|
return common.Error(bufio.Copy(os.Stdout, conn))
|
||||||
|
})
|
||||||
|
group.Cleanup(func() {
|
||||||
|
conn.Close()
|
||||||
|
})
|
||||||
|
err = group.Run(context.Background())
|
||||||
|
if E.IsClosed(err) {
|
||||||
|
log.Info(err)
|
||||||
|
} else {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
115
cmd/sing-box/cmd_tools_fetch.go
Normal file
115
cmd/sing-box/cmd_tools_fetch.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandFetch = &cobra.Command{
|
||||||
|
Use: "fetch",
|
||||||
|
Short: "Fetch an URL",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := fetch(args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandTools.AddCommand(commandFetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpClient *http.Client
|
||||||
|
http3Client *http.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func fetch(args []string) error {
|
||||||
|
instance, err := createPreStartedClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer instance.Close()
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
},
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer httpClient.CloseIdleConnections()
|
||||||
|
if C.WithQUIC {
|
||||||
|
err = initializeHTTP3Client(instance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer http3Client.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
for _, urlString := range args {
|
||||||
|
var parsedURL *url.URL
|
||||||
|
parsedURL, err = url.Parse(urlString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "":
|
||||||
|
parsedURL.Scheme = "http"
|
||||||
|
fallthrough
|
||||||
|
case "http", "https":
|
||||||
|
err = fetchHTTP(httpClient, parsedURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "http3":
|
||||||
|
if !C.WithQUIC {
|
||||||
|
return C.ErrQUICNotIncluded
|
||||||
|
}
|
||||||
|
parsedURL.Scheme = "https"
|
||||||
|
err = fetchHTTP(http3Client, parsedURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unsupported scheme: ", parsedURL.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
|
||||||
|
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Add("User-Agent", "curl/7.88.0")
|
||||||
|
response, err := httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
_, err = bufio.Copy(os.Stdout, response.Body)
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
36
cmd/sing-box/cmd_tools_fetch_http3.go
Normal file
36
cmd/sing-box/cmd_tools_fetch_http3.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http3Client = &http.Client{
|
||||||
|
Transport: &http3.Transport{
|
||||||
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
destination := M.ParseSocksaddr(addr)
|
||||||
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
|
if dErr != nil {
|
||||||
|
return nil, dErr
|
||||||
|
}
|
||||||
|
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
18
cmd/sing-box/cmd_tools_fetch_http3_stub.go
Normal file
18
cmd/sing-box/cmd_tools_fetch_http3_stub.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchHTTP3(parsedURL *url.URL) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/shell"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandInstallCACertificate = &cobra.Command{
|
|
||||||
Use: "install-ca <path to certificate>",
|
|
||||||
Short: "Install CA certificate to system",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := installCACertificate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandTools.AddCommand(commandInstallCACertificate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func installCACertificate(path string) error {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
return shell.Exec("powershell", "-Command", "Import-Certificate -FilePath \""+path+"\" -CertStoreLocation Cert:\\LocalMachine\\Root").Attach().Run()
|
|
||||||
case "darwin":
|
|
||||||
return shell.Exec("sudo", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", path).Attach().Run()
|
|
||||||
case "linux":
|
|
||||||
updateCertPath, updateCertPathNotFoundErr := exec.LookPath("update-ca-certificates")
|
|
||||||
if updateCertPathNotFoundErr == nil {
|
|
||||||
publicDer, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.MkdirAll("/usr/local/share/ca-certificates", 0o755)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrPermission) {
|
|
||||||
log.Info("Try running with sudo")
|
|
||||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileName := filepath.Base(updateCertPath)
|
|
||||||
if !strings.HasSuffix(fileName, ".crt") {
|
|
||||||
fileName = fileName + ".crt"
|
|
||||||
}
|
|
||||||
filePath, _ := filepath.Abs(filepath.Join("/usr/local/share/ca-certificates", fileName))
|
|
||||||
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrPermission) {
|
|
||||||
log.Info("Try running with sudo")
|
|
||||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate written to " + filePath + "\n")
|
|
||||||
err = shell.Exec(updateCertPath).Attach().Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate installed")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
updateTrustPath, updateTrustPathNotFoundErr := exec.LookPath("update-ca-trust")
|
|
||||||
if updateTrustPathNotFoundErr == nil {
|
|
||||||
publicDer, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileName := filepath.Base(updateTrustPath)
|
|
||||||
fileExt := filepath.Ext(path)
|
|
||||||
if fileExt != "" {
|
|
||||||
fileName = fileName[:len(fileName)-len(fileExt)]
|
|
||||||
}
|
|
||||||
filePath, _ := filepath.Abs(filepath.Join("/etc/pki/ca-trust/source/anchors/", fileName+".pem"))
|
|
||||||
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrPermission) {
|
|
||||||
log.Info("Try running with sudo")
|
|
||||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate written to " + filePath + "\n")
|
|
||||||
err = shell.Exec(updateTrustPath, "extract").Attach().Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate installed")
|
|
||||||
}
|
|
||||||
return E.New("update-ca-certificates or update-ca-trust not found")
|
|
||||||
default:
|
|
||||||
return E.New("unsupported operating system: ", runtime.GOOS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -40,11 +39,20 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncTime() error {
|
func syncTime() error {
|
||||||
|
instance, err := createPreStartedClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer instance.Close()
|
||||||
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
||||||
if serverAddress.Port == 0 {
|
if serverAddress.Port == 0 {
|
||||||
serverAddress.Port = 123
|
serverAddress.Port = 123
|
||||||
}
|
}
|
||||||
response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
|
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package certificate
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -17,8 +16,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
"software.sslmate.com/src/go-pkcs12"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.CertificateStore = (*Store)(nil)
|
var _ adapter.CertificateStore = (*Store)(nil)
|
||||||
@@ -30,9 +27,6 @@ type Store struct {
|
|||||||
certificatePaths []string
|
certificatePaths []string
|
||||||
certificateDirectoryPaths []string
|
certificateDirectoryPaths []string
|
||||||
watcher *fswatch.Watcher
|
watcher *fswatch.Watcher
|
||||||
tlsDecryptionEnabled bool
|
|
||||||
tlsDecryptionPrivateKey any
|
|
||||||
tlsDecryptionCertificate *x509.Certificate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||||
@@ -96,19 +90,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initializing certificate store")
|
return nil, E.Cause(err, "initializing certificate store")
|
||||||
}
|
}
|
||||||
if options.TLSDecryption != nil && options.TLSDecryption.Enabled {
|
|
||||||
pfxBytes, err := base64.StdEncoding.DecodeString(options.TLSDecryption.KeyPair)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode key pair base64 bytes")
|
|
||||||
}
|
|
||||||
privateKey, certificate, err := pkcs12.Decode(pfxBytes, options.TLSDecryption.KeyPairPassword)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode key pair")
|
|
||||||
}
|
|
||||||
store.tlsDecryptionEnabled = true
|
|
||||||
store.tlsDecryptionPrivateKey = privateKey
|
|
||||||
store.tlsDecryptionCertificate = certificate
|
|
||||||
}
|
|
||||||
return store, nil
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,15 +183,3 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
|||||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||||
return err == nil && !strings.Contains(target, "/")
|
return err == nil && !strings.Contains(target, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) TLSDecryptionEnabled() bool {
|
|
||||||
return s.tlsDecryptionEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) TLSDecryptionCertificate() *x509.Certificate {
|
|
||||||
return s.tlsDecryptionCertificate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) TLSDecryptionPrivateKey() any {
|
|
||||||
return s.tlsDecryptionPrivateKey
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,14 +19,18 @@ type DirectDialer interface {
|
|||||||
type DetourDialer struct {
|
type DetourDialer struct {
|
||||||
outboundManager adapter.OutboundManager
|
outboundManager adapter.OutboundManager
|
||||||
detour string
|
detour string
|
||||||
directResolver bool
|
legacyDNSDialer bool
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
initErr error
|
initErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer {
|
func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer {
|
||||||
return &DetourDialer{outboundManager: outboundManager, detour: detour}
|
return &DetourDialer{
|
||||||
|
outboundManager: outboundManager,
|
||||||
|
detour: detour,
|
||||||
|
legacyDNSDialer: legacyDNSDialer,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeDetour(dialer N.Dialer) error {
|
func InitializeDetour(dialer N.Dialer) error {
|
||||||
@@ -48,10 +52,12 @@ func (d *DetourDialer) init() {
|
|||||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
if !d.legacyDNSDialer {
|
||||||
if directDialer.IsEmpty() {
|
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
if directDialer.IsEmpty() {
|
||||||
return
|
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.dialer = dialer
|
d.dialer = dialer
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type Options struct {
|
|||||||
DirectResolver bool
|
DirectResolver bool
|
||||||
ResolverOnDetour bool
|
ResolverOnDetour bool
|
||||||
NewDialer bool
|
NewDialer bool
|
||||||
|
LegacyDNSDialer bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with NewWithOptions
|
// TODO: merge with NewWithOptions
|
||||||
@@ -45,7 +46,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
if outboundManager == nil {
|
if outboundManager == nil {
|
||||||
return nil, E.New("missing outbound manager")
|
return nil, E.New("missing outbound manager")
|
||||||
}
|
}
|
||||||
dialer = NewDetour(outboundManager, dialOptions.Detour)
|
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||||
} else {
|
} else {
|
||||||
dialer, err = NewDefault(options.Context, dialOptions)
|
dialer, err = NewDefault(options.Context, dialOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,12 +102,12 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
dnsQueryOptions.Transport = transport
|
dnsQueryOptions.Transport = transport
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
} else if options.NewDialer {
|
|
||||||
return nil, E.New("missing domain resolver for domain server address")
|
|
||||||
} else {
|
} else {
|
||||||
transports := dnsTransport.Transports()
|
transports := dnsTransport.Transports()
|
||||||
if len(transports) < 2 {
|
if len(transports) < 2 {
|
||||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||||
|
} else if options.NewDialer {
|
||||||
|
return nil, E.New("missing domain resolver for domain server address")
|
||||||
} else {
|
} else {
|
||||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,8 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
|||||||
return udpConn, err
|
return udpConn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Listener) ListenPacket(ctx context.Context, network string, address string) (net.PacketConn, error) {
|
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
|
||||||
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||||
var listenConfig net.ListenConfig
|
|
||||||
return listenConfig.ListenPacket(ctx, network, address)
|
return listenConfig.ListenPacket(ctx, network, address)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,5 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
|
|||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolHTTP
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
metadata.HTTPRequest = request
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
|||||||
if clientHello != nil {
|
if clientHello != nil {
|
||||||
metadata.Protocol = C.ProtocolTLS
|
metadata.Protocol = C.ProtocolTLS
|
||||||
metadata.Domain = clientHello.ServerName
|
metadata.Domain = clientHello.ServerName
|
||||||
metadata.ClientHello = clientHello
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||||
@@ -38,30 +35,17 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var template *x509.Certificate
|
template := &x509.Certificate{
|
||||||
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
SerialNumber: serialNumber,
|
||||||
template = &x509.Certificate{
|
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||||
SerialNumber: serialNumber,
|
NotAfter: expire,
|
||||||
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
NotAfter: expire,
|
BasicConstraintsValid: true,
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
Subject: pkix.Name{
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
CommonName: serverName,
|
||||||
BasicConstraintsValid: true,
|
},
|
||||||
}
|
DNSNames: []string{serverName},
|
||||||
} else {
|
|
||||||
template = &x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
|
||||||
NotAfter: expire,
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: serverName,
|
|
||||||
},
|
|
||||||
DNSNames: []string{serverName},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
parent = template
|
parent = template
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
const (
|
|
||||||
ScriptTypeSurge = "surge"
|
|
||||||
ScriptSourceTypeLocal = "local"
|
|
||||||
ScriptSourceTypeRemote = "remote"
|
|
||||||
)
|
|
||||||
@@ -15,6 +15,8 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
|
|||||||
}
|
}
|
||||||
responseLen := response.Len()
|
responseLen := response.Len()
|
||||||
if responseLen > maxLen {
|
if responseLen > maxLen {
|
||||||
|
copyResponse := *response
|
||||||
|
response = ©Response
|
||||||
response.Truncate(maxLen)
|
response.Truncate(maxLen)
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||||
|
|||||||
@@ -140,12 +140,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer stream.Close()
|
|
||||||
defer stream.CancelRead(0)
|
|
||||||
err = transport.WriteMessage(stream, 0, message)
|
err = transport.WriteMessage(stream, 0, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
stream.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
stream.Close()
|
||||||
return transport.ReadMessage(stream)
|
return transport.ReadMessage(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (
|
|||||||
return dialer.NewDefaultOutbound(ctx), nil
|
return dialer.NewDefaultOutbound(ctx), nil
|
||||||
} else {
|
} else {
|
||||||
return dialer.NewWithOptions(dialer.Options{
|
return dialer.NewWithOptions(dialer.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Options: options.DialerOptions,
|
Options: options.DialerOptions,
|
||||||
DirectResolver: true,
|
DirectResolver: true,
|
||||||
|
LegacyDNSDialer: options.Legacy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,10 +44,11 @@ func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions)
|
|||||||
return transportDialer, nil
|
return transportDialer, nil
|
||||||
} else {
|
} else {
|
||||||
return dialer.NewWithOptions(dialer.Options{
|
return dialer.NewWithOptions(dialer.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Options: options.DialerOptions,
|
Options: options.DialerOptions,
|
||||||
RemoteIsDomain: options.ServerIsDomain(),
|
RemoteIsDomain: options.ServerIsDomain(),
|
||||||
DirectResolver: true,
|
DirectResolver: true,
|
||||||
|
LegacyDNSDialer: options.Legacy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ func (m *TransportManager) Start(stage adapter.StartStage) error {
|
|||||||
transports := m.transports
|
transports := m.transports
|
||||||
m.access.Unlock()
|
m.access.Unlock()
|
||||||
if stage == adapter.StartStateStart {
|
if stage == adapter.StartStateStart {
|
||||||
|
if m.defaultTag != "" && m.defaultTransport == nil {
|
||||||
|
return E.New("default DNS server not found: ", m.defaultTag)
|
||||||
|
}
|
||||||
return m.startTransports(m.transports)
|
return m.startTransports(m.transports)
|
||||||
} else {
|
} else {
|
||||||
for _, outbound := range transports {
|
for _, outbound := range transports {
|
||||||
|
|||||||
@@ -2,10 +2,21 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 1.12.0-beta.1
|
#### 1.12.0-alpha.22
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
### 1.11.6
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
|
||||||
|
|
||||||
|
#### 1.12.0-alpha.19
|
||||||
|
|
||||||
|
* Update gVisor to 20250319.0
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.12.0-alpha.18
|
#### 1.12.0-alpha.18
|
||||||
|
|
||||||
* Add wildcard SNI support for ShadowTLS inbound **1**
|
* Add wildcard SNI support for ShadowTLS inbound **1**
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ Default padding scheme:
|
|||||||
|
|
||||||
```
|
```
|
||||||
stop=8
|
stop=8
|
||||||
0=34-120
|
0=30-30
|
||||||
1=100-400
|
1=100-400
|
||||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
||||||
3=500-1000
|
3=9-9,500-1000
|
||||||
4=500-1000
|
4=500-1000
|
||||||
5=500-1000
|
5=500-1000
|
||||||
6=500-1000
|
6=500-1000
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ AnyTLS 填充方案行数组。
|
|||||||
|
|
||||||
```
|
```
|
||||||
stop=8
|
stop=8
|
||||||
0=34-120
|
0=30-30
|
||||||
1=100-400
|
1=100-400
|
||||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
||||||
3=500-1000
|
3=9-9,500-1000
|
||||||
4=500-1000
|
4=500-1000
|
||||||
5=500-1000
|
5=500-1000
|
||||||
6=500-1000
|
6=500-1000
|
||||||
|
|||||||
@@ -19,12 +19,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bucketSelected = []byte("selected")
|
bucketSelected = []byte("selected")
|
||||||
bucketExpand = []byte("group_expand")
|
bucketExpand = []byte("group_expand")
|
||||||
bucketMode = []byte("clash_mode")
|
bucketMode = []byte("clash_mode")
|
||||||
bucketRuleSet = []byte("rule_set")
|
bucketRuleSet = []byte("rule_set")
|
||||||
bucketScript = []byte("script")
|
|
||||||
bucketSgPersistentStore = []byte("sg_persistent_store")
|
|
||||||
|
|
||||||
bucketNameList = []string{
|
bucketNameList = []string{
|
||||||
string(bucketSelected),
|
string(bucketSelected),
|
||||||
@@ -318,70 +316,3 @@ func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
|
|||||||
return bucket.Put([]byte(tag), setBinary)
|
return bucket.Put([]byte(tag), setBinary)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CacheFile) LoadScript(tag string) *adapter.SavedBinary {
|
|
||||||
var savedSet adapter.SavedBinary
|
|
||||||
err := c.DB.View(func(t *bbolt.Tx) error {
|
|
||||||
bucket := c.bucket(t, bucketScript)
|
|
||||||
if bucket == nil {
|
|
||||||
return os.ErrNotExist
|
|
||||||
}
|
|
||||||
scriptBinary := bucket.Get([]byte(tag))
|
|
||||||
if len(scriptBinary) == 0 {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
return savedSet.UnmarshalBinary(scriptBinary)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &savedSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CacheFile) SaveScript(tag string, set *adapter.SavedBinary) error {
|
|
||||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
|
||||||
bucket, err := c.createBucket(t, bucketScript)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
scriptBinary, err := set.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return bucket.Put([]byte(tag), scriptBinary)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CacheFile) SurgePersistentStoreRead(key string) string {
|
|
||||||
var value string
|
|
||||||
_ = c.DB.View(func(t *bbolt.Tx) error {
|
|
||||||
bucket := c.bucket(t, bucketSgPersistentStore)
|
|
||||||
if bucket == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
valueBinary := bucket.Get([]byte(key))
|
|
||||||
if len(valueBinary) > 0 {
|
|
||||||
value = string(valueBinary)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CacheFile) SurgePersistentStoreWrite(key string, value string) error {
|
|
||||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
|
||||||
if value != "" {
|
|
||||||
bucket, err := c.createBucket(t, bucketSgPersistentStore)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return bucket.Put([]byte(key), []byte(value))
|
|
||||||
} else {
|
|
||||||
bucket := c.bucket(t, bucketSgPersistentStore)
|
|
||||||
if bucket == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return bucket.Delete([]byte(key))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
package clashapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
"github.com/gofrs/uuid/v5"
|
|
||||||
"howett.net/plist"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mitmRouter(ctx context.Context) http.Handler {
|
|
||||||
r := chi.NewRouter()
|
|
||||||
r.Get("/mobileconfig", getMobileConfig(ctx))
|
|
||||||
r.Get("/certificate", getCertificate(ctx))
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMobileConfig(ctx context.Context) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
|
||||||
if !store.TLSDecryptionEnabled() {
|
|
||||||
http.NotFound(writer, request)
|
|
||||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
certificate := store.TLSDecryptionCertificate()
|
|
||||||
writer.Header().Set("Content-Type", "application/x-apple-aspen-config")
|
|
||||||
uuidGen := common.Must1(uuid.NewV4()).String()
|
|
||||||
mobileConfig := map[string]interface{}{
|
|
||||||
"PayloadContent": []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"PayloadCertificateFileName": "Certificates.cer",
|
|
||||||
"PayloadContent": certificate.Raw,
|
|
||||||
"PayloadDescription": "Adds a root certificate",
|
|
||||||
"PayloadDisplayName": certificate.Subject.CommonName,
|
|
||||||
"PayloadIdentifier": "com.apple.security.root." + uuidGen,
|
|
||||||
"PayloadType": "com.apple.security.root",
|
|
||||||
"PayloadUUID": uuidGen,
|
|
||||||
"PayloadVersion": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"PayloadDisplayName": certificate.Subject.CommonName,
|
|
||||||
"PayloadIdentifier": "io.nekohasekai.sfa.ca.profile." + uuidGen,
|
|
||||||
"PayloadRemovalDisallowed": false,
|
|
||||||
"PayloadType": "Configuration",
|
|
||||||
"PayloadUUID": uuidGen,
|
|
||||||
"PayloadVersion": 1,
|
|
||||||
}
|
|
||||||
encoder := plist.NewEncoder(writer)
|
|
||||||
encoder.Indent("\t")
|
|
||||||
encoder.Encode(mobileConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCertificate(ctx context.Context) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
|
||||||
if !store.TLSDecryptionEnabled() {
|
|
||||||
http.NotFound(writer, request)
|
|
||||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer.Header().Set("Content-Type", "application/x-x509-ca-cert")
|
|
||||||
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.crt")
|
|
||||||
writer.Write(store.TLSDecryptionCertificate().Raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -124,7 +124,6 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
|||||||
r.Mount("/profile", profileRouter())
|
r.Mount("/profile", profileRouter())
|
||||||
r.Mount("/cache", cacheRouter(ctx))
|
r.Mount("/cache", cacheRouter(ctx))
|
||||||
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
||||||
r.Mount("/mitm", mitmRouter(ctx))
|
|
||||||
|
|
||||||
s.setupMetaAPI(r)
|
s.setupMetaAPI(r)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,9 +32,4 @@ type Notification struct {
|
|||||||
Subtitle string
|
Subtitle string
|
||||||
Body string
|
Body string
|
||||||
OpenURL string
|
OpenURL string
|
||||||
Clipboard string
|
|
||||||
MediaURL string
|
|
||||||
MediaData []byte
|
|
||||||
MediaType string
|
|
||||||
Timeout int
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
go.mod
21
go.mod
@@ -3,12 +3,10 @@ module github.com/sagernet/sing-box
|
|||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/adhocore/gronx v1.19.5
|
github.com/anytls/sing-anytls v0.0.7
|
||||||
github.com/anytls/sing-anytls v0.0.6
|
|
||||||
github.com/caddyserver/certmagic v0.21.7
|
github.com/caddyserver/certmagic v0.21.7
|
||||||
github.com/cloudflare/circl v1.6.0
|
github.com/cloudflare/circl v1.6.0
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17
|
|
||||||
github.com/go-chi/chi/v5 v5.2.1
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/gofrs/uuid/v5 v5.3.1
|
github.com/gofrs/uuid/v5 v5.3.1
|
||||||
@@ -25,10 +23,10 @@ require (
|
|||||||
github.com/sagernet/cors v1.2.1
|
github.com/sagernet/cors v1.2.1
|
||||||
github.com/sagernet/fswatch v0.1.1
|
github.com/sagernet/fswatch v0.1.1
|
||||||
github.com/sagernet/gomobile v0.1.4
|
github.com/sagernet/gomobile v0.1.4
|
||||||
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff
|
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
|
||||||
github.com/sagernet/quic-go v0.49.0-beta.1
|
github.com/sagernet/quic-go v0.49.0-beta.1
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||||
github.com/sagernet/sing v0.6.4-0.20250319121229-11d8838dc56d
|
github.com/sagernet/sing v0.6.6-0.20250326051824-d39c2c2fddfa
|
||||||
github.com/sagernet/sing-mux v0.3.1
|
github.com/sagernet/sing-mux v0.3.1
|
||||||
github.com/sagernet/sing-quic v0.4.1-beta.1
|
github.com/sagernet/sing-quic v0.4.1-beta.1
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||||
@@ -37,7 +35,7 @@ require (
|
|||||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec
|
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec
|
||||||
github.com/sagernet/sing-vmess v0.2.0
|
github.com/sagernet/sing-vmess v0.2.0
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.0
|
github.com/sagernet/tailscale v1.80.3-mod.2
|
||||||
github.com/sagernet/utls v1.6.7
|
github.com/sagernet/utls v1.6.7
|
||||||
github.com/sagernet/wireguard-go v0.0.1-beta.5
|
github.com/sagernet/wireguard-go v0.0.1-beta.5
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||||
@@ -55,7 +53,6 @@ require (
|
|||||||
google.golang.org/grpc v1.70.0
|
google.golang.org/grpc v1.70.0
|
||||||
google.golang.org/protobuf v1.36.5
|
google.golang.org/protobuf v1.36.5
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//replace github.com/sagernet/sing => ../sing
|
//replace github.com/sagernet/sing => ../sing
|
||||||
@@ -75,14 +72,12 @@ require (
|
|||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
|
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/gaissmai/bart v0.11.1 // indirect
|
github.com/gaissmai/bart v0.11.1 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect
|
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||||
@@ -91,7 +86,7 @@ require (
|
|||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
|
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
@@ -109,7 +104,7 @@ require (
|
|||||||
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.9.7 // indirect
|
github.com/onsi/ginkgo/v2 v2.17.2 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||||
@@ -144,3 +139,5 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//replace github.com/sagernet/sing => ../sing
|
||||||
|
|||||||
46
go.sum
46
go.sum
@@ -1,9 +1,5 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
|
||||||
github.com/adhocore/gronx v1.19.5 h1:cwIG4nT1v9DvadxtHBe6MzE+FZ1JDvAUC45U2fl4eSQ=
|
|
||||||
github.com/adhocore/gronx v1.19.5/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
|
|
||||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||||
@@ -12,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
|||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/anytls/sing-anytls v0.0.6 h1:UatIjl/OvzWQGXQ1I2bAIkabL9WtihW0fA7G+DXGBUg=
|
github.com/anytls/sing-anytls v0.0.7 h1:0Q5dHNB2sqkFAWZCyK2vjQ/ckI5Iz3V/Frf3k7mBrGc=
|
||||||
github.com/anytls/sing-anytls v0.0.6/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
github.com/anytls/sing-anytls v0.0.7/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
|
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
|
||||||
@@ -34,7 +30,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
|||||||
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||||
@@ -43,10 +38,6 @@ github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbY
|
|||||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
||||||
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17 h1:spJaibPy2sZNwo6Q0HjBVufq7hBUj5jNFOKRoogCBow=
|
|
||||||
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
@@ -67,10 +58,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
@@ -94,8 +83,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
|
||||||
@@ -148,10 +137,10 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
|||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
|
github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
|
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
|
||||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
|
||||||
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
|
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
@@ -178,8 +167,8 @@ github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQ
|
|||||||
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
|
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
|
||||||
github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY=
|
github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY=
|
||||||
github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
|
github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
|
||||||
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff h1:mlohw3360Wg1BNGook/UHnISXhUx4Gd/3tVLs5T0nSs=
|
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38=
|
||||||
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw=
|
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
|
||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||||
@@ -189,8 +178,8 @@ github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8W
|
|||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||||
github.com/sagernet/sing v0.6.4-0.20250319121229-11d8838dc56d h1:8GJnvXlOBdgCa0spumUzPbMamkEbud4sfNTd8+1YaEg=
|
github.com/sagernet/sing v0.6.6-0.20250326051824-d39c2c2fddfa h1:18mz8gmh0/EL3Bk+hB0Xf3tGOO1p/tP1sjjhSDeyUtU=
|
||||||
github.com/sagernet/sing v0.6.4-0.20250319121229-11d8838dc56d/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing v0.6.6-0.20250326051824-d39c2c2fddfa/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||||
github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
|
github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
|
||||||
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
|
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
|
||||||
github.com/sagernet/sing-quic v0.4.1-beta.1 h1:V2VfMckT3EQR3ZdfSzJgZZDsvfZZH42QAZpnOnHKa0s=
|
github.com/sagernet/sing-quic v0.4.1-beta.1 h1:V2VfMckT3EQR3ZdfSzJgZZDsvfZZH42QAZpnOnHKa0s=
|
||||||
@@ -207,8 +196,8 @@ github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGT
|
|||||||
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.0 h1:oHIdivbR/yxoiA9d3a2rRlhYn2shY9XVF35Rr8jW508=
|
github.com/sagernet/tailscale v1.80.3-mod.2 h1:hT0CI74q727EuCcgQ+T4pvon8V0aoi4vTAxah7GsNMQ=
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.0/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
github.com/sagernet/tailscale v1.80.3-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||||
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
|
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
|
||||||
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
||||||
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
||||||
@@ -220,7 +209,6 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
|
|||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
@@ -333,8 +321,6 @@ google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultTimeFormat = "-0700 2006-01-02 15:04:05"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Options option.LogOptions
|
Options option.LogOptions
|
||||||
@@ -51,7 +47,7 @@ func New(options Options) (Factory, error) {
|
|||||||
DisableColors: logOptions.DisableColor || logFilePath != "",
|
DisableColors: logOptions.DisableColor || logFilePath != "",
|
||||||
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
||||||
FullTimestamp: logOptions.Timestamp,
|
FullTimestamp: logOptions.Timestamp,
|
||||||
TimestampFormat: DefaultTimeFormat,
|
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||||
}
|
}
|
||||||
factory := NewDefaultFactory(
|
factory := NewDefaultFactory(
|
||||||
options.Context,
|
options.Context,
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package mitm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
var surgeTinyGif = common.OnceValue(func() []byte {
|
|
||||||
return common.Must1(base64.StdEncoding.DecodeString("R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA="))
|
|
||||||
})
|
|
||||||
1099
mitm/engine.go
1099
mitm/engine.go
File diff suppressed because it is too large
Load Diff
@@ -11,13 +11,6 @@ type _CertificateOptions struct {
|
|||||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||||
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
||||||
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
|
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
|
||||||
TLSDecryption *TLSDecryptionOptions `json:"tls_decryption,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TLSDecryptionOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
KeyPair string `json:"key_pair_p12,omitempty"`
|
|
||||||
KeyPairPassword string `json:"key_pair_p12_password,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CertificateOptions _CertificateOptions
|
type CertificateOptions _CertificateOptions
|
||||||
|
|||||||
@@ -191,34 +191,24 @@ func (o *DNSServerOptions) Upgrade(ctx context.Context) error {
|
|||||||
serverType = C.DNSTypeUDP
|
serverType = C.DNSTypeUDP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var remoteOptions RemoteDNSServerOptions
|
remoteOptions := RemoteDNSServerOptions{
|
||||||
if options.Detour == "" {
|
LocalDNSServerOptions: LocalDNSServerOptions{
|
||||||
remoteOptions = RemoteDNSServerOptions{
|
DialerOptions: DialerOptions{
|
||||||
LocalDNSServerOptions: LocalDNSServerOptions{
|
Detour: options.Detour,
|
||||||
LegacyStrategy: options.Strategy,
|
DomainResolver: &DomainResolveOptions{
|
||||||
LegacyDefaultDialer: options.Detour == "",
|
Server: options.AddressResolver,
|
||||||
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
Strategy: options.AddressStrategy,
|
||||||
},
|
|
||||||
LegacyAddressResolver: options.AddressResolver,
|
|
||||||
LegacyAddressStrategy: options.AddressStrategy,
|
|
||||||
LegacyAddressFallbackDelay: options.AddressFallbackDelay,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
remoteOptions = RemoteDNSServerOptions{
|
|
||||||
LocalDNSServerOptions: LocalDNSServerOptions{
|
|
||||||
DialerOptions: DialerOptions{
|
|
||||||
Detour: options.Detour,
|
|
||||||
DomainResolver: &DomainResolveOptions{
|
|
||||||
Server: options.AddressResolver,
|
|
||||||
Strategy: options.AddressStrategy,
|
|
||||||
},
|
|
||||||
FallbackDelay: options.AddressFallbackDelay,
|
|
||||||
},
|
},
|
||||||
LegacyStrategy: options.Strategy,
|
FallbackDelay: options.AddressFallbackDelay,
|
||||||
LegacyDefaultDialer: options.Detour == "",
|
|
||||||
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
|
||||||
},
|
},
|
||||||
}
|
Legacy: true,
|
||||||
|
LegacyStrategy: options.Strategy,
|
||||||
|
LegacyDefaultDialer: options.Detour == "",
|
||||||
|
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||||
|
},
|
||||||
|
LegacyAddressResolver: options.AddressResolver,
|
||||||
|
LegacyAddressStrategy: options.AddressStrategy,
|
||||||
|
LegacyAddressFallbackDelay: options.AddressFallbackDelay,
|
||||||
}
|
}
|
||||||
switch serverType {
|
switch serverType {
|
||||||
case C.DNSTypeLocal:
|
case C.DNSTypeLocal:
|
||||||
@@ -362,6 +352,7 @@ type HostsDNSServerOptions struct {
|
|||||||
|
|
||||||
type LocalDNSServerOptions struct {
|
type LocalDNSServerOptions struct {
|
||||||
DialerOptions
|
DialerOptions
|
||||||
|
Legacy bool `json:"-"`
|
||||||
LegacyStrategy DomainStrategy `json:"-"`
|
LegacyStrategy DomainStrategy `json:"-"`
|
||||||
LegacyDefaultDialer bool `json:"-"`
|
LegacyDefaultDialer bool `json:"-"`
|
||||||
LegacyClientSubnet netip.Prefix `json:"-"`
|
LegacyClientSubnet netip.Prefix `json:"-"`
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package option
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MITMOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
HTTP2Enabled bool `json:"http2_enabled,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MITMRouteOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
Print bool `json:"print,omitempty"`
|
|
||||||
Script badoption.Listable[MITMRouteSurgeScriptOptions] `json:"surge_script,omitempty"`
|
|
||||||
SurgeURLRewrite badoption.Listable[SurgeURLRewriteLine] `json:"surge_url_rewrite,omitempty"`
|
|
||||||
SurgeHeaderRewrite badoption.Listable[SurgeHeaderRewriteLine] `json:"surge_header_rewrite,omitempty"`
|
|
||||||
SurgeBodyRewrite badoption.Listable[SurgeBodyRewriteLine] `json:"surge_body_rewrite,omitempty"`
|
|
||||||
SurgeMapLocal badoption.Listable[SurgeMapLocalLine] `json:"surge_map_local,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MITMRouteSurgeScriptOptions struct {
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
Type badoption.Listable[string] `json:"type"`
|
|
||||||
Pattern badoption.Listable[*badoption.Regexp] `json:"pattern"`
|
|
||||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
|
||||||
RequiresBody bool `json:"requires_body,omitempty"`
|
|
||||||
MaxSize int64 `json:"max_size,omitempty"`
|
|
||||||
BinaryBodyMode bool `json:"binary_body_mode,omitempty"`
|
|
||||||
Arguments badoption.Listable[string] `json:"arguments,omitempty"`
|
|
||||||
}
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
package option
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SurgeURLRewriteLine struct {
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
Destination *url.URL
|
|
||||||
Redirect bool
|
|
||||||
Reject bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeURLRewriteLine) String() string {
|
|
||||||
var fields []string
|
|
||||||
fields = append(fields, l.Pattern.String())
|
|
||||||
if l.Reject {
|
|
||||||
fields = append(fields, "_")
|
|
||||||
} else {
|
|
||||||
fields = append(fields, l.Destination.String())
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case l.Redirect:
|
|
||||||
fields = append(fields, "302")
|
|
||||||
case l.Reject:
|
|
||||||
fields = append(fields, "reject")
|
|
||||||
default:
|
|
||||||
fields = append(fields, "header")
|
|
||||||
}
|
|
||||||
return encodeSurgeKeys(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeURLRewriteLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeURLRewriteLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_url_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields) < 2 || len(fields) > 3 {
|
|
||||||
return E.New("invalid surge_url_rewrite line: ", stringValue)
|
|
||||||
}
|
|
||||||
pattern, err := regexp.Compile(fields[0].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_url_rewrite line: invalid pattern: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern = pattern
|
|
||||||
l.Destination, err = url.Parse(fields[1].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_url_rewrite line: invalid destination: ", stringValue)
|
|
||||||
}
|
|
||||||
if len(fields) == 3 {
|
|
||||||
switch fields[2].Key {
|
|
||||||
case "header":
|
|
||||||
case "302":
|
|
||||||
l.Redirect = true
|
|
||||||
case "reject":
|
|
||||||
l.Reject = true
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_url_rewrite line: invalid action: ", stringValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeHeaderRewriteLine struct {
|
|
||||||
Response bool
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
Add bool
|
|
||||||
Delete bool
|
|
||||||
Replace bool
|
|
||||||
ReplaceRegex bool
|
|
||||||
Key string
|
|
||||||
Match *regexp.Regexp
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeHeaderRewriteLine) String() string {
|
|
||||||
var fields []string
|
|
||||||
if !l.Response {
|
|
||||||
fields = append(fields, "http-request")
|
|
||||||
} else {
|
|
||||||
fields = append(fields, "http-response")
|
|
||||||
}
|
|
||||||
fields = append(fields, l.Pattern.String())
|
|
||||||
if l.Add {
|
|
||||||
fields = append(fields, "header-add")
|
|
||||||
} else if l.Delete {
|
|
||||||
fields = append(fields, "header-del")
|
|
||||||
} else if l.Replace {
|
|
||||||
fields = append(fields, "header-replace")
|
|
||||||
} else if l.ReplaceRegex {
|
|
||||||
fields = append(fields, "header-replace-regex")
|
|
||||||
}
|
|
||||||
fields = append(fields, l.Key)
|
|
||||||
if l.Add || l.Replace {
|
|
||||||
fields = append(fields, l.Value)
|
|
||||||
} else if l.ReplaceRegex {
|
|
||||||
fields = append(fields, l.Match.String(), l.Value)
|
|
||||||
}
|
|
||||||
return encodeSurgeKeys(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeHeaderRewriteLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeHeaderRewriteLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_header_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields) < 4 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: ", stringValue)
|
|
||||||
}
|
|
||||||
switch fields[0].Key {
|
|
||||||
case "http-request":
|
|
||||||
case "http-response":
|
|
||||||
l.Response = true
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_header_rewrite line: invalid type: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern, err = regexp.Compile(fields[1].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_header_rewrite line: invalid pattern: ", stringValue)
|
|
||||||
}
|
|
||||||
switch fields[2].Key {
|
|
||||||
case "header-add":
|
|
||||||
l.Add = true
|
|
||||||
if len(fields) != 5 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
|
||||||
}
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
l.Value = fields[4].Key
|
|
||||||
case "header-del":
|
|
||||||
l.Delete = true
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
case "header-replace":
|
|
||||||
l.Replace = true
|
|
||||||
if len(fields) != 5 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
|
||||||
}
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
l.Value = fields[4].Key
|
|
||||||
case "header-replace-regex":
|
|
||||||
l.ReplaceRegex = true
|
|
||||||
if len(fields) != 6 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
|
||||||
}
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
l.Match, err = regexp.Compile(fields[4].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_header_rewrite line: invalid match: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Value = fields[5].Key
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_header_rewrite line: invalid action: ", stringValue)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeBodyRewriteLine struct {
|
|
||||||
Response bool
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
Match []*regexp.Regexp
|
|
||||||
Replace []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeBodyRewriteLine) String() string {
|
|
||||||
var fields []string
|
|
||||||
if !l.Response {
|
|
||||||
fields = append(fields, "http-request")
|
|
||||||
} else {
|
|
||||||
fields = append(fields, "http-response")
|
|
||||||
}
|
|
||||||
for i := 0; i < len(l.Match); i += 2 {
|
|
||||||
fields = append(fields, l.Match[i].String(), l.Replace[i])
|
|
||||||
}
|
|
||||||
return strings.Join(fields, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeBodyRewriteLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeBodyRewriteLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_body_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields) < 4 {
|
|
||||||
return E.New("invalid surge_body_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields)%2 != 0 {
|
|
||||||
return E.New("invalid surge_body_rewrite line: ", stringValue)
|
|
||||||
}
|
|
||||||
switch fields[0].Key {
|
|
||||||
case "http-request":
|
|
||||||
case "http-response":
|
|
||||||
l.Response = true
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_body_rewrite line: invalid type: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern, err = regexp.Compile(fields[1].Key)
|
|
||||||
for i := 2; i < len(fields); i += 2 {
|
|
||||||
var match *regexp.Regexp
|
|
||||||
match, err = regexp.Compile(fields[i].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_body_rewrite line: invalid match: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Match = append(l.Match, match)
|
|
||||||
l.Replace = append(l.Replace, fields[i+1].Key)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeMapLocalLine struct {
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
StatusCode int
|
|
||||||
File bool
|
|
||||||
Text bool
|
|
||||||
TinyGif bool
|
|
||||||
Base64 bool
|
|
||||||
Data string
|
|
||||||
Base64Data []byte
|
|
||||||
Headers http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeMapLocalLine) String() string {
|
|
||||||
var fields []surgeField
|
|
||||||
fields = append(fields, surgeField{Key: l.Pattern.String()})
|
|
||||||
if l.File {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "file"})
|
|
||||||
fields = append(fields, surgeField{Key: "data", Value: l.Data})
|
|
||||||
} else if l.Text {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "text"})
|
|
||||||
fields = append(fields, surgeField{Key: "data", Value: l.Data})
|
|
||||||
} else if l.TinyGif {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "tiny-gif"})
|
|
||||||
} else if l.Base64 {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "base64"})
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: base64.StdEncoding.EncodeToString(l.Base64Data)})
|
|
||||||
}
|
|
||||||
if l.StatusCode != 0 {
|
|
||||||
fields = append(fields, surgeField{Key: "status-code", Value: F.ToString(l.StatusCode), ValueSet: true})
|
|
||||||
}
|
|
||||||
if len(l.Headers) > 0 {
|
|
||||||
var headers []string
|
|
||||||
for key, values := range l.Headers {
|
|
||||||
for _, value := range values {
|
|
||||||
headers = append(headers, key+":"+value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields = append(fields, surgeField{Key: "headers", Value: strings.Join(headers, "|")})
|
|
||||||
}
|
|
||||||
return encodeSurgeFields(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeMapLocalLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeMapLocalLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_map_local line: ", stringValue)
|
|
||||||
} else if len(fields) < 1 {
|
|
||||||
return E.New("invalid surge_map_local line: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern, err = regexp.Compile(fields[0].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_map_local line: invalid pattern: ", stringValue)
|
|
||||||
}
|
|
||||||
dataTypeField := common.Find(fields, func(it surgeField) bool {
|
|
||||||
return it.Key == "data-type"
|
|
||||||
})
|
|
||||||
if !dataTypeField.ValueSet {
|
|
||||||
return E.New("invalid surge_map_local line: missing data-type: ", stringValue)
|
|
||||||
}
|
|
||||||
switch dataTypeField.Value {
|
|
||||||
case "file":
|
|
||||||
l.File = true
|
|
||||||
case "text":
|
|
||||||
l.Text = true
|
|
||||||
case "tiny-gif":
|
|
||||||
l.TinyGif = true
|
|
||||||
case "base64":
|
|
||||||
l.Base64 = true
|
|
||||||
default:
|
|
||||||
return E.New("unsupported data-type ", dataTypeField.Value)
|
|
||||||
}
|
|
||||||
for i := 1; i < len(fields); i++ {
|
|
||||||
switch fields[i].Key {
|
|
||||||
case "data-type":
|
|
||||||
continue
|
|
||||||
case "data":
|
|
||||||
if l.File {
|
|
||||||
l.Data = fields[i].Value
|
|
||||||
} else if l.Text {
|
|
||||||
l.Data = fields[i].Value
|
|
||||||
} else if l.Base64 {
|
|
||||||
l.Base64Data, err = base64.StdEncoding.DecodeString(fields[i].Value)
|
|
||||||
if err != nil {
|
|
||||||
return E.New("invalid surge_map_local line: invalid base64 data: ", stringValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "status-code":
|
|
||||||
statusCode, err := strconv.ParseInt(fields[i].Value, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return E.New("invalid surge_map_local line: invalid status code: ", stringValue)
|
|
||||||
}
|
|
||||||
l.StatusCode = int(statusCode)
|
|
||||||
case "header":
|
|
||||||
headers := make(http.Header)
|
|
||||||
for _, headerLine := range strings.Split(fields[i].Value, "|") {
|
|
||||||
if !strings.Contains(headerLine, ":") {
|
|
||||||
return E.New("invalid surge_map_local line: headers: missing `:` in item: ", stringValue, ": ", headerLine)
|
|
||||||
}
|
|
||||||
headers.Add(common.SubstringBefore(headerLine, ":"), common.SubstringAfter(headerLine, ":"))
|
|
||||||
}
|
|
||||||
l.Headers = headers
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_map_local line: unknown options: ", fields[i].Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type surgeField struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
ValueSet bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeSurgeKeys(keys []string) string {
|
|
||||||
keys = common.Map(keys, func(it string) string {
|
|
||||||
if strings.ContainsFunc(it, unicode.IsSpace) {
|
|
||||||
return "\"" + it + "\""
|
|
||||||
} else {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return strings.Join(keys, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeSurgeFields(fields []surgeField) string {
|
|
||||||
return strings.Join(common.Map(fields, func(it surgeField) string {
|
|
||||||
if !it.ValueSet {
|
|
||||||
if strings.ContainsFunc(it.Key, unicode.IsSpace) {
|
|
||||||
return "\"" + it.Key + "\""
|
|
||||||
} else {
|
|
||||||
return it.Key
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if strings.ContainsFunc(it.Value, unicode.IsSpace) {
|
|
||||||
return it.Key + "=\"" + it.Value + "\""
|
|
||||||
} else {
|
|
||||||
return it.Key + "=" + it.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}), " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func surgeFields(s string) ([]surgeField, error) {
|
|
||||||
var (
|
|
||||||
fields []surgeField
|
|
||||||
currentField *surgeField
|
|
||||||
)
|
|
||||||
for _, field := range strings.Fields(s) {
|
|
||||||
if currentField != nil {
|
|
||||||
field = " " + field
|
|
||||||
if strings.HasSuffix(field, "\"") {
|
|
||||||
field = field[:len(field)-1]
|
|
||||||
if !currentField.ValueSet {
|
|
||||||
currentField.Key += field
|
|
||||||
} else {
|
|
||||||
currentField.Value += field
|
|
||||||
}
|
|
||||||
fields = append(fields, *currentField)
|
|
||||||
currentField = nil
|
|
||||||
} else {
|
|
||||||
if !currentField.ValueSet {
|
|
||||||
currentField.Key += field
|
|
||||||
} else {
|
|
||||||
currentField.Value += field
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !strings.Contains(field, "=") {
|
|
||||||
if strings.HasPrefix(field, "\"") {
|
|
||||||
field = field[1:]
|
|
||||||
if strings.HasSuffix(field, "\"") {
|
|
||||||
field = field[:len(field)-1]
|
|
||||||
} else {
|
|
||||||
currentField = &surgeField{Key: field}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields = append(fields, surgeField{Key: field})
|
|
||||||
} else {
|
|
||||||
key := common.SubstringBefore(field, "=")
|
|
||||||
value := common.SubstringAfter(field, "=")
|
|
||||||
if strings.HasPrefix(value, "\"") {
|
|
||||||
value = value[1:]
|
|
||||||
if strings.HasSuffix(field, "\"") {
|
|
||||||
value = value[:len(value)-1]
|
|
||||||
} else {
|
|
||||||
currentField = &surgeField{Key: key, Value: value, ValueSet: true}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields = append(fields, surgeField{Key: key, Value: value, ValueSet: true})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if currentField != nil {
|
|
||||||
return nil, E.New("invalid surge fields line: ", s)
|
|
||||||
}
|
|
||||||
return fields, nil
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,15 +13,13 @@ type _Options struct {
|
|||||||
Schema string `json:"$schema,omitempty"`
|
Schema string `json:"$schema,omitempty"`
|
||||||
Log *LogOptions `json:"log,omitempty"`
|
Log *LogOptions `json:"log,omitempty"`
|
||||||
DNS *DNSOptions `json:"dns,omitempty"`
|
DNS *DNSOptions `json:"dns,omitempty"`
|
||||||
|
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||||
|
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||||
Route *RouteOptions `json:"route,omitempty"`
|
Route *RouteOptions `json:"route,omitempty"`
|
||||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
|
||||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
|
||||||
MITM *MITMOptions `json:"mitm,omitempty"`
|
|
||||||
Scripts []Script `json:"scripts,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options _Options
|
type Options _Options
|
||||||
@@ -33,7 +32,7 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
o.RawMessage = content
|
o.RawMessage = content
|
||||||
return nil
|
return checkOptions(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogOptions struct {
|
type LogOptions struct {
|
||||||
@@ -45,3 +44,43 @@ type LogOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StubOptions struct{}
|
type StubOptions struct{}
|
||||||
|
|
||||||
|
func checkOptions(options *Options) error {
|
||||||
|
err := checkInbounds(options.Inbounds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = checkOutbounds(options.Outbounds, options.Endpoints)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkInbounds(inbounds []Inbound) error {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if seen[inbound.Tag] {
|
||||||
|
return E.New("duplicate inbound tag: ", inbound.Tag)
|
||||||
|
}
|
||||||
|
seen[inbound.Tag] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
if seen[outbound.Tag] {
|
||||||
|
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
|
||||||
|
}
|
||||||
|
seen[outbound.Tag] = true
|
||||||
|
}
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if seen[endpoint.Tag] {
|
||||||
|
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
|
||||||
|
}
|
||||||
|
seen[endpoint.Tag] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,8 +158,6 @@ type RawRouteOptionsActionOptions struct {
|
|||||||
|
|
||||||
TLSFragment bool `json:"tls_fragment,omitempty"`
|
TLSFragment bool `json:"tls_fragment,omitempty"`
|
||||||
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
||||||
|
|
||||||
MITM *MITMRouteOptions `json:"mitm,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
||||||
|
|||||||
128
option/script.go
128
option/script.go
@@ -1,128 +0,0 @@
|
|||||||
package option
|
|
||||||
|
|
||||||
import (
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
"github.com/sagernet/sing/common/json/badjson"
|
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
|
||||||
)
|
|
||||||
|
|
||||||
type _ScriptSourceOptions struct {
|
|
||||||
Source string `json:"source"`
|
|
||||||
LocalOptions LocalScriptSource `json:"-"`
|
|
||||||
RemoteOptions RemoteScriptSource `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalScriptSource struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoteScriptSource struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
DownloadDetour string `json:"download_detour,omitempty"`
|
|
||||||
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScriptSourceOptions _ScriptSourceOptions
|
|
||||||
|
|
||||||
func (o ScriptSourceOptions) MarshalJSON() ([]byte, error) {
|
|
||||||
var source any
|
|
||||||
switch o.Source {
|
|
||||||
case C.ScriptSourceTypeLocal:
|
|
||||||
source = o.LocalOptions
|
|
||||||
case C.ScriptSourceTypeRemote:
|
|
||||||
source = o.RemoteOptions
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown script source: ", o.Source)
|
|
||||||
}
|
|
||||||
return badjson.MarshallObjects((_ScriptSourceOptions)(o), source)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *ScriptSourceOptions) UnmarshalJSON(bytes []byte) error {
|
|
||||||
err := json.Unmarshal(bytes, (*_ScriptSourceOptions)(o))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var source any
|
|
||||||
switch o.Source {
|
|
||||||
case C.ScriptSourceTypeLocal:
|
|
||||||
source = &o.LocalOptions
|
|
||||||
case C.ScriptSourceTypeRemote:
|
|
||||||
source = &o.RemoteOptions
|
|
||||||
default:
|
|
||||||
return E.New("unknown script source: ", o.Source)
|
|
||||||
}
|
|
||||||
return json.Unmarshal(bytes, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make struct in order
|
|
||||||
type Script struct {
|
|
||||||
ScriptSourceOptions
|
|
||||||
ScriptOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Script) MarshalJSON() ([]byte, error) {
|
|
||||||
return badjson.MarshallObjects(s.ScriptSourceOptions, s.ScriptOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) UnmarshalJSON(bytes []byte) error {
|
|
||||||
err := json.Unmarshal(bytes, &s.ScriptSourceOptions)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return badjson.UnmarshallExcluded(bytes, &s.ScriptSourceOptions, &s.ScriptOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
type _ScriptOptions struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
SurgeOptions SurgeScriptOptions `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScriptOptions _ScriptOptions
|
|
||||||
|
|
||||||
func (o ScriptOptions) MarshalJSON() ([]byte, error) {
|
|
||||||
var v any
|
|
||||||
switch o.Type {
|
|
||||||
case C.ScriptTypeSurge:
|
|
||||||
v = &o.SurgeOptions
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown script type: ", o.Type)
|
|
||||||
}
|
|
||||||
if v == nil {
|
|
||||||
return badjson.MarshallObjects((_ScriptOptions)(o))
|
|
||||||
}
|
|
||||||
return badjson.MarshallObjects((_ScriptOptions)(o), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *ScriptOptions) UnmarshalJSON(bytes []byte) error {
|
|
||||||
err := json.Unmarshal(bytes, (*_ScriptOptions)(o))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var v any
|
|
||||||
switch o.Type {
|
|
||||||
case C.ScriptTypeSurge:
|
|
||||||
v = &o.SurgeOptions
|
|
||||||
case "":
|
|
||||||
return E.New("missing script type")
|
|
||||||
default:
|
|
||||||
return E.New("unknown script type: ", o.Type)
|
|
||||||
}
|
|
||||||
if v == nil {
|
|
||||||
// check unknown fields
|
|
||||||
return json.UnmarshalDisallowUnknownFields(bytes, &_ScriptOptions{})
|
|
||||||
}
|
|
||||||
return badjson.UnmarshallExcluded(bytes, (*_ScriptOptions)(o), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeScriptOptions struct {
|
|
||||||
CronOptions *CronScriptOptions `json:"cron,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronScriptOptions struct {
|
|
||||||
Expression string `json:"expression"`
|
|
||||||
Arguments []string `json:"arguments,omitempty"`
|
|
||||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
|
||||||
}
|
|
||||||
@@ -121,40 +121,48 @@ func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
|||||||
t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil)
|
t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
type tproxyPacketWriter struct {
|
|
||||||
ctx context.Context
|
|
||||||
source netip.AddrPort
|
|
||||||
destination M.Socksaddr
|
|
||||||
conn *net.UDPConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
|
func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
|
||||||
ctx := log.ContextWithNewID(t.ctx)
|
ctx := log.ContextWithNewID(t.ctx)
|
||||||
writer := &tproxyPacketWriter{ctx: ctx, source: source.AddrPort(), destination: destination}
|
writer := &tproxyPacketWriter{
|
||||||
|
ctx: ctx,
|
||||||
|
listener: t.listener,
|
||||||
|
source: source.AddrPort(),
|
||||||
|
destination: destination,
|
||||||
|
}
|
||||||
return true, ctx, writer, func(it error) {
|
return true, ctx, writer, func(it error) {
|
||||||
common.Close(common.PtrOrNil(writer.conn))
|
common.Close(common.PtrOrNil(writer.conn))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tproxyPacketWriter struct {
|
||||||
|
ctx context.Context
|
||||||
|
listener *listener.Listener
|
||||||
|
source netip.AddrPort
|
||||||
|
destination M.Socksaddr
|
||||||
|
conn *net.UDPConn
|
||||||
|
}
|
||||||
|
|
||||||
func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
conn := w.conn
|
if w.listener.ListenOptions().NetNs == "" {
|
||||||
if w.destination == destination && conn != nil {
|
conn := w.conn
|
||||||
_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source)
|
if w.destination == destination && conn != nil {
|
||||||
if err != nil {
|
_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source)
|
||||||
w.conn = nil
|
if err != nil {
|
||||||
|
w.conn = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
var listener net.ListenConfig
|
var listenConfig net.ListenConfig
|
||||||
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||||
listener.Control = control.Append(listener.Control, redir.TProxyWriteBack())
|
listenConfig.Control = control.Append(listenConfig.Control, redir.TProxyWriteBack())
|
||||||
packetConn, err := listener.ListenPacket(w.ctx, "udp", destination.String())
|
packetConn, err := w.listener.ListenPacket(listenConfig, w.ctx, "udp", destination.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
udpConn := packetConn.(*net.UDPConn)
|
udpConn := packetConn.(*net.UDPConn)
|
||||||
if w.destination == destination {
|
if w.listener.ListenOptions().NetNs == "" && w.destination == destination {
|
||||||
w.conn = udpConn
|
w.conn = udpConn
|
||||||
} else {
|
} else {
|
||||||
defer udpConn.Close()
|
defer udpConn.Close()
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package tailscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -147,6 +149,17 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
|
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
|
||||||
},
|
},
|
||||||
DNS: &dnsConfigurtor{},
|
DNS: &dnsConfigurtor{},
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address))
|
||||||
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return &Endpoint{
|
return &Endpoint{
|
||||||
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
||||||
@@ -446,6 +459,10 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Endpoint) Server() *tsnet.Server {
|
||||||
|
return t.server
|
||||||
|
}
|
||||||
|
|
||||||
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
||||||
if destination.Is6() {
|
if destination.Is6() {
|
||||||
return tcpip.AddrFrom16(destination.As16())
|
return tcpip.AddrFrom16(destination.As16())
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e -o pipefail
|
|
||||||
|
|
||||||
if [ -d /usr/local/go ]; then
|
|
||||||
export PATH="$PATH:/usr/local/go/bin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DIR=$(dirname "$0")
|
|
||||||
PROJECT=$DIR/../..
|
|
||||||
|
|
||||||
pushd $PROJECT
|
|
||||||
go install -v -trimpath -ldflags "-s -w -buildid=" ./cmd/sing-box
|
|
||||||
popd
|
|
||||||
|
|
||||||
sudo cp $(go env GOPATH)/bin/sing-box /usr/local/bin/
|
|
||||||
sudo mkdir -p /usr/local/etc/sing-box
|
|
||||||
sudo cp $PROJECT/release/config/config.json /usr/local/etc/sing-box/config.json
|
|
||||||
sudo cp $DIR/sing-box.service /etc/systemd/system
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
@@ -24,31 +24,23 @@ import (
|
|||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
||||||
|
|
||||||
type ConnectionManager struct {
|
type ConnectionManager struct {
|
||||||
ctx context.Context
|
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
mitm adapter.MITMEngine
|
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
connections list.List[io.Closer]
|
connections list.List[io.Closer]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnectionManager(ctx context.Context, logger logger.ContextLogger) *ConnectionManager {
|
func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager {
|
||||||
return &ConnectionManager{
|
return &ConnectionManager{
|
||||||
ctx: ctx,
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
||||||
switch stage {
|
|
||||||
case adapter.StartStateInitialize:
|
|
||||||
m.mitm = service.FromContext[adapter.MITMEngine](m.ctx)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,14 +55,6 @@ func (m *ConnectionManager) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
if metadata.MITM != nil && metadata.MITM.Enabled {
|
|
||||||
if m.mitm == nil {
|
|
||||||
m.logger.WarnContext(ctx, "MITM disabled")
|
|
||||||
} else {
|
|
||||||
m.mitm.NewConnection(ctx, this, conn, metadata, onClose)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx = adapter.WithContext(ctx, &metadata)
|
ctx = adapter.WithContext(ctx, &metadata)
|
||||||
var (
|
var (
|
||||||
remoteConn net.Conn
|
remoteConn net.Conn
|
||||||
|
|||||||
@@ -458,9 +458,6 @@ match:
|
|||||||
metadata.TLSFragment = true
|
metadata.TLSFragment = true
|
||||||
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
||||||
}
|
}
|
||||||
if routeOptions.MITM != nil && routeOptions.MITM.Enabled {
|
|
||||||
metadata.MITM = routeOptions.MITM
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch action := currentRule.Action().(type) {
|
switch action := currentRule.Action().(type) {
|
||||||
case *rule.RuleActionSniff:
|
case *rule.RuleActionSniff:
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
|||||||
UDPConnect: action.RouteOptions.UDPConnect,
|
UDPConnect: action.RouteOptions.UDPConnect,
|
||||||
TLSFragment: action.RouteOptions.TLSFragment,
|
TLSFragment: action.RouteOptions.TLSFragment,
|
||||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
||||||
MITM: action.RouteOptions.MITM,
|
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeRouteOptions:
|
case C.RuleActionTypeRouteOptions:
|
||||||
@@ -54,7 +53,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
|||||||
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
||||||
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
||||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
||||||
MITM: action.RouteOptionsOptions.MITM,
|
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeDirect:
|
case C.RuleActionTypeDirect:
|
||||||
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
||||||
@@ -154,7 +152,15 @@ func (r *RuleActionRoute) Type() string {
|
|||||||
func (r *RuleActionRoute) String() string {
|
func (r *RuleActionRoute) String() string {
|
||||||
var descriptions []string
|
var descriptions []string
|
||||||
descriptions = append(descriptions, r.Outbound)
|
descriptions = append(descriptions, r.Outbound)
|
||||||
descriptions = append(descriptions, r.Descriptions()...)
|
if r.UDPDisableDomainUnmapping {
|
||||||
|
descriptions = append(descriptions, "udp-disable-domain-unmapping")
|
||||||
|
}
|
||||||
|
if r.UDPConnect {
|
||||||
|
descriptions = append(descriptions, "udp-connect")
|
||||||
|
}
|
||||||
|
if r.TLSFragment {
|
||||||
|
descriptions = append(descriptions, "tls-fragment")
|
||||||
|
}
|
||||||
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,14 +176,13 @@ type RuleActionRouteOptions struct {
|
|||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
TLSFragment bool
|
TLSFragment bool
|
||||||
TLSFragmentFallbackDelay time.Duration
|
TLSFragmentFallbackDelay time.Duration
|
||||||
MITM *option.MITMRouteOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) Type() string {
|
func (r *RuleActionRouteOptions) Type() string {
|
||||||
return C.RuleActionTypeRouteOptions
|
return C.RuleActionTypeRouteOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) Descriptions() []string {
|
func (r *RuleActionRouteOptions) String() string {
|
||||||
var descriptions []string
|
var descriptions []string
|
||||||
if r.OverrideAddress.IsValid() {
|
if r.OverrideAddress.IsValid() {
|
||||||
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
||||||
@@ -204,22 +209,9 @@ func (r *RuleActionRouteOptions) Descriptions() []string {
|
|||||||
descriptions = append(descriptions, "udp-connect")
|
descriptions = append(descriptions, "udp-connect")
|
||||||
}
|
}
|
||||||
if r.UDPTimeout > 0 {
|
if r.UDPTimeout > 0 {
|
||||||
descriptions = append(descriptions, F.ToString("udp-timeout=", r.UDPTimeout))
|
descriptions = append(descriptions, "udp-timeout")
|
||||||
}
|
}
|
||||||
if r.TLSFragment {
|
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
|
||||||
descriptions = append(descriptions, "tls-fragment")
|
|
||||||
if r.TLSFragmentFallbackDelay > 0 {
|
|
||||||
descriptions = append(descriptions, F.ToString("tls-fragment-fallbac-delay=", r.TLSFragmentFallbackDelay.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if r.MITM != nil && r.MITM.Enabled {
|
|
||||||
descriptions = append(descriptions, "mitm")
|
|
||||||
}
|
|
||||||
return descriptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) String() string {
|
|
||||||
return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleActionDNSRoute struct {
|
type RuleActionDNSRoute struct {
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package jsc
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewUint8Array(runtime *goja.Runtime, data []byte) goja.Value {
|
|
||||||
buffer := runtime.NewArrayBuffer(data)
|
|
||||||
ctor, loaded := goja.AssertConstructor(runtimeGetUint8Array(runtime))
|
|
||||||
if !loaded {
|
|
||||||
panic(runtime.NewTypeError("missing UInt8Array constructor"))
|
|
||||||
}
|
|
||||||
array, err := ctor(nil, runtime.ToValue(buffer))
|
|
||||||
if err != nil {
|
|
||||||
panic(runtime.NewGoError(err))
|
|
||||||
}
|
|
||||||
return array
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname runtimeGetUint8Array github.com/dop251/goja.(*Runtime).getUint8Array
|
|
||||||
func runtimeGetUint8Array(r *goja.Runtime) *goja.Object
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package jsc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewUInt8Array(t *testing.T) {
|
|
||||||
runtime := goja.New()
|
|
||||||
runtime.Set("hello", jsc.NewUint8Array(runtime, []byte("world")))
|
|
||||||
result, err := runtime.RunString("hello instanceof Uint8Array")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, result.ToBoolean())
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package jsc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IsNil(value goja.Value) bool {
|
|
||||||
return value == nil || goja.IsUndefined(value) || goja.IsNull(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertObject(vm *goja.Runtime, value goja.Value, name string, nilable bool) *goja.Object {
|
|
||||||
if IsNil(value) {
|
|
||||||
if nilable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
|
||||||
}
|
|
||||||
objectValue, isObject := value.(*goja.Object)
|
|
||||||
if !isObject {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected object, but got ", value)))
|
|
||||||
}
|
|
||||||
return objectValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertString(vm *goja.Runtime, value goja.Value, name string, nilable bool) string {
|
|
||||||
if IsNil(value) {
|
|
||||||
if nilable {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
|
||||||
}
|
|
||||||
stringValue, isString := value.Export().(string)
|
|
||||||
if !isString {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, but got ", value)))
|
|
||||||
}
|
|
||||||
return stringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertInt(vm *goja.Runtime, value goja.Value, name string, nilable bool) int64 {
|
|
||||||
if IsNil(value) {
|
|
||||||
if nilable {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
|
||||||
}
|
|
||||||
integerValue, isNumber := value.Export().(int64)
|
|
||||||
if !isNumber {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected integer, but got ", value)))
|
|
||||||
}
|
|
||||||
return integerValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertBool(vm *goja.Runtime, value goja.Value, name string, nilable bool) bool {
|
|
||||||
if IsNil(value) {
|
|
||||||
if nilable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
|
||||||
}
|
|
||||||
boolValue, isBool := value.Export().(bool)
|
|
||||||
if !isBool {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected boolean, but got ", value)))
|
|
||||||
}
|
|
||||||
return boolValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
|
|
||||||
if IsNil(value) {
|
|
||||||
if nilable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
|
||||||
}
|
|
||||||
switch exportedValue := value.Export().(type) {
|
|
||||||
case []byte:
|
|
||||||
return exportedValue
|
|
||||||
case goja.ArrayBuffer:
|
|
||||||
return exportedValue.Bytes()
|
|
||||||
default:
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected Uint8Array or ArrayBuffer, but got ", value)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertStringBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
|
|
||||||
if IsNil(value) {
|
|
||||||
if nilable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
|
||||||
}
|
|
||||||
switch exportedValue := value.Export().(type) {
|
|
||||||
case string:
|
|
||||||
return []byte(exportedValue)
|
|
||||||
case []byte:
|
|
||||||
return exportedValue
|
|
||||||
case goja.ArrayBuffer:
|
|
||||||
return exportedValue.Bytes()
|
|
||||||
default:
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, Uint8Array or ArrayBuffer, but got ", value)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertFunction(vm *goja.Runtime, value goja.Value, name string) goja.Callable {
|
|
||||||
if IsNil(value) {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
|
||||||
}
|
|
||||||
functionValue, isFunction := goja.AssertFunction(value)
|
|
||||||
if !isFunction {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected function, but got ", value)))
|
|
||||||
}
|
|
||||||
return functionValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertHTTPHeader(vm *goja.Runtime, value goja.Value, name string) http.Header {
|
|
||||||
headersObject := AssertObject(vm, value, name, true)
|
|
||||||
if headersObject == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ObjectToHeaders(vm, headersObject, name)
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
package jsc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Module interface {
|
|
||||||
Runtime() *goja.Runtime
|
|
||||||
}
|
|
||||||
|
|
||||||
type Class[M Module, C any] interface {
|
|
||||||
Module() M
|
|
||||||
Runtime() *goja.Runtime
|
|
||||||
DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value))
|
|
||||||
DefineMethod(name string, method func(this C, call goja.FunctionCall) any)
|
|
||||||
DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any)
|
|
||||||
DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C)
|
|
||||||
ToValue() goja.Value
|
|
||||||
New(instance C) *goja.Object
|
|
||||||
Prototype() *goja.Object
|
|
||||||
Is(value goja.Value) bool
|
|
||||||
As(value goja.Value) C
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetClass[M Module, C any](runtime *goja.Runtime, exports *goja.Object, className string) Class[M, C] {
|
|
||||||
objectValue := exports.Get(className)
|
|
||||||
if objectValue == nil {
|
|
||||||
panic(runtime.NewTypeError("Missing class: " + className))
|
|
||||||
}
|
|
||||||
object, isObject := objectValue.(*goja.Object)
|
|
||||||
if !isObject {
|
|
||||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
|
||||||
}
|
|
||||||
classObject, isClass := object.Get("_class").(*goja.Object)
|
|
||||||
if !isClass {
|
|
||||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
|
||||||
}
|
|
||||||
class, isClass := classObject.Export().(Class[M, C])
|
|
||||||
if !isClass {
|
|
||||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
|
||||||
}
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
type goClass[M Module, C any] struct {
|
|
||||||
m M
|
|
||||||
prototype *goja.Object
|
|
||||||
constructor goja.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClass[M Module, C any](module M) Class[M, C] {
|
|
||||||
class := &goClass[M, C]{
|
|
||||||
m: module,
|
|
||||||
prototype: module.Runtime().NewObject(),
|
|
||||||
}
|
|
||||||
clazz := module.Runtime().ToValue(class).(*goja.Object)
|
|
||||||
clazz.Set("toString", module.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
|
||||||
return module.Runtime().ToValue("[sing-box Class]")
|
|
||||||
}))
|
|
||||||
class.prototype.DefineAccessorProperty("_class", class.Runtime().ToValue(func(call goja.FunctionCall) goja.Value { return clazz }), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) Module() M {
|
|
||||||
return c.m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) Runtime() *goja.Runtime {
|
|
||||||
return c.m.Runtime()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value)) {
|
|
||||||
var (
|
|
||||||
getterValue goja.Value
|
|
||||||
setterValue goja.Value
|
|
||||||
)
|
|
||||||
if getter != nil {
|
|
||||||
getterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
|
||||||
this, isThis := call.This.Export().(C)
|
|
||||||
if !isThis {
|
|
||||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.ExportType().String()))
|
|
||||||
}
|
|
||||||
return c.toValue(getter(this), goja.Null())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if setter != nil {
|
|
||||||
setterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
|
||||||
this, isThis := call.This.Export().(C)
|
|
||||||
if !isThis {
|
|
||||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
|
|
||||||
}
|
|
||||||
setter(this, call.Argument(0))
|
|
||||||
return goja.Undefined()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
c.prototype.DefineAccessorProperty(name, getterValue, setterValue, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) DefineMethod(name string, method func(this C, call goja.FunctionCall) any) {
|
|
||||||
methodValue := c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
|
||||||
this, isThis := call.This.Export().(C)
|
|
||||||
if !isThis {
|
|
||||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
|
|
||||||
}
|
|
||||||
return c.toValue(method(this, call), goja.Undefined())
|
|
||||||
})
|
|
||||||
c.prototype.Set(name, methodValue)
|
|
||||||
if name == "entries" {
|
|
||||||
c.prototype.DefineDataPropertySymbol(goja.SymIterator, methodValue, goja.FLAG_TRUE, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any) {
|
|
||||||
c.prototype.Set(name, c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
|
||||||
return c.toValue(method(c, call), goja.Undefined())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C) {
|
|
||||||
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
|
|
||||||
value := constructor(c, call)
|
|
||||||
object := c.toValue(value, goja.Undefined()).(*goja.Object)
|
|
||||||
object.SetPrototype(call.This.Prototype())
|
|
||||||
return object
|
|
||||||
}).(*goja.Object)
|
|
||||||
constructorObject.SetPrototype(c.prototype)
|
|
||||||
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
|
|
||||||
c.constructor = constructorObject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) toValue(rawValue any, defaultValue goja.Value) goja.Value {
|
|
||||||
switch value := rawValue.(type) {
|
|
||||||
case nil:
|
|
||||||
return defaultValue
|
|
||||||
case time.Time:
|
|
||||||
return TimeToValue(c.Runtime(), value)
|
|
||||||
default:
|
|
||||||
return c.Runtime().ToValue(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) ToValue() goja.Value {
|
|
||||||
if c.constructor == nil {
|
|
||||||
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
|
|
||||||
panic(c.Runtime().NewTypeError("Illegal constructor call"))
|
|
||||||
}).(*goja.Object)
|
|
||||||
constructorObject.SetPrototype(c.prototype)
|
|
||||||
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
|
|
||||||
c.constructor = constructorObject
|
|
||||||
}
|
|
||||||
return c.constructor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) New(instance C) *goja.Object {
|
|
||||||
object := c.Runtime().ToValue(instance).(*goja.Object)
|
|
||||||
object.SetPrototype(c.prototype)
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) Prototype() *goja.Object {
|
|
||||||
return c.prototype
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) Is(value goja.Value) bool {
|
|
||||||
object, isObject := value.(*goja.Object)
|
|
||||||
if !isObject {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
prototype := object.Prototype()
|
|
||||||
for prototype != nil {
|
|
||||||
if prototype == c.prototype {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
prototype = prototype.Prototype()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *goClass[M, C]) As(value goja.Value) C {
|
|
||||||
object, isObject := value.(*goja.Object)
|
|
||||||
if !isObject {
|
|
||||||
return common.DefaultValue[C]()
|
|
||||||
}
|
|
||||||
if !c.Is(object) {
|
|
||||||
return common.DefaultValue[C]()
|
|
||||||
}
|
|
||||||
return object.Export().(C)
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package jsc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HeadersToValue(runtime *goja.Runtime, headers http.Header) goja.Value {
|
|
||||||
object := runtime.NewObject()
|
|
||||||
for key, value := range headers {
|
|
||||||
if len(value) == 1 {
|
|
||||||
object.Set(key, value[0])
|
|
||||||
} else {
|
|
||||||
object.Set(key, ArrayToValue(runtime, value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
func ArrayToValue[T any](runtime *goja.Runtime, values []T) goja.Value {
|
|
||||||
return runtime.NewArray(common.Map(values, func(it T) any { return it })...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ObjectToHeaders(vm *goja.Runtime, object *goja.Object, name string) http.Header {
|
|
||||||
headers := make(http.Header)
|
|
||||||
for _, key := range object.Keys() {
|
|
||||||
valueObject := object.Get(key)
|
|
||||||
switch headerValue := valueObject.(type) {
|
|
||||||
case goja.String:
|
|
||||||
headers.Set(key, headerValue.String())
|
|
||||||
case *goja.Object:
|
|
||||||
values := headerValue.Export()
|
|
||||||
valueArray, isArray := values.([]any)
|
|
||||||
if !isArray {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, "expected string or string array, got ", valueObject.String())))
|
|
||||||
}
|
|
||||||
newValues := make([]string, 0, len(valueArray))
|
|
||||||
for _, value := range valueArray {
|
|
||||||
stringValue, isString := value.(string)
|
|
||||||
if !isString {
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got array item type: ", reflect.TypeOf(value))))
|
|
||||||
}
|
|
||||||
newValues = append(newValues, stringValue)
|
|
||||||
}
|
|
||||||
headers[key] = newValues
|
|
||||||
default:
|
|
||||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got ", valueObject.String())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package jsc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHeaders(t *testing.T) {
|
|
||||||
runtime := goja.New()
|
|
||||||
runtime.Set("headers", jsc.HeadersToValue(runtime, http.Header{
|
|
||||||
"My-Header": []string{"My-Value1", "My-Value2"},
|
|
||||||
}))
|
|
||||||
headers := runtime.Get("headers").(*goja.Object).Get("My-Header").(*goja.Object)
|
|
||||||
fmt.Println(reflect.ValueOf(headers.Export()).Type().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBody(t *testing.T) {
|
|
||||||
runtime := goja.New()
|
|
||||||
_, err := runtime.RunString(`
|
|
||||||
var responseBody = new Uint8Array([1, 2, 3, 4, 5])
|
|
||||||
`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println(reflect.TypeOf(runtime.Get("responseBody").Export()))
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package jsc
|
|
||||||
|
|
||||||
import "github.com/dop251/goja"
|
|
||||||
|
|
||||||
type Iterator[M Module, T any] struct {
|
|
||||||
c Class[M, *Iterator[M, T]]
|
|
||||||
values []T
|
|
||||||
block func(this T) any
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIterator[M Module, T any](class Class[M, *Iterator[M, T]], values []T, block func(this T) any) goja.Value {
|
|
||||||
return class.New(&Iterator[M, T]{class, values, block})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateIterator[M Module, T any](module M) Class[M, *Iterator[M, T]] {
|
|
||||||
class := NewClass[M, *Iterator[M, T]](module)
|
|
||||||
class.DefineMethod("next", (*Iterator[M, T]).next)
|
|
||||||
class.DefineMethod("toString", (*Iterator[M, T]).toString)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Iterator[M, T]) next(call goja.FunctionCall) any {
|
|
||||||
result := i.c.Runtime().NewObject()
|
|
||||||
if len(i.values) == 0 {
|
|
||||||
result.Set("done", true)
|
|
||||||
} else {
|
|
||||||
result.Set("done", false)
|
|
||||||
result.Set("value", i.block(i.values[0]))
|
|
||||||
i.values = i.values[1:]
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Iterator[M, T]) toString(call goja.FunctionCall) any {
|
|
||||||
return "[sing-box Iterator]"
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package jsc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TimeToValue(runtime *goja.Runtime, time time.Time) goja.Value {
|
|
||||||
return runtimeNewDateObject(runtime, time, true, runtimeGetDatePrototype(runtime))
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname runtimeNewDateObject github.com/dop251/goja.(*Runtime).newDateObject
|
|
||||||
func runtimeNewDateObject(r *goja.Runtime, t time.Time, isSet bool, proto *goja.Object) *goja.Object
|
|
||||||
|
|
||||||
//go:linkname runtimeGetDatePrototype github.com/dop251/goja.(*Runtime).getDatePrototype
|
|
||||||
func runtimeGetDatePrototype(r *goja.Runtime) *goja.Object
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package jsc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTimeToValue(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
runtime := goja.New()
|
|
||||||
now := time.Now()
|
|
||||||
err := runtime.Set("now", jsc.TimeToValue(runtime, now))
|
|
||||||
require.NoError(t, err)
|
|
||||||
println(runtime.Get("now").String())
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const assert = {
|
|
||||||
_isSameValue(a, b) {
|
|
||||||
if (a === b) {
|
|
||||||
// Handle +/-0 vs. -/+0
|
|
||||||
return a !== 0 || 1 / a === 1 / b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle NaN vs. NaN
|
|
||||||
return a !== a && b !== b;
|
|
||||||
},
|
|
||||||
|
|
||||||
_toString(value) {
|
|
||||||
try {
|
|
||||||
if (value === 0 && 1 / value === -Infinity) {
|
|
||||||
return '-0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name === 'TypeError') {
|
|
||||||
return Object.prototype.toString.call(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sameValue(actual, expected, message) {
|
|
||||||
if (assert._isSameValue(actual, expected)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message === undefined) {
|
|
||||||
message = '';
|
|
||||||
} else {
|
|
||||||
message += ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
message += 'Expected SameValue(«' + assert._toString(actual) + '», «' + assert._toString(expected) + '») to be true';
|
|
||||||
|
|
||||||
throw new Error(message);
|
|
||||||
},
|
|
||||||
|
|
||||||
throws(f, ctor, message) {
|
|
||||||
if (message === undefined) {
|
|
||||||
message = '';
|
|
||||||
} else {
|
|
||||||
message += ' ';
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
f();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.constructor !== ctor) {
|
|
||||||
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(message + "No exception was thrown");
|
|
||||||
},
|
|
||||||
|
|
||||||
throwsNodeError(f, ctor, code, message) {
|
|
||||||
if (message === undefined) {
|
|
||||||
message = '';
|
|
||||||
} else {
|
|
||||||
message += ' ';
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
f();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.constructor !== ctor) {
|
|
||||||
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
|
|
||||||
}
|
|
||||||
if (e.code !== code) {
|
|
||||||
throw new Error(message + "Wrong exception code was thrown: " + e.code);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(message + "No exception was thrown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = assert;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package jstest
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/script/modules/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assert.js
|
|
||||||
var assertJS []byte
|
|
||||||
|
|
||||||
func NewRegistry() *require.Registry {
|
|
||||||
return require.NewRegistry(require.WithFsEnable(true), require.WithLoader(func(path string) ([]byte, error) {
|
|
||||||
switch path {
|
|
||||||
case "assert.js":
|
|
||||||
return assertJS, nil
|
|
||||||
default:
|
|
||||||
return require.DefaultSourceLoader(path)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
//go:build with_script
|
|
||||||
|
|
||||||
package script
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
"github.com/sagernet/sing/common/task"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.ScriptManager = (*Manager)(nil)
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger logger.ContextLogger
|
|
||||||
scripts []adapter.Script
|
|
||||||
scriptByName map[string]adapter.Script
|
|
||||||
surgeCache *adapter.SurgeInMemoryCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager(ctx context.Context, logFactory log.Factory, scripts []option.Script) (*Manager, error) {
|
|
||||||
manager := &Manager{
|
|
||||||
ctx: ctx,
|
|
||||||
logger: logFactory.NewLogger("script"),
|
|
||||||
scriptByName: make(map[string]adapter.Script),
|
|
||||||
}
|
|
||||||
for _, scriptOptions := range scripts {
|
|
||||||
script, err := NewScript(ctx, logFactory.NewLogger(F.ToString("script/", scriptOptions.Type, "[", scriptOptions.Tag, "]")), scriptOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize script: ", scriptOptions.Tag)
|
|
||||||
}
|
|
||||||
manager.scripts = append(manager.scripts, script)
|
|
||||||
manager.scriptByName[scriptOptions.Tag] = script
|
|
||||||
}
|
|
||||||
return manager, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
|
||||||
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
|
||||||
switch stage {
|
|
||||||
case adapter.StartStateStart:
|
|
||||||
var cacheContext *adapter.HTTPStartContext
|
|
||||||
if len(m.scripts) > 0 {
|
|
||||||
monitor.Start("initialize rule-set")
|
|
||||||
cacheContext = adapter.NewHTTPStartContext(m.ctx)
|
|
||||||
var scriptStartGroup task.Group
|
|
||||||
for _, script := range m.scripts {
|
|
||||||
scriptInPlace := script
|
|
||||||
scriptStartGroup.Append0(func(ctx context.Context) error {
|
|
||||||
err := scriptInPlace.StartContext(ctx, cacheContext)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "initialize script/", scriptInPlace.Type(), "[", scriptInPlace.Tag(), "]")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
scriptStartGroup.Concurrency(5)
|
|
||||||
scriptStartGroup.FastFail()
|
|
||||||
err := scriptStartGroup.Run(m.ctx)
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cacheContext != nil {
|
|
||||||
cacheContext.Close()
|
|
||||||
}
|
|
||||||
case adapter.StartStatePostStart:
|
|
||||||
for _, script := range m.scripts {
|
|
||||||
monitor.Start(F.ToString("post start script/", script.Type(), "[", script.Tag(), "]"))
|
|
||||||
err := script.PostStart()
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "post start script/", script.Type(), "[", script.Tag(), "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Close() error {
|
|
||||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
|
||||||
var err error
|
|
||||||
for _, script := range m.scripts {
|
|
||||||
monitor.Start(F.ToString("close start script/", script.Type(), "[", script.Tag(), "]"))
|
|
||||||
err = E.Append(err, script.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close script/", script.Type(), "[", script.Tag(), "]")
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Scripts() []adapter.Script {
|
|
||||||
return m.scripts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Script(name string) (adapter.Script, bool) {
|
|
||||||
script, loaded := m.scriptByName[name]
|
|
||||||
return script, loaded
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) SurgeCache() *adapter.SurgeInMemoryCache {
|
|
||||||
if m.surgeCache == nil {
|
|
||||||
m.surgeCache = &adapter.SurgeInMemoryCache{
|
|
||||||
Data: make(map[string]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m.surgeCache
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
//go:build !with_script
|
|
||||||
|
|
||||||
package script
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.ScriptManager = (*Manager)(nil)
|
|
||||||
|
|
||||||
type Manager struct{}
|
|
||||||
|
|
||||||
func NewManager(ctx context.Context, logFactory log.Factory, scripts []option.Script) (*Manager, error) {
|
|
||||||
if len(scripts) > 0 {
|
|
||||||
return nil, E.New(`script is not included in this build, rebuild with -tags with_script`)
|
|
||||||
}
|
|
||||||
return (*Manager)(nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Scripts() []adapter.Script {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Script(name string) (adapter.Script, bool) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) SurgeCache() *adapter.SurgeInMemoryCache {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package boxctx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
class jsc.Class[*Module, *Context]
|
|
||||||
Context context.Context
|
|
||||||
Logger logger.ContextLogger
|
|
||||||
Tag string
|
|
||||||
StartedAt time.Time
|
|
||||||
ErrorHandler func(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromRuntime(runtime *goja.Runtime) *Context {
|
|
||||||
contextValue := runtime.Get("context")
|
|
||||||
if contextValue == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
context, isContext := contextValue.Export().(*Context)
|
|
||||||
if !isContext {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustFromRuntime(runtime *goja.Runtime) *Context {
|
|
||||||
context := FromRuntime(runtime)
|
|
||||||
if context == nil {
|
|
||||||
panic(runtime.NewTypeError("Missing sing-box context"))
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
func createContext(module *Module) jsc.Class[*Module, *Context] {
|
|
||||||
class := jsc.NewClass[*Module, *Context](module)
|
|
||||||
class.DefineMethod("toString", (*Context).toString)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) toString(call goja.FunctionCall) any {
|
|
||||||
return "[sing-box Context]"
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package boxctx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/require"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ModuleName = "context"
|
|
||||||
|
|
||||||
type Module struct {
|
|
||||||
runtime *goja.Runtime
|
|
||||||
classContext jsc.Class[*Module, *Context]
|
|
||||||
}
|
|
||||||
|
|
||||||
func Require(runtime *goja.Runtime, module *goja.Object) {
|
|
||||||
m := &Module{
|
|
||||||
runtime: runtime,
|
|
||||||
}
|
|
||||||
m.classContext = createContext(m)
|
|
||||||
exports := module.Get("exports").(*goja.Object)
|
|
||||||
exports.Set("Context", m.classContext.ToValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Enable(runtime *goja.Runtime, context *Context) {
|
|
||||||
exports := require.Require(runtime, ModuleName).ToObject(runtime)
|
|
||||||
classContext := jsc.GetClass[*Module, *Context](runtime, exports, "Context")
|
|
||||||
context.class = classContext
|
|
||||||
runtime.Set("context", classContext.New(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) Runtime() *goja.Runtime {
|
|
||||||
return m.runtime
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
package console
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
sLog "github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/boxctx"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Console struct {
|
|
||||||
class jsc.Class[*Module, *Console]
|
|
||||||
access sync.Mutex
|
|
||||||
countMap map[string]int
|
|
||||||
timeMap map[string]time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConsole(class jsc.Class[*Module, *Console]) goja.Value {
|
|
||||||
return class.New(&Console{
|
|
||||||
class: class,
|
|
||||||
countMap: make(map[string]int),
|
|
||||||
timeMap: make(map[string]time.Time),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func createConsole(m *Module) jsc.Class[*Module, *Console] {
|
|
||||||
class := jsc.NewClass[*Module, *Console](m)
|
|
||||||
class.DefineMethod("assert", (*Console).assert)
|
|
||||||
class.DefineMethod("clear", (*Console).clear)
|
|
||||||
class.DefineMethod("count", (*Console).count)
|
|
||||||
class.DefineMethod("countReset", (*Console).countReset)
|
|
||||||
class.DefineMethod("debug", (*Console).debug)
|
|
||||||
class.DefineMethod("dir", (*Console).dir)
|
|
||||||
class.DefineMethod("dirxml", (*Console).dirxml)
|
|
||||||
class.DefineMethod("error", (*Console).error)
|
|
||||||
class.DefineMethod("group", (*Console).stub)
|
|
||||||
class.DefineMethod("groupCollapsed", (*Console).stub)
|
|
||||||
class.DefineMethod("groupEnd", (*Console).stub)
|
|
||||||
class.DefineMethod("info", (*Console).info)
|
|
||||||
class.DefineMethod("log", (*Console)._log)
|
|
||||||
class.DefineMethod("profile", (*Console).stub)
|
|
||||||
class.DefineMethod("profileEnd", (*Console).profileEnd)
|
|
||||||
class.DefineMethod("table", (*Console).table)
|
|
||||||
class.DefineMethod("time", (*Console).time)
|
|
||||||
class.DefineMethod("timeEnd", (*Console).timeEnd)
|
|
||||||
class.DefineMethod("timeLog", (*Console).timeLog)
|
|
||||||
class.DefineMethod("timeStamp", (*Console).stub)
|
|
||||||
class.DefineMethod("trace", (*Console).trace)
|
|
||||||
class.DefineMethod("warn", (*Console).warn)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) stub(call goja.FunctionCall) any {
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) assert(call goja.FunctionCall) any {
|
|
||||||
assertion := call.Argument(0).ToBoolean()
|
|
||||||
if !assertion {
|
|
||||||
return c.log(logger.ContextLogger.ErrorContext, call.Arguments[1:])
|
|
||||||
}
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) clear(call goja.FunctionCall) any {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) count(call goja.FunctionCall) any {
|
|
||||||
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
|
|
||||||
if label == "" {
|
|
||||||
label = "default"
|
|
||||||
}
|
|
||||||
c.access.Lock()
|
|
||||||
newValue := c.countMap[label] + 1
|
|
||||||
c.countMap[label] = newValue
|
|
||||||
c.access.Unlock()
|
|
||||||
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, F.ToString(label, ": ", newValue))
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) countReset(call goja.FunctionCall) any {
|
|
||||||
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
|
|
||||||
if label == "" {
|
|
||||||
label = "default"
|
|
||||||
}
|
|
||||||
c.access.Lock()
|
|
||||||
delete(c.countMap, label)
|
|
||||||
c.access.Unlock()
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) log(logFunc func(logger.ContextLogger, context.Context, ...any), args []goja.Value) any {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
var formatString string
|
|
||||||
if len(args) > 0 {
|
|
||||||
formatString = args[0].String()
|
|
||||||
}
|
|
||||||
format(c.class.Runtime(), &buffer, formatString, args[1:]...)
|
|
||||||
writeLog(c.class.Runtime(), logFunc, buffer.String())
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) debug(call goja.FunctionCall) any {
|
|
||||||
return c.log(logger.ContextLogger.DebugContext, call.Arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) dir(call goja.FunctionCall) any {
|
|
||||||
object := jsc.AssertObject(c.class.Runtime(), call.Argument(0), "object", false)
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
for _, key := range object.Keys() {
|
|
||||||
value := object.Get(key)
|
|
||||||
buffer.WriteString(key)
|
|
||||||
buffer.WriteString(": ")
|
|
||||||
buffer.WriteString(value.String())
|
|
||||||
buffer.WriteString("\n")
|
|
||||||
}
|
|
||||||
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, buffer.String())
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) dirxml(call goja.FunctionCall) any {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
encoder := xml.NewEncoder(&buffer)
|
|
||||||
encoder.Indent("", " ")
|
|
||||||
encoder.Encode(call.Argument(0).Export())
|
|
||||||
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, buffer.String())
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) error(call goja.FunctionCall) any {
|
|
||||||
return c.log(logger.ContextLogger.ErrorContext, call.Arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) info(call goja.FunctionCall) any {
|
|
||||||
return c.log(logger.ContextLogger.InfoContext, call.Arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) _log(call goja.FunctionCall) any {
|
|
||||||
return c.log(logger.ContextLogger.InfoContext, call.Arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) profileEnd(call goja.FunctionCall) any {
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) table(call goja.FunctionCall) any {
|
|
||||||
return c.dir(call)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) time(call goja.FunctionCall) any {
|
|
||||||
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
|
|
||||||
if label == "" {
|
|
||||||
label = "default"
|
|
||||||
}
|
|
||||||
c.access.Lock()
|
|
||||||
c.timeMap[label] = time.Now()
|
|
||||||
c.access.Unlock()
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) timeEnd(call goja.FunctionCall) any {
|
|
||||||
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
|
|
||||||
if label == "" {
|
|
||||||
label = "default"
|
|
||||||
}
|
|
||||||
c.access.Lock()
|
|
||||||
startTime, ok := c.timeMap[label]
|
|
||||||
if !ok {
|
|
||||||
c.access.Unlock()
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
delete(c.timeMap, label)
|
|
||||||
c.access.Unlock()
|
|
||||||
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, F.ToString(label, ": ", time.Since(startTime).String(), " - - timer ended"))
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) timeLog(call goja.FunctionCall) any {
|
|
||||||
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
|
|
||||||
if label == "" {
|
|
||||||
label = "default"
|
|
||||||
}
|
|
||||||
c.access.Lock()
|
|
||||||
startTime, ok := c.timeMap[label]
|
|
||||||
c.access.Unlock()
|
|
||||||
if !ok {
|
|
||||||
writeLog(c.class.Runtime(), logger.ContextLogger.ErrorContext, F.ToString("Timer \"", label, "\" doesn't exist."))
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, F.ToString(label, ": ", time.Since(startTime)))
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) trace(call goja.FunctionCall) any {
|
|
||||||
return c.log(logger.ContextLogger.TraceContext, call.Arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) warn(call goja.FunctionCall) any {
|
|
||||||
return c.log(logger.ContextLogger.WarnContext, call.Arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeLog(runtime *goja.Runtime, logFunc func(logger.ContextLogger, context.Context, ...any), message string) {
|
|
||||||
var (
|
|
||||||
ctx context.Context
|
|
||||||
sLogger logger.ContextLogger
|
|
||||||
)
|
|
||||||
boxCtx := boxctx.FromRuntime(runtime)
|
|
||||||
if boxCtx != nil {
|
|
||||||
ctx = boxCtx.Context
|
|
||||||
sLogger = boxCtx.Logger
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
sLogger = sLog.StdLogger()
|
|
||||||
}
|
|
||||||
logFunc(sLogger, ctx, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func format(runtime *goja.Runtime, b *bytes.Buffer, f string, args ...goja.Value) {
|
|
||||||
pct := false
|
|
||||||
argNum := 0
|
|
||||||
for _, chr := range f {
|
|
||||||
if pct {
|
|
||||||
if argNum < len(args) {
|
|
||||||
if format1(runtime, chr, args[argNum], b) {
|
|
||||||
argNum++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
b.WriteByte('%')
|
|
||||||
b.WriteRune(chr)
|
|
||||||
}
|
|
||||||
pct = false
|
|
||||||
} else {
|
|
||||||
if chr == '%' {
|
|
||||||
pct = true
|
|
||||||
} else {
|
|
||||||
b.WriteRune(chr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arg := range args[argNum:] {
|
|
||||||
b.WriteByte(' ')
|
|
||||||
b.WriteString(arg.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func format1(runtime *goja.Runtime, f rune, val goja.Value, w *bytes.Buffer) bool {
|
|
||||||
switch f {
|
|
||||||
case 's':
|
|
||||||
w.WriteString(val.String())
|
|
||||||
case 'd':
|
|
||||||
w.WriteString(val.ToNumber().String())
|
|
||||||
case 'j':
|
|
||||||
if json, ok := runtime.Get("JSON").(*goja.Object); ok {
|
|
||||||
if stringify, ok := goja.AssertFunction(json.Get("stringify")); ok {
|
|
||||||
res, err := stringify(json, val)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
w.WriteString(res.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case '%':
|
|
||||||
w.WriteByte('%')
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
w.WriteByte('%')
|
|
||||||
w.WriteRune(f)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package console
|
|
||||||
|
|
||||||
type Context struct{}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package console
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/require"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ModuleName = "console"
|
|
||||||
|
|
||||||
type Module struct {
|
|
||||||
runtime *goja.Runtime
|
|
||||||
console jsc.Class[*Module, *Console]
|
|
||||||
}
|
|
||||||
|
|
||||||
func Require(runtime *goja.Runtime, module *goja.Object) {
|
|
||||||
m := &Module{
|
|
||||||
runtime: runtime,
|
|
||||||
}
|
|
||||||
m.console = createConsole(m)
|
|
||||||
exports := module.Get("exports").(*goja.Object)
|
|
||||||
exports.Set("Console", m.console.ToValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Enable(runtime *goja.Runtime) {
|
|
||||||
exports := require.Require(runtime, ModuleName).ToObject(runtime)
|
|
||||||
classConsole := jsc.GetClass[*Module, *Console](runtime, exports, "Console")
|
|
||||||
runtime.Set("console", NewConsole(classConsole))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) Runtime() *goja.Runtime {
|
|
||||||
return m.runtime
|
|
||||||
}
|
|
||||||
@@ -1,489 +0,0 @@
|
|||||||
package eventloop
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type job struct {
|
|
||||||
cancel func() bool
|
|
||||||
fn func()
|
|
||||||
idx int
|
|
||||||
|
|
||||||
cancelled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Timer struct {
|
|
||||||
job
|
|
||||||
timer *time.Timer
|
|
||||||
}
|
|
||||||
|
|
||||||
type Interval struct {
|
|
||||||
job
|
|
||||||
ticker *time.Ticker
|
|
||||||
stopChan chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Immediate struct {
|
|
||||||
job
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventLoop struct {
|
|
||||||
vm *goja.Runtime
|
|
||||||
jobChan chan func()
|
|
||||||
jobs []*job
|
|
||||||
jobCount int32
|
|
||||||
canRun int32
|
|
||||||
|
|
||||||
auxJobsLock sync.Mutex
|
|
||||||
wakeupChan chan struct{}
|
|
||||||
|
|
||||||
auxJobsSpare, auxJobs []func()
|
|
||||||
|
|
||||||
stopLock sync.Mutex
|
|
||||||
stopCond *sync.Cond
|
|
||||||
running bool
|
|
||||||
terminated bool
|
|
||||||
|
|
||||||
errorHandler func(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Enable(runtime *goja.Runtime, errorHandler func(error)) *EventLoop {
|
|
||||||
loop := &EventLoop{
|
|
||||||
vm: runtime,
|
|
||||||
jobChan: make(chan func()),
|
|
||||||
wakeupChan: make(chan struct{}, 1),
|
|
||||||
errorHandler: errorHandler,
|
|
||||||
}
|
|
||||||
loop.stopCond = sync.NewCond(&loop.stopLock)
|
|
||||||
runtime.Set("setTimeout", loop.setTimeout)
|
|
||||||
runtime.Set("setInterval", loop.setInterval)
|
|
||||||
runtime.Set("setImmediate", loop.setImmediate)
|
|
||||||
runtime.Set("clearTimeout", loop.clearTimeout)
|
|
||||||
runtime.Set("clearInterval", loop.clearInterval)
|
|
||||||
runtime.Set("clearImmediate", loop.clearImmediate)
|
|
||||||
return loop
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value {
|
|
||||||
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
|
|
||||||
delay := call.Argument(1).ToInteger()
|
|
||||||
var args []goja.Value
|
|
||||||
if len(call.Arguments) > 2 {
|
|
||||||
args = append(args, call.Arguments[2:]...)
|
|
||||||
}
|
|
||||||
f := func() {
|
|
||||||
_, err := fn(nil, args...)
|
|
||||||
if err != nil {
|
|
||||||
loop.errorHandler(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop.jobCount++
|
|
||||||
var job *job
|
|
||||||
var ret goja.Value
|
|
||||||
if repeating {
|
|
||||||
interval := loop.newInterval(f)
|
|
||||||
interval.start(loop, time.Duration(delay)*time.Millisecond)
|
|
||||||
job = &interval.job
|
|
||||||
ret = loop.vm.ToValue(interval)
|
|
||||||
} else {
|
|
||||||
timeout := loop.newTimeout(f)
|
|
||||||
timeout.start(loop, time.Duration(delay)*time.Millisecond)
|
|
||||||
job = &timeout.job
|
|
||||||
ret = loop.vm.ToValue(timeout)
|
|
||||||
}
|
|
||||||
job.idx = len(loop.jobs)
|
|
||||||
loop.jobs = append(loop.jobs, job)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value {
|
|
||||||
return loop.schedule(call, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value {
|
|
||||||
return loop.schedule(call, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) setImmediate(call goja.FunctionCall) goja.Value {
|
|
||||||
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
|
|
||||||
var args []goja.Value
|
|
||||||
if len(call.Arguments) > 1 {
|
|
||||||
args = append(args, call.Arguments[1:]...)
|
|
||||||
}
|
|
||||||
f := func() {
|
|
||||||
_, err := fn(nil, args...)
|
|
||||||
if err != nil {
|
|
||||||
loop.errorHandler(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop.jobCount++
|
|
||||||
return loop.vm.ToValue(loop.addImmediate(f))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTimeout schedules to run the specified function in the context
|
|
||||||
// of the loop as soon as possible after the specified timeout period.
|
|
||||||
// SetTimeout returns a Timer which can be passed to ClearTimeout.
|
|
||||||
// The instance of goja.Runtime that is passed to the function and any Values derived
|
|
||||||
// from it must not be used outside the function. SetTimeout is
|
|
||||||
// safe to call inside or outside the loop.
|
|
||||||
// If the loop is terminated (see Terminate()) returns nil.
|
|
||||||
func (loop *EventLoop) SetTimeout(fn func(*goja.Runtime), timeout time.Duration) *Timer {
|
|
||||||
t := loop.newTimeout(func() { fn(loop.vm) })
|
|
||||||
if loop.addAuxJob(func() {
|
|
||||||
t.start(loop, timeout)
|
|
||||||
loop.jobCount++
|
|
||||||
t.idx = len(loop.jobs)
|
|
||||||
loop.jobs = append(loop.jobs, &t.job)
|
|
||||||
}) {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet.
|
|
||||||
// ClearTimeout is safe to call inside or outside the loop.
|
|
||||||
func (loop *EventLoop) ClearTimeout(t *Timer) {
|
|
||||||
loop.addAuxJob(func() {
|
|
||||||
loop.clearTimeout(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetInterval schedules to repeatedly run the specified function in
|
|
||||||
// the context of the loop as soon as possible after every specified
|
|
||||||
// timeout period. SetInterval returns an Interval which can be
|
|
||||||
// passed to ClearInterval. The instance of goja.Runtime that is passed to the
|
|
||||||
// function and any Values derived from it must not be used outside
|
|
||||||
// the function. SetInterval is safe to call inside or outside the
|
|
||||||
// loop.
|
|
||||||
// If the loop is terminated (see Terminate()) returns nil.
|
|
||||||
func (loop *EventLoop) SetInterval(fn func(*goja.Runtime), timeout time.Duration) *Interval {
|
|
||||||
i := loop.newInterval(func() { fn(loop.vm) })
|
|
||||||
if loop.addAuxJob(func() {
|
|
||||||
i.start(loop, timeout)
|
|
||||||
loop.jobCount++
|
|
||||||
i.idx = len(loop.jobs)
|
|
||||||
loop.jobs = append(loop.jobs, &i.job)
|
|
||||||
}) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearInterval cancels an Interval returned by SetInterval.
|
|
||||||
// ClearInterval is safe to call inside or outside the loop.
|
|
||||||
func (loop *EventLoop) ClearInterval(i *Interval) {
|
|
||||||
loop.addAuxJob(func() {
|
|
||||||
loop.clearInterval(i)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) setRunning() {
|
|
||||||
loop.stopLock.Lock()
|
|
||||||
defer loop.stopLock.Unlock()
|
|
||||||
if loop.running {
|
|
||||||
panic("Loop is already started")
|
|
||||||
}
|
|
||||||
loop.running = true
|
|
||||||
atomic.StoreInt32(&loop.canRun, 1)
|
|
||||||
loop.auxJobsLock.Lock()
|
|
||||||
loop.terminated = false
|
|
||||||
loop.auxJobsLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run
|
|
||||||
// after which it stops the loop and returns.
|
|
||||||
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
|
|
||||||
// outside the function.
|
|
||||||
// Do NOT use this function while the loop is already running. Use RunOnLoop() instead.
|
|
||||||
// If the loop is already started it will panic.
|
|
||||||
func (loop *EventLoop) Run(fn func(*goja.Runtime)) {
|
|
||||||
loop.setRunning()
|
|
||||||
fn(loop.vm)
|
|
||||||
loop.run(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the event loop in the background. The loop continues to run until Stop() is called.
|
|
||||||
// If the loop is already started it will panic.
|
|
||||||
func (loop *EventLoop) Start() {
|
|
||||||
loop.setRunning()
|
|
||||||
go loop.run(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartInForeground starts the event loop in the current goroutine. The loop continues to run until Stop() is called.
|
|
||||||
// If the loop is already started it will panic.
|
|
||||||
// Use this instead of Start if you want to recover from panics that may occur while calling native Go functions from
|
|
||||||
// within setInterval and setTimeout callbacks.
|
|
||||||
func (loop *EventLoop) StartInForeground() {
|
|
||||||
loop.setRunning()
|
|
||||||
loop.run(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the loop that was started with Start(). After this function returns there will be no more jobs executed
|
|
||||||
// by the loop. It is possible to call Start() or Run() again after this to resume the execution.
|
|
||||||
// Note, it does not cancel active timeouts (use Terminate() instead if you want this).
|
|
||||||
// It is not allowed to run Start() (or Run()) and Stop() or Terminate() concurrently.
|
|
||||||
// Calling Stop() on a non-running loop has no effect.
|
|
||||||
// It is not allowed to call Stop() from the loop, because it is synchronous and cannot complete until the loop
|
|
||||||
// is not running any jobs. Use StopNoWait() instead.
|
|
||||||
// return number of jobs remaining
|
|
||||||
func (loop *EventLoop) Stop() int {
|
|
||||||
loop.stopLock.Lock()
|
|
||||||
for loop.running {
|
|
||||||
atomic.StoreInt32(&loop.canRun, 0)
|
|
||||||
loop.wakeup()
|
|
||||||
loop.stopCond.Wait()
|
|
||||||
}
|
|
||||||
loop.stopLock.Unlock()
|
|
||||||
return int(loop.jobCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopNoWait tells the loop to stop and returns immediately. Can be used inside the loop. Calling it on a
|
|
||||||
// non-running loop has no effect.
|
|
||||||
func (loop *EventLoop) StopNoWait() {
|
|
||||||
loop.stopLock.Lock()
|
|
||||||
if loop.running {
|
|
||||||
atomic.StoreInt32(&loop.canRun, 0)
|
|
||||||
loop.wakeup()
|
|
||||||
}
|
|
||||||
loop.stopLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terminate stops the loop and clears all active timeouts and intervals. After it returns there are no
|
|
||||||
// active timers or goroutines associated with the loop. Any attempt to submit a task (by using RunOnLoop(),
|
|
||||||
// SetTimeout() or SetInterval()) will not succeed.
|
|
||||||
// After being terminated the loop can be restarted again by using Start() or Run().
|
|
||||||
// This method must not be called concurrently with Stop*(), Start(), or Run().
|
|
||||||
func (loop *EventLoop) Terminate() {
|
|
||||||
loop.Stop()
|
|
||||||
|
|
||||||
loop.auxJobsLock.Lock()
|
|
||||||
loop.terminated = true
|
|
||||||
loop.auxJobsLock.Unlock()
|
|
||||||
|
|
||||||
loop.runAux()
|
|
||||||
|
|
||||||
for i := 0; i < len(loop.jobs); i++ {
|
|
||||||
job := loop.jobs[i]
|
|
||||||
if !job.cancelled {
|
|
||||||
job.cancelled = true
|
|
||||||
if job.cancel() {
|
|
||||||
loop.removeJob(job)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for len(loop.jobs) > 0 {
|
|
||||||
(<-loop.jobChan)()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunOnLoop schedules to run the specified function in the context of the loop as soon as possible.
|
|
||||||
// The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop())
|
|
||||||
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
|
|
||||||
// outside the function. It is safe to call inside or outside the loop.
|
|
||||||
// Returns true on success or false if the loop is terminated (see Terminate()).
|
|
||||||
func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) bool {
|
|
||||||
return loop.addAuxJob(func() { fn(loop.vm) })
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) runAux() {
|
|
||||||
loop.auxJobsLock.Lock()
|
|
||||||
jobs := loop.auxJobs
|
|
||||||
loop.auxJobs = loop.auxJobsSpare
|
|
||||||
loop.auxJobsLock.Unlock()
|
|
||||||
for i, job := range jobs {
|
|
||||||
job()
|
|
||||||
jobs[i] = nil
|
|
||||||
}
|
|
||||||
loop.auxJobsSpare = jobs[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) run(inBackground bool) {
|
|
||||||
loop.runAux()
|
|
||||||
if inBackground {
|
|
||||||
loop.jobCount++
|
|
||||||
}
|
|
||||||
LOOP:
|
|
||||||
for loop.jobCount > 0 {
|
|
||||||
select {
|
|
||||||
case job := <-loop.jobChan:
|
|
||||||
job()
|
|
||||||
case <-loop.wakeupChan:
|
|
||||||
loop.runAux()
|
|
||||||
if atomic.LoadInt32(&loop.canRun) == 0 {
|
|
||||||
break LOOP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if inBackground {
|
|
||||||
loop.jobCount--
|
|
||||||
}
|
|
||||||
|
|
||||||
loop.stopLock.Lock()
|
|
||||||
loop.running = false
|
|
||||||
loop.stopLock.Unlock()
|
|
||||||
loop.stopCond.Broadcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) wakeup() {
|
|
||||||
select {
|
|
||||||
case loop.wakeupChan <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) addAuxJob(fn func()) bool {
|
|
||||||
loop.auxJobsLock.Lock()
|
|
||||||
if loop.terminated {
|
|
||||||
loop.auxJobsLock.Unlock()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
loop.auxJobs = append(loop.auxJobs, fn)
|
|
||||||
loop.auxJobsLock.Unlock()
|
|
||||||
loop.wakeup()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) newTimeout(f func()) *Timer {
|
|
||||||
t := &Timer{
|
|
||||||
job: job{fn: f},
|
|
||||||
}
|
|
||||||
t.cancel = t.doCancel
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Timer) start(loop *EventLoop, timeout time.Duration) {
|
|
||||||
t.timer = time.AfterFunc(timeout, func() {
|
|
||||||
loop.jobChan <- func() {
|
|
||||||
loop.doTimeout(t)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) newInterval(f func()) *Interval {
|
|
||||||
i := &Interval{
|
|
||||||
job: job{fn: f},
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
i.cancel = i.doCancel
|
|
||||||
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Interval) start(loop *EventLoop, timeout time.Duration) {
|
|
||||||
// https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = time.Millisecond
|
|
||||||
}
|
|
||||||
i.ticker = time.NewTicker(timeout)
|
|
||||||
go i.run(loop)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) addImmediate(f func()) *Immediate {
|
|
||||||
i := &Immediate{
|
|
||||||
job: job{fn: f},
|
|
||||||
}
|
|
||||||
loop.addAuxJob(func() {
|
|
||||||
loop.doImmediate(i)
|
|
||||||
})
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) doTimeout(t *Timer) {
|
|
||||||
loop.removeJob(&t.job)
|
|
||||||
if !t.cancelled {
|
|
||||||
t.cancelled = true
|
|
||||||
loop.jobCount--
|
|
||||||
t.fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) doInterval(i *Interval) {
|
|
||||||
if !i.cancelled {
|
|
||||||
i.fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) doImmediate(i *Immediate) {
|
|
||||||
if !i.cancelled {
|
|
||||||
i.cancelled = true
|
|
||||||
loop.jobCount--
|
|
||||||
i.fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) clearTimeout(t *Timer) {
|
|
||||||
if t != nil && !t.cancelled {
|
|
||||||
t.cancelled = true
|
|
||||||
loop.jobCount--
|
|
||||||
if t.doCancel() {
|
|
||||||
loop.removeJob(&t.job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) clearInterval(i *Interval) {
|
|
||||||
if i != nil && !i.cancelled {
|
|
||||||
i.cancelled = true
|
|
||||||
loop.jobCount--
|
|
||||||
i.doCancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) removeJob(job *job) {
|
|
||||||
idx := job.idx
|
|
||||||
if idx < 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if idx < len(loop.jobs)-1 {
|
|
||||||
loop.jobs[idx] = loop.jobs[len(loop.jobs)-1]
|
|
||||||
loop.jobs[idx].idx = idx
|
|
||||||
}
|
|
||||||
loop.jobs[len(loop.jobs)-1] = nil
|
|
||||||
loop.jobs = loop.jobs[:len(loop.jobs)-1]
|
|
||||||
job.idx = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *EventLoop) clearImmediate(i *Immediate) {
|
|
||||||
if i != nil && !i.cancelled {
|
|
||||||
i.cancelled = true
|
|
||||||
loop.jobCount--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Interval) doCancel() bool {
|
|
||||||
close(i.stopChan)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Timer) doCancel() bool {
|
|
||||||
return t.timer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Interval) run(loop *EventLoop) {
|
|
||||||
L:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-i.stopChan:
|
|
||||||
i.ticker.Stop()
|
|
||||||
break L
|
|
||||||
case <-i.ticker.C:
|
|
||||||
loop.jobChan <- func() {
|
|
||||||
loop.doInterval(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop.jobChan <- func() {
|
|
||||||
loop.removeJob(&i.job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
package require
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
js "github.com/dop251/goja"
|
|
||||||
"github.com/dop251/goja/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ModuleLoader func(*js.Runtime, *js.Object)
|
|
||||||
|
|
||||||
// SourceLoader represents a function that returns a file data at a given path.
|
|
||||||
// The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory.
|
|
||||||
// This error will be ignored by the resolver and the search will continue. Any other errors will be propagated.
|
|
||||||
type SourceLoader func(path string) ([]byte, error)
|
|
||||||
|
|
||||||
var (
|
|
||||||
InvalidModuleError = errors.New("Invalid module")
|
|
||||||
IllegalModuleNameError = errors.New("Illegal module name")
|
|
||||||
NoSuchBuiltInModuleError = errors.New("No such built-in module")
|
|
||||||
ModuleFileDoesNotExistError = errors.New("module file does not exist")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Registry contains a cache of compiled modules which can be used by multiple Runtimes
|
|
||||||
type Registry struct {
|
|
||||||
sync.Mutex
|
|
||||||
native map[string]ModuleLoader
|
|
||||||
builtin map[string]ModuleLoader
|
|
||||||
compiled map[string]*js.Program
|
|
||||||
|
|
||||||
srcLoader SourceLoader
|
|
||||||
globalFolders []string
|
|
||||||
fsEnabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type RequireModule struct {
|
|
||||||
r *Registry
|
|
||||||
runtime *js.Runtime
|
|
||||||
modules map[string]*js.Object
|
|
||||||
nodeModules map[string]*js.Object
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRegistry(opts ...Option) *Registry {
|
|
||||||
r := &Registry{}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option func(*Registry)
|
|
||||||
|
|
||||||
// WithLoader sets a function which will be called by the require() function in order to get a source code for a
|
|
||||||
// module at the given path. The same function will be used to get external source maps.
|
|
||||||
// Note, this only affects the modules loaded by the require() function. If you need to use it as a source map
|
|
||||||
// loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions()
|
|
||||||
func WithLoader(srcLoader SourceLoader) Option {
|
|
||||||
return func(r *Registry) {
|
|
||||||
r.srcLoader = srcLoader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithGlobalFolders appends the given paths to the registry's list of
|
|
||||||
// global folders to search if the requested module is not found
|
|
||||||
// elsewhere. By default, a registry's global folders list is empty.
|
|
||||||
// In the reference Node.js implementation, the default global folders
|
|
||||||
// list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and
|
|
||||||
// $PREFIX/lib/node, see
|
|
||||||
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders.
|
|
||||||
func WithGlobalFolders(globalFolders ...string) Option {
|
|
||||||
return func(r *Registry) {
|
|
||||||
r.globalFolders = globalFolders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithFsEnable(enabled bool) Option {
|
|
||||||
return func(r *Registry) {
|
|
||||||
r.fsEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable adds the require() function to the specified runtime.
|
|
||||||
func (r *Registry) Enable(runtime *js.Runtime) *RequireModule {
|
|
||||||
rrt := &RequireModule{
|
|
||||||
r: r,
|
|
||||||
runtime: runtime,
|
|
||||||
modules: make(map[string]*js.Object),
|
|
||||||
nodeModules: make(map[string]*js.Object),
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime.Set("require", rrt.require)
|
|
||||||
return rrt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) RegisterNodeModule(name string, loader ModuleLoader) {
|
|
||||||
r.Lock()
|
|
||||||
defer r.Unlock()
|
|
||||||
|
|
||||||
if r.builtin == nil {
|
|
||||||
r.builtin = make(map[string]ModuleLoader)
|
|
||||||
}
|
|
||||||
name = filepathClean(name)
|
|
||||||
r.builtin[name] = loader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) {
|
|
||||||
r.Lock()
|
|
||||||
defer r.Unlock()
|
|
||||||
|
|
||||||
if r.native == nil {
|
|
||||||
r.native = make(map[string]ModuleLoader)
|
|
||||||
}
|
|
||||||
name = filepathClean(name)
|
|
||||||
r.native[name] = loader
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem.
|
|
||||||
func DefaultSourceLoader(filename string) ([]byte, error) {
|
|
||||||
fp := filepath.FromSlash(filename)
|
|
||||||
f, err := os.Open(fp)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
|
||||||
err = ModuleFileDoesNotExistError
|
|
||||||
} else if runtime.GOOS == "windows" {
|
|
||||||
if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect.
|
|
||||||
err = ModuleFileDoesNotExistError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
|
||||||
// On some systems (e.g. plan9 and FreeBSD) it is possible to use the standard read() call on directories
|
|
||||||
// which means we cannot rely on read() returning an error, we have to do stat() instead.
|
|
||||||
if fi, err := f.Stat(); err == nil {
|
|
||||||
if fi.IsDir() {
|
|
||||||
return nil, ModuleFileDoesNotExistError
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return io.ReadAll(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) getSource(p string) ([]byte, error) {
|
|
||||||
srcLoader := r.srcLoader
|
|
||||||
if srcLoader == nil {
|
|
||||||
srcLoader = DefaultSourceLoader
|
|
||||||
}
|
|
||||||
return srcLoader(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) getCompiledSource(p string) (*js.Program, error) {
|
|
||||||
r.Lock()
|
|
||||||
defer r.Unlock()
|
|
||||||
|
|
||||||
prg := r.compiled[p]
|
|
||||||
if prg == nil {
|
|
||||||
buf, err := r.getSource(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s := string(buf)
|
|
||||||
|
|
||||||
if path.Ext(p) == ".json" {
|
|
||||||
s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')"
|
|
||||||
}
|
|
||||||
|
|
||||||
source := "(function(exports, require, module) {" + s + "\n})"
|
|
||||||
parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
prg, err = js.CompileAST(parsed, false)
|
|
||||||
if err == nil {
|
|
||||||
if r.compiled == nil {
|
|
||||||
r.compiled = make(map[string]*js.Program)
|
|
||||||
}
|
|
||||||
r.compiled[p] = prg
|
|
||||||
}
|
|
||||||
return prg, err
|
|
||||||
}
|
|
||||||
return prg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) require(call js.FunctionCall) js.Value {
|
|
||||||
ret, err := r.Require(call.Argument(0).String())
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*js.Exception); !ok {
|
|
||||||
panic(r.runtime.NewGoError(err))
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func filepathClean(p string) string {
|
|
||||||
return path.Clean(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Require can be used to import modules from Go source (similar to JS require() function).
|
|
||||||
func (r *RequireModule) Require(p string) (ret js.Value, err error) {
|
|
||||||
module, err := r.resolve(p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ret = module.Get("exports")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func Require(runtime *js.Runtime, name string) js.Value {
|
|
||||||
if r, ok := js.AssertFunction(runtime.Get("require")); ok {
|
|
||||||
mod, err := r(js.Undefined(), runtime.ToValue(name))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return mod
|
|
||||||
}
|
|
||||||
panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)"))
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
package require
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
js "github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
const NodePrefix = "node:"
|
|
||||||
|
|
||||||
// NodeJS module search algorithm described by
|
|
||||||
// https://nodejs.org/api/modules.html#modules_all_together
|
|
||||||
func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) {
|
|
||||||
origPath, modpath := modpath, filepathClean(modpath)
|
|
||||||
if modpath == "" {
|
|
||||||
return nil, IllegalModuleNameError
|
|
||||||
}
|
|
||||||
|
|
||||||
var start string
|
|
||||||
err = nil
|
|
||||||
if path.IsAbs(origPath) {
|
|
||||||
start = "/"
|
|
||||||
} else {
|
|
||||||
start = r.getCurrentModulePath()
|
|
||||||
}
|
|
||||||
|
|
||||||
p := path.Join(start, modpath)
|
|
||||||
if isFileOrDirectoryPath(origPath) && r.r.fsEnabled {
|
|
||||||
if module = r.modules[p]; module != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
module, err = r.loadAsFileOrDirectory(p)
|
|
||||||
if err == nil && module != nil {
|
|
||||||
r.modules[p] = module
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
module, err = r.loadNative(origPath)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
if err == InvalidModuleError {
|
|
||||||
err = nil
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if module = r.nodeModules[p]; module != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.r.fsEnabled {
|
|
||||||
module, err = r.loadNodeModules(modpath, start)
|
|
||||||
if err == nil && module != nil {
|
|
||||||
r.nodeModules[p] = module
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if module == nil && err == nil {
|
|
||||||
err = InvalidModuleError
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadNative(path string) (*js.Object, error) {
|
|
||||||
module := r.modules[path]
|
|
||||||
if module != nil {
|
|
||||||
return module, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ldr ModuleLoader
|
|
||||||
if r.r.native != nil {
|
|
||||||
ldr = r.r.native[path]
|
|
||||||
}
|
|
||||||
var isBuiltIn, withPrefix bool
|
|
||||||
if ldr == nil {
|
|
||||||
if r.r.builtin != nil {
|
|
||||||
ldr = r.r.builtin[path]
|
|
||||||
}
|
|
||||||
if ldr == nil && strings.HasPrefix(path, NodePrefix) {
|
|
||||||
ldr = r.r.builtin[path[len(NodePrefix):]]
|
|
||||||
if ldr == nil {
|
|
||||||
return nil, NoSuchBuiltInModuleError
|
|
||||||
}
|
|
||||||
withPrefix = true
|
|
||||||
}
|
|
||||||
isBuiltIn = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ldr != nil {
|
|
||||||
module = r.createModuleObject()
|
|
||||||
r.modules[path] = module
|
|
||||||
if isBuiltIn {
|
|
||||||
if withPrefix {
|
|
||||||
r.modules[path[len(NodePrefix):]] = module
|
|
||||||
} else {
|
|
||||||
if !strings.HasPrefix(path, NodePrefix) {
|
|
||||||
r.modules[NodePrefix+path] = module
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ldr(r.runtime, module)
|
|
||||||
return module, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, InvalidModuleError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) {
|
|
||||||
if module, err = r.loadAsFile(path); module != nil || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.loadAsDirectory(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) {
|
|
||||||
if module, err = r.loadModule(path); module != nil || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p := path + ".js"
|
|
||||||
if module, err = r.loadModule(p); module != nil || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p = path + ".json"
|
|
||||||
return r.loadModule(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) {
|
|
||||||
p := path.Join(modpath, "index.js")
|
|
||||||
if module, err = r.loadModule(p); module != nil || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p = path.Join(modpath, "index.json")
|
|
||||||
return r.loadModule(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) {
|
|
||||||
p := path.Join(modpath, "package.json")
|
|
||||||
buf, err := r.r.getSource(p)
|
|
||||||
if err != nil {
|
|
||||||
return r.loadIndex(modpath)
|
|
||||||
}
|
|
||||||
var pkg struct {
|
|
||||||
Main string
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(buf, &pkg)
|
|
||||||
if err != nil || len(pkg.Main) == 0 {
|
|
||||||
return r.loadIndex(modpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := path.Join(modpath, pkg.Main)
|
|
||||||
if module, err = r.loadAsFile(m); module != nil || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.loadIndex(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) {
|
|
||||||
return r.loadAsFileOrDirectory(path.Join(start, modpath))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) {
|
|
||||||
for _, dir := range r.r.globalFolders {
|
|
||||||
if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
var p string
|
|
||||||
if path.Base(start) != "node_modules" {
|
|
||||||
p = path.Join(start, "node_modules")
|
|
||||||
} else {
|
|
||||||
p = start
|
|
||||||
}
|
|
||||||
if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if start == ".." { // Dir('..') is '.'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
parent := path.Dir(start)
|
|
||||||
if parent == start {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
start = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) getCurrentModulePath() string {
|
|
||||||
var buf [2]js.StackFrame
|
|
||||||
frames := r.runtime.CaptureCallStack(2, buf[:0])
|
|
||||||
if len(frames) < 2 {
|
|
||||||
return "."
|
|
||||||
}
|
|
||||||
return path.Dir(frames[1].SrcName())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) createModuleObject() *js.Object {
|
|
||||||
module := r.runtime.NewObject()
|
|
||||||
module.Set("exports", r.runtime.NewObject())
|
|
||||||
return module
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadModule(path string) (*js.Object, error) {
|
|
||||||
module := r.modules[path]
|
|
||||||
if module == nil {
|
|
||||||
module = r.createModuleObject()
|
|
||||||
r.modules[path] = module
|
|
||||||
err := r.loadModuleFile(path, module)
|
|
||||||
if err != nil {
|
|
||||||
module = nil
|
|
||||||
delete(r.modules, path)
|
|
||||||
if errors.Is(err, ModuleFileDoesNotExistError) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return module, err
|
|
||||||
}
|
|
||||||
return module, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error {
|
|
||||||
prg, err := r.r.getCompiledSource(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := r.runtime.RunProgram(prg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if call, ok := js.AssertFunction(f); ok {
|
|
||||||
jsExports := jsModule.Get("exports")
|
|
||||||
jsRequire := r.runtime.Get("require")
|
|
||||||
|
|
||||||
// Run the module source, with "jsExports" as "this",
|
|
||||||
// "jsExports" as the "exports" variable, "jsRequire"
|
|
||||||
// as the "require" variable and "jsModule" as the
|
|
||||||
// "module" variable (Nodejs capable).
|
|
||||||
_, err = call(jsExports, jsExports, jsRequire, jsModule)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return InvalidModuleError
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isFileOrDirectoryPath(path string) bool {
|
|
||||||
result := path == "." || path == ".." ||
|
|
||||||
strings.HasPrefix(path, "/") ||
|
|
||||||
strings.HasPrefix(path, "./") ||
|
|
||||||
strings.HasPrefix(path, "../")
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
result = result ||
|
|
||||||
strings.HasPrefix(path, `.\`) ||
|
|
||||||
strings.HasPrefix(path, `..\`) ||
|
|
||||||
filepath.IsAbs(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package sgnotification
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SurgeNotification struct {
|
|
||||||
vm *goja.Runtime
|
|
||||||
logger logger.Logger
|
|
||||||
platformInterface platform.Interface
|
|
||||||
scriptTag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Enable(vm *goja.Runtime, ctx context.Context, logger logger.Logger) {
|
|
||||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
|
||||||
notification := &SurgeNotification{
|
|
||||||
vm: vm,
|
|
||||||
logger: logger,
|
|
||||||
platformInterface: platformInterface,
|
|
||||||
}
|
|
||||||
notificationObject := vm.NewObject()
|
|
||||||
notificationObject.Set("post", notification.js_post)
|
|
||||||
vm.Set("$notification", notificationObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SurgeNotification) js_post(call goja.FunctionCall) goja.Value {
|
|
||||||
var (
|
|
||||||
title string
|
|
||||||
subtitle string
|
|
||||||
body string
|
|
||||||
openURL string
|
|
||||||
clipboard string
|
|
||||||
mediaURL string
|
|
||||||
mediaData []byte
|
|
||||||
mediaType string
|
|
||||||
autoDismiss int
|
|
||||||
)
|
|
||||||
title = jsc.AssertString(s.vm, call.Argument(0), "title", true)
|
|
||||||
subtitle = jsc.AssertString(s.vm, call.Argument(1), "subtitle", true)
|
|
||||||
body = jsc.AssertString(s.vm, call.Argument(2), "body", true)
|
|
||||||
options := jsc.AssertObject(s.vm, call.Argument(3), "options", true)
|
|
||||||
if options != nil {
|
|
||||||
action := jsc.AssertString(s.vm, options.Get("action"), "options.action", true)
|
|
||||||
switch action {
|
|
||||||
case "open-url":
|
|
||||||
openURL = jsc.AssertString(s.vm, options.Get("url"), "options.url", false)
|
|
||||||
case "clipboard":
|
|
||||||
clipboard = jsc.AssertString(s.vm, options.Get("clipboard"), "options.clipboard", false)
|
|
||||||
}
|
|
||||||
mediaURL = jsc.AssertString(s.vm, options.Get("media-url"), "options.media-url", true)
|
|
||||||
mediaBase64 := jsc.AssertString(s.vm, options.Get("media-base64"), "options.media-base64", true)
|
|
||||||
if mediaBase64 != "" {
|
|
||||||
mediaBinary, err := base64.StdEncoding.DecodeString(mediaBase64)
|
|
||||||
if err != nil {
|
|
||||||
panic(s.vm.NewGoError(E.Cause(err, "decode media-base64")))
|
|
||||||
}
|
|
||||||
mediaData = mediaBinary
|
|
||||||
mediaType = jsc.AssertString(s.vm, options.Get("media-base64-mime"), "options.media-base64-mime", false)
|
|
||||||
}
|
|
||||||
autoDismiss = int(jsc.AssertInt(s.vm, options.Get("auto-dismiss"), "options.auto-dismiss", true))
|
|
||||||
}
|
|
||||||
if title != "" && subtitle == "" && body == "" {
|
|
||||||
body = title
|
|
||||||
title = ""
|
|
||||||
} else if title != "" && subtitle != "" && body == "" {
|
|
||||||
body = subtitle
|
|
||||||
subtitle = ""
|
|
||||||
}
|
|
||||||
var builder strings.Builder
|
|
||||||
if title != "" {
|
|
||||||
builder.WriteString("[")
|
|
||||||
builder.WriteString(title)
|
|
||||||
if subtitle != "" {
|
|
||||||
builder.WriteString(" - ")
|
|
||||||
builder.WriteString(subtitle)
|
|
||||||
}
|
|
||||||
builder.WriteString("]: ")
|
|
||||||
}
|
|
||||||
builder.WriteString(body)
|
|
||||||
s.logger.Info("notification: " + builder.String())
|
|
||||||
if s.platformInterface != nil {
|
|
||||||
err := s.platformInterface.SendNotification(&platform.Notification{
|
|
||||||
Identifier: "surge-script-notification-" + s.scriptTag,
|
|
||||||
TypeName: "Surge Script Notification (" + s.scriptTag + ")",
|
|
||||||
TypeID: 11,
|
|
||||||
Title: title,
|
|
||||||
Subtitle: subtitle,
|
|
||||||
Body: body,
|
|
||||||
OpenURL: openURL,
|
|
||||||
Clipboard: clipboard,
|
|
||||||
MediaURL: mediaURL,
|
|
||||||
MediaData: mediaData,
|
|
||||||
MediaType: mediaType,
|
|
||||||
Timeout: autoDismiss,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error(E.Cause(err, "send notification"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package surge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/experimental/locale"
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Environment struct {
|
|
||||||
class jsc.Class[*Module, *Environment]
|
|
||||||
}
|
|
||||||
|
|
||||||
func createEnvironment(module *Module) jsc.Class[*Module, *Environment] {
|
|
||||||
class := jsc.NewClass[*Module, *Environment](module)
|
|
||||||
class.DefineField("system", (*Environment).getSystem, nil)
|
|
||||||
class.DefineField("surge-build", (*Environment).getSurgeBuild, nil)
|
|
||||||
class.DefineField("surge-version", (*Environment).getSurgeVersion, nil)
|
|
||||||
class.DefineField("language", (*Environment).getLanguage, nil)
|
|
||||||
class.DefineField("device-model", (*Environment).getDeviceModel, nil)
|
|
||||||
class.DefineMethod("toString", (*Environment).toString)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) getSystem() any {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "ios":
|
|
||||||
return "iOS"
|
|
||||||
case "darwin":
|
|
||||||
return "macOS"
|
|
||||||
case "tvos":
|
|
||||||
return "tvOS"
|
|
||||||
case "linux":
|
|
||||||
return "Linux"
|
|
||||||
case "android":
|
|
||||||
return "Android"
|
|
||||||
case "windows":
|
|
||||||
return "Windows"
|
|
||||||
default:
|
|
||||||
return runtime.GOOS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) getSurgeBuild() any {
|
|
||||||
return "N/A"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) getSurgeVersion() any {
|
|
||||||
return "sing-box " + C.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) getLanguage() any {
|
|
||||||
return locale.Current().Locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) getDeviceModel() any {
|
|
||||||
return "N/A"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) toString(call goja.FunctionCall) any {
|
|
||||||
return "[sing-box Surge environment"
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package surge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/boxctx"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"golang.org/x/net/publicsuffix"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HTTP struct {
|
|
||||||
class jsc.Class[*Module, *HTTP]
|
|
||||||
cookieJar *cookiejar.Jar
|
|
||||||
httpTransport *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func createHTTP(module *Module) jsc.Class[*Module, *HTTP] {
|
|
||||||
class := jsc.NewClass[*Module, *HTTP](module)
|
|
||||||
class.DefineConstructor(newHTTP)
|
|
||||||
class.DefineMethod("get", httpRequest(http.MethodGet))
|
|
||||||
class.DefineMethod("post", httpRequest(http.MethodPost))
|
|
||||||
class.DefineMethod("put", httpRequest(http.MethodPut))
|
|
||||||
class.DefineMethod("delete", httpRequest(http.MethodDelete))
|
|
||||||
class.DefineMethod("head", httpRequest(http.MethodHead))
|
|
||||||
class.DefineMethod("options", httpRequest(http.MethodOptions))
|
|
||||||
class.DefineMethod("patch", httpRequest(http.MethodPatch))
|
|
||||||
class.DefineMethod("trace", httpRequest(http.MethodTrace))
|
|
||||||
class.DefineMethod("toString", (*HTTP).toString)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP(class jsc.Class[*Module, *HTTP], call goja.ConstructorCall) *HTTP {
|
|
||||||
return &HTTP{
|
|
||||||
class: class,
|
|
||||||
cookieJar: common.Must1(cookiejar.New(&cookiejar.Options{
|
|
||||||
PublicSuffixList: publicsuffix.List,
|
|
||||||
})),
|
|
||||||
httpTransport: &http.Transport{
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
TLSClientConfig: &tls.Config{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpRequest(method string) func(s *HTTP, call goja.FunctionCall) any {
|
|
||||||
return func(s *HTTP, call goja.FunctionCall) any {
|
|
||||||
if len(call.Arguments) != 2 {
|
|
||||||
panic(s.class.Runtime().NewTypeError("invalid arguments"))
|
|
||||||
}
|
|
||||||
context := boxctx.MustFromRuntime(s.class.Runtime())
|
|
||||||
var (
|
|
||||||
url string
|
|
||||||
headers http.Header
|
|
||||||
body []byte
|
|
||||||
timeout = 5 * time.Second
|
|
||||||
insecure bool
|
|
||||||
autoCookie bool = true
|
|
||||||
autoRedirect bool
|
|
||||||
// policy string
|
|
||||||
binaryMode bool
|
|
||||||
)
|
|
||||||
switch optionsValue := call.Argument(0).(type) {
|
|
||||||
case goja.String:
|
|
||||||
url = optionsValue.String()
|
|
||||||
case *goja.Object:
|
|
||||||
url = jsc.AssertString(s.class.Runtime(), optionsValue.Get("url"), "options.url", false)
|
|
||||||
headers = jsc.AssertHTTPHeader(s.class.Runtime(), optionsValue.Get("headers"), "option.headers")
|
|
||||||
body = jsc.AssertStringBinary(s.class.Runtime(), optionsValue.Get("body"), "options.body", true)
|
|
||||||
timeoutInt := jsc.AssertInt(s.class.Runtime(), optionsValue.Get("timeout"), "options.timeout", true)
|
|
||||||
if timeoutInt > 0 {
|
|
||||||
timeout = time.Duration(timeoutInt) * time.Second
|
|
||||||
}
|
|
||||||
insecure = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("insecure"), "options.insecure", true)
|
|
||||||
autoCookie = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("auto-cookie"), "options.auto-cookie", true)
|
|
||||||
autoRedirect = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("auto-redirect"), "options.auto-redirect", true)
|
|
||||||
// policy = jsc.AssertString(s.class.Runtime(), optionsValue.Get("policy"), "options.policy", true)
|
|
||||||
binaryMode = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("binary-mode"), "options.binary-mode", true)
|
|
||||||
default:
|
|
||||||
panic(s.class.Runtime().NewTypeError(F.ToString("invalid argument: options: expected string or object, but got ", optionsValue)))
|
|
||||||
}
|
|
||||||
callback := jsc.AssertFunction(s.class.Runtime(), call.Argument(1), "callback")
|
|
||||||
s.httpTransport.TLSClientConfig.InsecureSkipVerify = insecure
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Timeout: timeout,
|
|
||||||
Transport: s.httpTransport,
|
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
||||||
if autoRedirect {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return http.ErrUseLastResponse
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if autoCookie {
|
|
||||||
httpClient.Jar = s.cookieJar
|
|
||||||
}
|
|
||||||
request, err := http.NewRequestWithContext(context.Context, method, url, bytes.NewReader(body))
|
|
||||||
if host := headers.Get("Host"); host != "" {
|
|
||||||
request.Host = host
|
|
||||||
headers.Del("Host")
|
|
||||||
}
|
|
||||||
request.Header = headers
|
|
||||||
if err != nil {
|
|
||||||
panic(s.class.Runtime().NewGoError(err))
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
defer s.httpTransport.CloseIdleConnections()
|
|
||||||
response, executeErr := httpClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
_, err = callback(nil, s.class.Runtime().NewGoError(executeErr), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
context.ErrorHandler(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
var content []byte
|
|
||||||
content, err = io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
_, err = callback(nil, s.class.Runtime().NewGoError(err), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
context.ErrorHandler(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
responseObject := s.class.Runtime().NewObject()
|
|
||||||
responseObject.Set("status", response.StatusCode)
|
|
||||||
responseObject.Set("headers", jsc.HeadersToValue(s.class.Runtime(), response.Header))
|
|
||||||
var bodyValue goja.Value
|
|
||||||
if binaryMode {
|
|
||||||
bodyValue = jsc.NewUint8Array(s.class.Runtime(), content)
|
|
||||||
} else {
|
|
||||||
bodyValue = s.class.Runtime().ToValue(string(content))
|
|
||||||
}
|
|
||||||
_, err = callback(nil, nil, responseObject, bodyValue)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HTTP) toString(call goja.FunctionCall) any {
|
|
||||||
return "[sing-box Surge HTTP]"
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package surge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/require"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ModuleName = "surge"
|
|
||||||
|
|
||||||
type Module struct {
|
|
||||||
runtime *goja.Runtime
|
|
||||||
classScript jsc.Class[*Module, *Script]
|
|
||||||
classEnvironment jsc.Class[*Module, *Environment]
|
|
||||||
classPersistentStore jsc.Class[*Module, *PersistentStore]
|
|
||||||
classHTTP jsc.Class[*Module, *HTTP]
|
|
||||||
classUtils jsc.Class[*Module, *Utils]
|
|
||||||
classNotification jsc.Class[*Module, *Notification]
|
|
||||||
}
|
|
||||||
|
|
||||||
func Require(runtime *goja.Runtime, module *goja.Object) {
|
|
||||||
m := &Module{
|
|
||||||
runtime: runtime,
|
|
||||||
}
|
|
||||||
m.classScript = createScript(m)
|
|
||||||
m.classEnvironment = createEnvironment(m)
|
|
||||||
m.classPersistentStore = createPersistentStore(m)
|
|
||||||
m.classHTTP = createHTTP(m)
|
|
||||||
m.classUtils = createUtils(m)
|
|
||||||
m.classNotification = createNotification(m)
|
|
||||||
exports := module.Get("exports").(*goja.Object)
|
|
||||||
exports.Set("Script", m.classScript.ToValue())
|
|
||||||
exports.Set("Environment", m.classEnvironment.ToValue())
|
|
||||||
exports.Set("PersistentStore", m.classPersistentStore.ToValue())
|
|
||||||
exports.Set("HTTP", m.classHTTP.ToValue())
|
|
||||||
exports.Set("Utils", m.classUtils.ToValue())
|
|
||||||
exports.Set("Notification", m.classNotification.ToValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Enable(runtime *goja.Runtime, scriptType string, args []string) {
|
|
||||||
exports := require.Require(runtime, ModuleName).ToObject(runtime)
|
|
||||||
classScript := jsc.GetClass[*Module, *Script](runtime, exports, "Script")
|
|
||||||
classEnvironment := jsc.GetClass[*Module, *Environment](runtime, exports, "Environment")
|
|
||||||
classPersistentStore := jsc.GetClass[*Module, *PersistentStore](runtime, exports, "PersistentStore")
|
|
||||||
classHTTP := jsc.GetClass[*Module, *HTTP](runtime, exports, "HTTP")
|
|
||||||
classUtils := jsc.GetClass[*Module, *Utils](runtime, exports, "Utils")
|
|
||||||
classNotification := jsc.GetClass[*Module, *Notification](runtime, exports, "Notification")
|
|
||||||
runtime.Set("$script", classScript.New(&Script{class: classScript, ScriptType: scriptType}))
|
|
||||||
runtime.Set("$environment", classEnvironment.New(&Environment{class: classEnvironment}))
|
|
||||||
runtime.Set("$persistentStore", newPersistentStore(classPersistentStore))
|
|
||||||
runtime.Set("$http", classHTTP.New(newHTTP(classHTTP, goja.ConstructorCall{})))
|
|
||||||
runtime.Set("$utils", classUtils.New(&Utils{class: classUtils}))
|
|
||||||
runtime.Set("$notification", newNotification(classNotification))
|
|
||||||
runtime.Set("$argument", runtime.NewArray(common.Map(args, func(it string) any {
|
|
||||||
return it
|
|
||||||
})...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) Runtime() *goja.Runtime {
|
|
||||||
return m.runtime
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package surge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/boxctx"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Notification struct {
|
|
||||||
class jsc.Class[*Module, *Notification]
|
|
||||||
logger logger.ContextLogger
|
|
||||||
tag string
|
|
||||||
platformInterface platform.Interface
|
|
||||||
}
|
|
||||||
|
|
||||||
func createNotification(module *Module) jsc.Class[*Module, *Notification] {
|
|
||||||
class := jsc.NewClass[*Module, *Notification](module)
|
|
||||||
class.DefineMethod("post", (*Notification).post)
|
|
||||||
class.DefineMethod("toString", (*Notification).toString)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNotification(class jsc.Class[*Module, *Notification]) goja.Value {
|
|
||||||
context := boxctx.MustFromRuntime(class.Runtime())
|
|
||||||
return class.New(&Notification{
|
|
||||||
class: class,
|
|
||||||
logger: context.Logger,
|
|
||||||
tag: context.Tag,
|
|
||||||
platformInterface: service.FromContext[platform.Interface](context.Context),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Notification) post(call goja.FunctionCall) any {
|
|
||||||
var (
|
|
||||||
title string
|
|
||||||
subtitle string
|
|
||||||
body string
|
|
||||||
openURL string
|
|
||||||
clipboard string
|
|
||||||
mediaURL string
|
|
||||||
mediaData []byte
|
|
||||||
mediaType string
|
|
||||||
autoDismiss int
|
|
||||||
)
|
|
||||||
title = jsc.AssertString(s.class.Runtime(), call.Argument(0), "title", true)
|
|
||||||
subtitle = jsc.AssertString(s.class.Runtime(), call.Argument(1), "subtitle", true)
|
|
||||||
body = jsc.AssertString(s.class.Runtime(), call.Argument(2), "body", true)
|
|
||||||
options := jsc.AssertObject(s.class.Runtime(), call.Argument(3), "options", true)
|
|
||||||
if options != nil {
|
|
||||||
action := jsc.AssertString(s.class.Runtime(), options.Get("action"), "options.action", true)
|
|
||||||
switch action {
|
|
||||||
case "open-url":
|
|
||||||
openURL = jsc.AssertString(s.class.Runtime(), options.Get("url"), "options.url", false)
|
|
||||||
case "clipboard":
|
|
||||||
clipboard = jsc.AssertString(s.class.Runtime(), options.Get("clipboard"), "options.clipboard", false)
|
|
||||||
}
|
|
||||||
mediaURL = jsc.AssertString(s.class.Runtime(), options.Get("media-url"), "options.media-url", true)
|
|
||||||
mediaBase64 := jsc.AssertString(s.class.Runtime(), options.Get("media-base64"), "options.media-base64", true)
|
|
||||||
if mediaBase64 != "" {
|
|
||||||
mediaBinary, err := base64.StdEncoding.DecodeString(mediaBase64)
|
|
||||||
if err != nil {
|
|
||||||
panic(s.class.Runtime().NewGoError(E.Cause(err, "decode media-base64")))
|
|
||||||
}
|
|
||||||
mediaData = mediaBinary
|
|
||||||
mediaType = jsc.AssertString(s.class.Runtime(), options.Get("media-base64-mime"), "options.media-base64-mime", false)
|
|
||||||
}
|
|
||||||
autoDismiss = int(jsc.AssertInt(s.class.Runtime(), options.Get("auto-dismiss"), "options.auto-dismiss", true))
|
|
||||||
}
|
|
||||||
if title != "" && subtitle == "" && body == "" {
|
|
||||||
body = title
|
|
||||||
title = ""
|
|
||||||
} else if title != "" && subtitle != "" && body == "" {
|
|
||||||
body = subtitle
|
|
||||||
subtitle = ""
|
|
||||||
}
|
|
||||||
var builder strings.Builder
|
|
||||||
if title != "" {
|
|
||||||
builder.WriteString("[")
|
|
||||||
builder.WriteString(title)
|
|
||||||
if subtitle != "" {
|
|
||||||
builder.WriteString(" - ")
|
|
||||||
builder.WriteString(subtitle)
|
|
||||||
}
|
|
||||||
builder.WriteString("]: ")
|
|
||||||
}
|
|
||||||
builder.WriteString(body)
|
|
||||||
s.logger.Info("notification: " + builder.String())
|
|
||||||
if s.platformInterface != nil {
|
|
||||||
err := s.platformInterface.SendNotification(&platform.Notification{
|
|
||||||
Identifier: "surge-script-notification-" + s.tag,
|
|
||||||
TypeName: "Surge Script Notification (" + s.tag + ")",
|
|
||||||
TypeID: 11,
|
|
||||||
Title: title,
|
|
||||||
Subtitle: subtitle,
|
|
||||||
Body: body,
|
|
||||||
OpenURL: openURL,
|
|
||||||
Clipboard: clipboard,
|
|
||||||
MediaURL: mediaURL,
|
|
||||||
MediaData: mediaData,
|
|
||||||
MediaType: mediaType,
|
|
||||||
Timeout: autoDismiss,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error(E.Cause(err, "send notification"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Notification) toString(call goja.FunctionCall) any {
|
|
||||||
return "[sing-box Surge notification]"
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package surge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/boxctx"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PersistentStore struct {
|
|
||||||
class jsc.Class[*Module, *PersistentStore]
|
|
||||||
cacheFile adapter.CacheFile
|
|
||||||
inMemoryCache *adapter.SurgeInMemoryCache
|
|
||||||
tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPersistentStore(module *Module) jsc.Class[*Module, *PersistentStore] {
|
|
||||||
class := jsc.NewClass[*Module, *PersistentStore](module)
|
|
||||||
class.DefineMethod("get", (*PersistentStore).get)
|
|
||||||
class.DefineMethod("set", (*PersistentStore).set)
|
|
||||||
class.DefineMethod("toString", (*PersistentStore).toString)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPersistentStore(class jsc.Class[*Module, *PersistentStore]) goja.Value {
|
|
||||||
boxCtx := boxctx.MustFromRuntime(class.Runtime())
|
|
||||||
return class.New(&PersistentStore{
|
|
||||||
class: class,
|
|
||||||
cacheFile: service.FromContext[adapter.CacheFile](boxCtx.Context),
|
|
||||||
inMemoryCache: service.FromContext[adapter.ScriptManager](boxCtx.Context).SurgeCache(),
|
|
||||||
tag: boxCtx.Tag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersistentStore) get(call goja.FunctionCall) any {
|
|
||||||
key := jsc.AssertString(s.class.Runtime(), call.Argument(0), "key", true)
|
|
||||||
if key == "" {
|
|
||||||
key = s.tag
|
|
||||||
}
|
|
||||||
var value string
|
|
||||||
if s.cacheFile != nil {
|
|
||||||
value = s.cacheFile.SurgePersistentStoreRead(key)
|
|
||||||
} else {
|
|
||||||
s.inMemoryCache.RLock()
|
|
||||||
value = s.inMemoryCache.Data[key]
|
|
||||||
s.inMemoryCache.RUnlock()
|
|
||||||
}
|
|
||||||
if value == "" {
|
|
||||||
return goja.Null()
|
|
||||||
} else {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersistentStore) set(call goja.FunctionCall) any {
|
|
||||||
data := jsc.AssertString(s.class.Runtime(), call.Argument(0), "data", true)
|
|
||||||
key := jsc.AssertString(s.class.Runtime(), call.Argument(1), "key", true)
|
|
||||||
if key == "" {
|
|
||||||
key = s.tag
|
|
||||||
}
|
|
||||||
if s.cacheFile != nil {
|
|
||||||
err := s.cacheFile.SurgePersistentStoreWrite(key, data)
|
|
||||||
if err != nil {
|
|
||||||
panic(s.class.Runtime().NewGoError(err))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.inMemoryCache.Lock()
|
|
||||||
s.inMemoryCache.Data[key] = data
|
|
||||||
s.inMemoryCache.Unlock()
|
|
||||||
}
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersistentStore) toString(call goja.FunctionCall) any {
|
|
||||||
return "[sing-box Surge persistentStore]"
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package surge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
"github.com/sagernet/sing-box/script/modules/boxctx"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Script struct {
|
|
||||||
class jsc.Class[*Module, *Script]
|
|
||||||
ScriptType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func createScript(module *Module) jsc.Class[*Module, *Script] {
|
|
||||||
class := jsc.NewClass[*Module, *Script](module)
|
|
||||||
class.DefineField("name", (*Script).getName, nil)
|
|
||||||
class.DefineField("type", (*Script).getType, nil)
|
|
||||||
class.DefineField("startTime", (*Script).getStartTime, nil)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) getName() any {
|
|
||||||
return F.ToString("script:", boxctx.MustFromRuntime(s.class.Runtime()).Tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) getType() any {
|
|
||||||
return s.ScriptType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) getStartTime() any {
|
|
||||||
return boxctx.MustFromRuntime(s.class.Runtime()).StartedAt
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package surge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/script/jsc"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Utils struct {
|
|
||||||
class jsc.Class[*Module, *Utils]
|
|
||||||
}
|
|
||||||
|
|
||||||
func createUtils(module *Module) jsc.Class[*Module, *Utils] {
|
|
||||||
class := jsc.NewClass[*Module, *Utils](module)
|
|
||||||
class.DefineMethod("geoip", (*Utils).stub)
|
|
||||||
class.DefineMethod("ipasn", (*Utils).stub)
|
|
||||||
class.DefineMethod("ipaso", (*Utils).stub)
|
|
||||||
class.DefineMethod("ungzip", (*Utils).ungzip)
|
|
||||||
class.DefineMethod("toString", (*Utils).toString)
|
|
||||||
return class
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Utils) stub(call goja.FunctionCall) any {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Utils) ungzip(call goja.FunctionCall) any {
|
|
||||||
if len(call.Arguments) != 1 {
|
|
||||||
panic(u.class.Runtime().NewGoError(E.New("invalid argument")))
|
|
||||||
}
|
|
||||||
binary := jsc.AssertBinary(u.class.Runtime(), call.Argument(0), "binary", false)
|
|
||||||
reader, err := gzip.NewReader(bytes.NewReader(binary))
|
|
||||||
if err != nil {
|
|
||||||
panic(u.class.Runtime().NewGoError(err))
|
|
||||||
}
|
|
||||||
binary, err = io.ReadAll(reader)
|
|
||||||
if err != nil {
|
|
||||||
panic(u.class.Runtime().NewGoError(err))
|
|
||||||
}
|
|
||||||
return jsc.NewUint8Array(u.class.Runtime(), binary)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Utils) toString(call goja.FunctionCall) any {
|
|
||||||
return "[sing-box Surge utils]"
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package url
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
var tblEscapeURLQuery = [128]byte{
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// The code below is mostly borrowed from the standard Go url package
|
|
||||||
|
|
||||||
const upperhex = "0123456789ABCDEF"
|
|
||||||
|
|
||||||
func escape(s string, table *[128]byte, spaceToPlus bool) string {
|
|
||||||
spaceCount, hexCount := 0, 0
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
c := s[i]
|
|
||||||
if c > 127 || table[c] == 0 {
|
|
||||||
if c == ' ' && spaceToPlus {
|
|
||||||
spaceCount++
|
|
||||||
} else {
|
|
||||||
hexCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if spaceCount == 0 && hexCount == 0 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
hexBuf := [3]byte{'%', 0, 0}
|
|
||||||
|
|
||||||
sb.Grow(len(s) + 2*hexCount)
|
|
||||||
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
switch c := s[i]; {
|
|
||||||
case c == ' ' && spaceToPlus:
|
|
||||||
sb.WriteByte('+')
|
|
||||||
case c > 127 || table[c] == 0:
|
|
||||||
hexBuf[1] = upperhex[c>>4]
|
|
||||||
hexBuf[2] = upperhex[c&15]
|
|
||||||
sb.Write(hexBuf[:])
|
|
||||||
default:
|
|
||||||
sb.WriteByte(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user