mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-07 20:16:53 +03:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
984ae5f2a9 | ||
|
|
7a62898541 | ||
|
|
7911d1d29f | ||
|
|
bc673b7881 | ||
|
|
0493565c5f | ||
|
|
4cd1094395 | ||
|
|
e87b431d86 | ||
|
|
b9ee917abf | ||
|
|
715a278af8 | ||
|
|
9bc2b5ffef | ||
|
|
9d89258c0c | ||
|
|
52d1c5d95f | ||
|
|
587e5245d3 | ||
|
|
e7578d61bc | ||
|
|
9918b71a82 | ||
|
|
f48c4ff2bb | ||
|
|
e77bcc386a | ||
|
|
455c19ab2e | ||
|
|
914e1792f3 | ||
|
|
826245a89a | ||
|
|
b5cfc017fe | ||
|
|
267fd2b793 | ||
|
|
c0b400dfb0 | ||
|
|
752636347e | ||
|
|
28aeb29c51 | ||
|
|
6ff543d7fb | ||
|
|
b89fe33296 | ||
|
|
3d63a82815 | ||
|
|
934f802879 | ||
|
|
4d0755e4c0 | ||
|
|
88ee7b4a54 | ||
|
|
0eb575d171 | ||
|
|
9a46d731c9 | ||
|
|
a45ab62885 | ||
|
|
b7bad57299 | ||
|
|
4ac755bd36 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
123
.github/workflows/build.yml
vendored
123
.github/workflows/build.yml
vendored
@@ -2,53 +2,118 @@ name: Build packages
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- '*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
preparation:
|
||||||
name: Build podkop and luci-app-podkop
|
name: Setup build version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.2.1
|
- uses: actions/checkout@v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "0.$(date +%d%m%Y)")
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Builder for ${{ matrix.package_type }} podkop and luci-app-podkop
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: preparation
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { package_type: ipk }
|
||||||
|
- { package_type: apk }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Extract version
|
- name: Build ${{ matrix.package_type }}
|
||||||
id: version
|
uses: docker/build-push-action@v6.18.0
|
||||||
run: |
|
|
||||||
VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "dev_$(date +%d%m%Y)")
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v6.9.0
|
|
||||||
with:
|
with:
|
||||||
|
file: ./Dockerfile-${{ matrix.package_type }}
|
||||||
context: .
|
context: .
|
||||||
tags: podkop:ci
|
tags: podkop:ci-${{ matrix.package_type }}
|
||||||
build-args: |
|
build-args: |
|
||||||
PKG_VERSION=${{ steps.version.outputs.version }}
|
PODKOP_VERSION=${{ needs.preparation.outputs.version }}
|
||||||
|
|
||||||
- name: Create Docker container
|
- name: Create ${{ matrix.package_type }} Docker container
|
||||||
run: docker create --name podkop podkop:ci
|
run: docker create --name ${{ matrix.package_type }} podkop:ci-${{ matrix.package_type }}
|
||||||
|
|
||||||
- name: Copy file from Docker container
|
- name: Copy files from ${{ matrix.package_type }} Docker container
|
||||||
run: |
|
run: |
|
||||||
docker cp podkop:/builder/bin/packages/x86_64/utilites/. ./bin/
|
mkdir -p ./bin/${{ matrix.package_type }}
|
||||||
docker cp podkop:/builder/bin/packages/x86_64/luci/. ./bin/
|
docker cp ${{ matrix.package_type }}:/builder/bin/packages/x86_64/utilities/. ./bin/${{ matrix.package_type }}/
|
||||||
|
docker cp ${{ matrix.package_type }}:/builder/bin/packages/x86_64/luci/. ./bin/${{ matrix.package_type }}/
|
||||||
|
|
||||||
- name: Filter IPK files
|
# IPK uses underscore `_` in filenames, while APK uses only dash `-`
|
||||||
|
- name: Fix naming difference between build for packages (replace _ with -)
|
||||||
|
if: matrix.package_type == 'ipk'
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# Извлекаем версию из тега, убирая префикс 'v'
|
for f in ./bin/${{ matrix.package_type }}/*.${{ matrix.package_type }}; do
|
||||||
VERSION=${GITHUB_REF#refs/tags/v}
|
[ -e "$f" ] || continue
|
||||||
|
base=$(basename "$f")
|
||||||
|
newname=$(echo "$base" | sed 's/_/-/g')
|
||||||
|
mv "$f" "./bin/${{ matrix.package_type }}/$newname"
|
||||||
|
done
|
||||||
|
|
||||||
mkdir -p ./filtered-bin
|
- name: Filter files
|
||||||
cp ./bin/luci-i18n-podkop-ru_*.ipk "./filtered-bin/luci-i18n-podkop-ru_${VERSION}.ipk"
|
shell: bash
|
||||||
cp ./bin/podkop_*.ipk ./filtered-bin/
|
run: |
|
||||||
cp ./bin/luci-app-podkop_*.ipk ./filtered-bin/
|
# Use version from preparation job (already without 'v' prefix)
|
||||||
|
VERSION="${{ needs.preparation.outputs.version }}"
|
||||||
|
|
||||||
|
mkdir -p ./filtered-bin/${{ matrix.package_type }}
|
||||||
|
cp ./bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-*.${{ matrix.package_type }} "./filtered-bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-${VERSION}.${{ matrix.package_type }}"
|
||||||
|
cp ./bin/${{ matrix.package_type }}/podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/
|
||||||
|
cp ./bin/${{ matrix.package_type }}/luci-app-podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/
|
||||||
|
|
||||||
- name: Remove Docker container
|
- name: Remove Docker container
|
||||||
run: docker rm podkop
|
run: docker rm ${{ matrix.package_type }}
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4.6.2
|
||||||
|
with:
|
||||||
|
name: release-files-${{ github.ref_name }}-${{ matrix.package_type }}
|
||||||
|
path: ./filtered-bin/${{ matrix.package_type }}/*.${{ matrix.package_type }}
|
||||||
|
retention-days: 1
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5.0.0
|
||||||
|
- name: Create release dir
|
||||||
|
run: mkdir -p ./filtered-bin/release
|
||||||
|
|
||||||
|
- name: Download ipk artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-files-${{ github.ref_name }}-ipk
|
||||||
|
path: ./filtered-bin/release
|
||||||
|
|
||||||
|
- name: Download apk artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-files-${{ github.ref_name }}-apk
|
||||||
|
path: ./filtered-bin/release
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2.0.8
|
uses: softprops/action-gh-release@v2.4.0
|
||||||
with:
|
with:
|
||||||
files: ./filtered-bin/*.ipk
|
files: ./filtered-bin/release/*.*
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
FROM itdoginfo/openwrt-sdk:24.10.1
|
|
||||||
|
|
||||||
ARG PKG_VERSION
|
|
||||||
ENV PKG_VERSION=${PKG_VERSION}
|
|
||||||
|
|
||||||
COPY ./podkop /builder/package/feeds/utilites/podkop
|
|
||||||
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop
|
|
||||||
|
|
||||||
RUN make defconfig && make package/podkop/compile && make package/luci-app-podkop/compile V=s -j4
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
FROM openwrt/sdk:x86_64-v24.10.1
|
|
||||||
|
|
||||||
RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/
|
|
||||||
11
Dockerfile-apk
Normal file
11
Dockerfile-apk
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM itdoginfo/openwrt-sdk-apk:09102025
|
||||||
|
|
||||||
|
ARG PODKOP_VERSION
|
||||||
|
ENV PODKOP_VERSION=${PODKOP_VERSION}
|
||||||
|
|
||||||
|
COPY ./podkop /builder/package/feeds/utilities/podkop
|
||||||
|
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop
|
||||||
|
|
||||||
|
RUN make defconfig && \
|
||||||
|
make package/podkop/compile -j1 V=s && \
|
||||||
|
make package/luci-app-podkop/compile -j1 V=s
|
||||||
11
Dockerfile-ipk
Normal file
11
Dockerfile-ipk
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM itdoginfo/openwrt-sdk-ipk:24.10.3
|
||||||
|
|
||||||
|
ARG PODKOP_VERSION
|
||||||
|
|
||||||
|
COPY ./podkop /builder/package/feeds/utilities/podkop
|
||||||
|
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop
|
||||||
|
|
||||||
|
RUN export PODKOP_VERSION="v${PODKOP_VERSION}" && \
|
||||||
|
make defconfig && \
|
||||||
|
make package/podkop/compile V=s -j4 && \
|
||||||
|
make package/luci-app-podkop/compile V=s -j4
|
||||||
@@ -4,6 +4,7 @@ export const STATUS_COLORS = {
|
|||||||
WARNING: '#ff9800',
|
WARNING: '#ff9800',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PODKOP_LUCI_APP_VERSION = '__COMPILED_VERSION_VARIABLE__';
|
||||||
export const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
|
export const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
|
||||||
export const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
|
export const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export function getClashApiUrl(): string {
|
export function getClashApiUrl(): string {
|
||||||
const { protocol, hostname } = window.location;
|
const { hostname } = window.location;
|
||||||
|
|
||||||
return `${protocol}//${hostname}:9090`;
|
return `http://${hostname}:9090`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClashWsUrl(): string {
|
export function getClashWsUrl(): string {
|
||||||
@@ -9,3 +9,9 @@ export function getClashWsUrl(): string {
|
|||||||
|
|
||||||
return `ws://${hostname}:9090`;
|
return `ws://${hostname}:9090`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getClashUIUrl(): string {
|
||||||
|
const { hostname } = window.location;
|
||||||
|
|
||||||
|
return `http://${hostname}:9090/ui`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ export * from './getProxyUrlName';
|
|||||||
export * from './onMount';
|
export * from './onMount';
|
||||||
export * from './getClashApiUrl';
|
export * from './getClashApiUrl';
|
||||||
export * from './splitProxyString';
|
export * from './splitProxyString';
|
||||||
|
export * from './preserveScrollForPage';
|
||||||
|
export * from './parseQueryString';
|
||||||
|
|||||||
22
fe-app-podkop/src/helpers/parseQueryString.ts
Normal file
22
fe-app-podkop/src/helpers/parseQueryString.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export function parseQueryString(query: string): Record<string, string> {
|
||||||
|
const clean = query.startsWith('?') ? query.slice(1) : query;
|
||||||
|
|
||||||
|
return clean
|
||||||
|
.split('&')
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce(
|
||||||
|
(acc, pair) => {
|
||||||
|
const [rawKey, rawValue = ''] = pair.split('=');
|
||||||
|
|
||||||
|
if (!rawKey) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = decodeURIComponent(rawKey);
|
||||||
|
const value = decodeURIComponent(rawValue);
|
||||||
|
|
||||||
|
return { ...acc, [key]: value };
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
}
|
||||||
9
fe-app-podkop/src/helpers/preserveScrollForPage.ts
Normal file
9
fe-app-podkop/src/helpers/preserveScrollForPage.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function preserveScrollForPage(renderFn: () => void) {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
|
||||||
|
renderFn();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo({ top: scrollY });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -61,6 +61,12 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
|||||||
(proxy) => proxy.code === `${section['.name']}-out`,
|
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const parsedOutbound = JSON.parse(section.outbound_json);
|
||||||
|
const parsedTag = parsedOutbound?.tag
|
||||||
|
? decodeURIComponent(parsedOutbound?.tag)
|
||||||
|
: undefined;
|
||||||
|
const proxyDisplayName = parsedTag || outbound?.value?.name || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
withTagSelect: false,
|
withTagSelect: false,
|
||||||
code: outbound?.code || section['.name'],
|
code: outbound?.code || section['.name'],
|
||||||
@@ -68,10 +74,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
|||||||
outbounds: [
|
outbounds: [
|
||||||
{
|
{
|
||||||
code: outbound?.code || section['.name'],
|
code: outbound?.code || section['.name'],
|
||||||
displayName:
|
displayName: proxyDisplayName,
|
||||||
decodeURIComponent(JSON.parse(section.outbound_json)?.tag) ||
|
|
||||||
outbound?.value?.name ||
|
|
||||||
'',
|
|
||||||
latency: outbound?.value?.history?.[0]?.delay || 0,
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||||
type: outbound?.value?.type || '',
|
type: outbound?.value?.type || '',
|
||||||
selected: true,
|
selected: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export async function getPodkopStatus(): Promise<{
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: '/usr/bin/podkop',
|
command: '/usr/bin/podkop',
|
||||||
args: ['get_status'],
|
args: ['get_status'],
|
||||||
timeout: 1000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function getSingboxStatus(): Promise<{
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: '/usr/bin/podkop',
|
command: '/usr/bin/podkop',
|
||||||
args: ['get_sing_box_status'],
|
args: ['get_sing_box_status'],
|
||||||
timeout: 1000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import {
|
|||||||
getPodkopStatus,
|
getPodkopStatus,
|
||||||
getSingboxStatus,
|
getSingboxStatus,
|
||||||
} from '../../methods';
|
} from '../../methods';
|
||||||
import { getClashWsUrl, onMount } from '../../../helpers';
|
import {
|
||||||
|
getClashApiUrl,
|
||||||
|
getClashWsUrl,
|
||||||
|
onMount,
|
||||||
|
preserveScrollForPage,
|
||||||
|
} from '../../../helpers';
|
||||||
import {
|
import {
|
||||||
triggerLatencyGroupTest,
|
triggerLatencyGroupTest,
|
||||||
triggerLatencyProxyTest,
|
triggerLatencyProxyTest,
|
||||||
@@ -29,8 +34,13 @@ async function fetchDashboardSections() {
|
|||||||
|
|
||||||
const { data, success } = await getDashboardSections();
|
const { data, success } = await getDashboardSections();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
console.log('[fetchDashboardSections]: failed to fetch', getClashApiUrl());
|
||||||
|
}
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
sectionsWidget: {
|
sectionsWidget: {
|
||||||
|
latencyFetching: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
failed: !success,
|
failed: !success,
|
||||||
data,
|
data,
|
||||||
@@ -39,18 +49,30 @@ async function fetchDashboardSections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const [podkop, singbox] = await Promise.all([
|
try {
|
||||||
getPodkopStatus(),
|
const [podkop, singbox] = await Promise.all([
|
||||||
getSingboxStatus(),
|
getPodkopStatus(),
|
||||||
]);
|
getSingboxStatus(),
|
||||||
|
]);
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
servicesInfoWidget: {
|
servicesInfoWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
failed: false,
|
failed: false,
|
||||||
data: { singbox: singbox.running, podkop: podkop.enabled },
|
data: { singbox: singbox.running, podkop: podkop.enabled },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log('[fetchServicesInfo]: failed to fetchServices', err);
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { singbox: 0, podkop: 0 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectToClashSockets() {
|
async function connectToClashSockets() {
|
||||||
@@ -68,6 +90,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
'[fetchDashboardSections]: failed to connect',
|
||||||
|
getClashWsUrl(),
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
bandwidthWidget: {
|
bandwidthWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -103,6 +129,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
'[fetchDashboardSections]: failed to connect',
|
||||||
|
getClashWsUrl(),
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
trafficTotalWidget: {
|
trafficTotalWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -130,25 +160,41 @@ async function handleChooseOutbound(selector: string, tag: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleTestGroupLatency(tag: string) {
|
async function handleTestGroupLatency(tag: string) {
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
...store.get().sectionsWidget,
|
||||||
|
latencyFetching: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await triggerLatencyGroupTest(tag);
|
await triggerLatencyGroupTest(tag);
|
||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
...store.get().sectionsWidget,
|
||||||
|
latencyFetching: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTestProxyLatency(tag: string) {
|
async function handleTestProxyLatency(tag: string) {
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
...store.get().sectionsWidget,
|
||||||
|
latencyFetching: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await triggerLatencyProxyTest(tag);
|
await triggerLatencyProxyTest(tag);
|
||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
}
|
|
||||||
|
|
||||||
function replaceTestLatencyButtonsWithSkeleton() {
|
store.set({
|
||||||
document
|
sectionsWidget: {
|
||||||
.querySelectorAll('.dashboard-sections-grid-item-test-latency')
|
...store.get().sectionsWidget,
|
||||||
.forEach((el) => {
|
latencyFetching: false,
|
||||||
const newDiv = document.createElement('div');
|
},
|
||||||
newDiv.className = 'skeleton';
|
});
|
||||||
newDiv.style.width = '99px';
|
|
||||||
newDiv.style.height = '28px';
|
|
||||||
el.replaceWith(newDiv);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderer
|
// Renderer
|
||||||
@@ -170,8 +216,12 @@ async function renderSectionsWidget() {
|
|||||||
},
|
},
|
||||||
onTestLatency: () => {},
|
onTestLatency: () => {},
|
||||||
onChooseOutbound: () => {},
|
onChooseOutbound: () => {},
|
||||||
|
latencyFetching: sectionsWidget.latencyFetching,
|
||||||
|
});
|
||||||
|
|
||||||
|
return preserveScrollForPage(() => {
|
||||||
|
container!.replaceChildren(renderedWidget);
|
||||||
});
|
});
|
||||||
return container!.replaceChildren(renderedWidget);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderedWidgets = sectionsWidget.data.map((section) =>
|
const renderedWidgets = sectionsWidget.data.map((section) =>
|
||||||
@@ -179,9 +229,8 @@ async function renderSectionsWidget() {
|
|||||||
loading: sectionsWidget.loading,
|
loading: sectionsWidget.loading,
|
||||||
failed: sectionsWidget.failed,
|
failed: sectionsWidget.failed,
|
||||||
section,
|
section,
|
||||||
|
latencyFetching: sectionsWidget.latencyFetching,
|
||||||
onTestLatency: (tag) => {
|
onTestLatency: (tag) => {
|
||||||
replaceTestLatencyButtonsWithSkeleton();
|
|
||||||
|
|
||||||
if (section.withTagSelect) {
|
if (section.withTagSelect) {
|
||||||
return handleTestGroupLatency(tag);
|
return handleTestGroupLatency(tag);
|
||||||
}
|
}
|
||||||
@@ -194,7 +243,9 @@ async function renderSectionsWidget() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return container!.replaceChildren(...renderedWidgets);
|
return preserveScrollForPage(() => {
|
||||||
|
container!.replaceChildren(...renderedWidgets);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderBandwidthWidget() {
|
async function renderBandwidthWidget() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Podkop } from '../../types';
|
import { Podkop } from '../../types';
|
||||||
|
import { getClashApiUrl } from '../../../helpers';
|
||||||
|
|
||||||
interface IRenderSectionsProps {
|
interface IRenderSectionsProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -6,6 +7,7 @@ interface IRenderSectionsProps {
|
|||||||
section: Podkop.OutboundGroup;
|
section: Podkop.OutboundGroup;
|
||||||
onTestLatency: (tag: string) => void;
|
onTestLatency: (tag: string) => void;
|
||||||
onChooseOutbound: (selector: string, tag: string) => void;
|
onChooseOutbound: (selector: string, tag: string) => void;
|
||||||
|
latencyFetching: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFailedState() {
|
function renderFailedState() {
|
||||||
@@ -15,7 +17,10 @@ function renderFailedState() {
|
|||||||
class: 'pdk_dashboard-page__outbound-section centered',
|
class: 'pdk_dashboard-page__outbound-section centered',
|
||||||
style: 'height: 127px',
|
style: 'height: 127px',
|
||||||
},
|
},
|
||||||
E('span', {}, _('Dashboard currently unavailable')),
|
E('span', {}, [
|
||||||
|
E('span', {}, _('Dashboard currently unavailable')),
|
||||||
|
E('div', { style: 'text-align: center;' }, `API: ${getClashApiUrl()}`),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +36,7 @@ export function renderDefaultState({
|
|||||||
section,
|
section,
|
||||||
onChooseOutbound,
|
onChooseOutbound,
|
||||||
onTestLatency,
|
onTestLatency,
|
||||||
|
latencyFetching,
|
||||||
}: IRenderSectionsProps) {
|
}: IRenderSectionsProps) {
|
||||||
function testLatency() {
|
function testLatency() {
|
||||||
if (section.withTagSelect) {
|
if (section.withTagSelect) {
|
||||||
@@ -48,11 +54,11 @@ export function renderDefaultState({
|
|||||||
return 'pdk_dashboard-page__outbound-grid__item__latency--empty';
|
return 'pdk_dashboard-page__outbound-grid__item__latency--empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outbound.latency < 200) {
|
if (outbound.latency < 800) {
|
||||||
return 'pdk_dashboard-page__outbound-grid__item__latency--green';
|
return 'pdk_dashboard-page__outbound-grid__item__latency--green';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outbound.latency < 400) {
|
if (outbound.latency < 1500) {
|
||||||
return 'pdk_dashboard-page__outbound-grid__item__latency--yellow';
|
return 'pdk_dashboard-page__outbound-grid__item__latency--yellow';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,14 +101,16 @@ export function renderDefaultState({
|
|||||||
},
|
},
|
||||||
section.displayName,
|
section.displayName,
|
||||||
),
|
),
|
||||||
E(
|
latencyFetching
|
||||||
'button',
|
? E('div', { class: 'skeleton', style: 'width: 99px; height: 28px' })
|
||||||
{
|
: E(
|
||||||
class: 'btn dashboard-sections-grid-item-test-latency',
|
'button',
|
||||||
click: () => testLatency(),
|
{
|
||||||
},
|
class: 'btn dashboard-sections-grid-item-test-latency',
|
||||||
_('Test latency'),
|
click: () => testLatency(),
|
||||||
),
|
},
|
||||||
|
_('Test latency'),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
E(
|
E(
|
||||||
'div',
|
'div',
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export interface StoreType {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
failed: boolean;
|
failed: boolean;
|
||||||
data: Podkop.OutboundGroup[];
|
data: Podkop.OutboundGroup[];
|
||||||
|
latencyFetching: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +173,7 @@ const initialStore: StoreType = {
|
|||||||
sectionsWidget: {
|
sectionsWidget: {
|
||||||
loading: true,
|
loading: true,
|
||||||
failed: false,
|
failed: false,
|
||||||
|
latencyFetching: false,
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { validateShadowsocksUrl } from '../validateShadowsocksUrl';
|
||||||
|
|
||||||
|
const validUrls = [
|
||||||
|
[
|
||||||
|
'no-client',
|
||||||
|
'ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'client',
|
||||||
|
'ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206S3FiWXZiNkhwb1RmTUt0N2VGcUZQSmJNNXBXaHlFU0ZKTXY2dEp1Ym1Fdz06dzRNMEx5RU9OTGQ5SWlkSGc0endTbzN2R3h4NS9aQ3hId0FpaWlxck5hcz0@127.0.0.1:26627?type=tcp#shadowsocks-client',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'plain-user',
|
||||||
|
'ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@127.0.0.1:27214?type=tcp#shadowsocks-plain-user',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const invalidUrls = [
|
||||||
|
['No prefix', 'uuid@127.0.0.1:443?type=tcp'],
|
||||||
|
['No host', 'ss://password@:443?type=tcp'],
|
||||||
|
['No port', 'ss://password@127.0.0.1?type=tcp'],
|
||||||
|
['Invalid port', 'ss://password@127.0.0.1:abc?type=tcp'],
|
||||||
|
['Missing type', 'ss://password@127.0.0.1:443'],
|
||||||
|
['Contains space', 'ss://password@127.0.0.1:443?type=tcp #extra'],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('validateShadowsocksUrl', () => {
|
||||||
|
describe.each(validUrls)('Valid URL: %s', (_desc, url) => {
|
||||||
|
it(`returns valid=true for "${url}"`, () => {
|
||||||
|
const res = validateShadowsocksUrl(url);
|
||||||
|
expect(res.valid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => {
|
||||||
|
it(`returns valid=false for "${url}"`, () => {
|
||||||
|
const res = validateShadowsocksUrl(url);
|
||||||
|
expect(res.valid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects invalid port range', () => {
|
||||||
|
const res = validateShadowsocksUrl(
|
||||||
|
'ss://password@127.0.0.1:99999?type=tcp',
|
||||||
|
);
|
||||||
|
expect(res.valid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
131
fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js
Normal file
131
fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { validateTrojanUrl } from '../validateTrojanUrl';
|
||||||
|
|
||||||
|
const validUrls = [
|
||||||
|
// TCP
|
||||||
|
[
|
||||||
|
'tcp + none',
|
||||||
|
'trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tcp + reality',
|
||||||
|
'trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni=google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tcp + tls',
|
||||||
|
'trojan://EJjpAj02lg@127.0.0.1:11381?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-tcp-tls',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tcp + tls + insecure',
|
||||||
|
'trojan://ZP2Ik5sxN3@127.0.0.1:16247?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-tcp-tls-insecure',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tcp + tls + ech',
|
||||||
|
'trojan://90caP481ay@127.0.0.1:59708?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACC2y%2BAe4dqthLNpfvmtE6g%2BnaJ%2FciK6P%2BREbRLkR%2Fg%2FEgAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-tcp-tls-ech',
|
||||||
|
],
|
||||||
|
|
||||||
|
// mKCP
|
||||||
|
[
|
||||||
|
'mKCP + none',
|
||||||
|
'trojan://N5v7iIOe9G@127.0.0.1:36319?type=kcp&headerType=none&seed=P91wFIfjzZ&security=none#trojan-mKCP',
|
||||||
|
],
|
||||||
|
|
||||||
|
// WebSocket
|
||||||
|
[
|
||||||
|
'ws + none',
|
||||||
|
'trojan://G3cE9phv1g@127.0.0.1:57370?type=ws&path=%2Fwspath&host=google.com&security=none#trojan-websocket-none',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ws + tls',
|
||||||
|
'trojan://FBok41WczO@127.0.0.1:59919?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-websocket-tls',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ws + tls + insecure',
|
||||||
|
'trojan://bhwvndUBPA@127.0.0.1:22969?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-websocket-tls-insecure',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ws + tls + ech',
|
||||||
|
'trojan://pwiduqFUWO@127.0.0.1:46765?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCFcQYEtwrFOidJJLYHvSiN%2BljRgaAIrNHoVnio3uXAOwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-websocket-tls-ech',
|
||||||
|
],
|
||||||
|
|
||||||
|
// gRPC
|
||||||
|
[
|
||||||
|
'grpc + none',
|
||||||
|
'trojan://WMR7qkKhsV@127.0.0.1:27897?type=grpc&serviceName=TunService&authority=authority&security=none#trojan-gRPC-none',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'grpc + reality',
|
||||||
|
'trojan://KVuRNsu6KG@127.0.0.1:46077?type=grpc&serviceName=TunService&authority=authority&security=reality&pbk=Xn59i4gum3ppCICS6-_NuywrhHIVVAH54b2mjd5CFkE&fp=chrome&sni=google.com&sid=e5be&spx=%2F#trojan-gRPC-reality',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'grpc + tls',
|
||||||
|
'trojan://7BJtbywy8h@127.0.0.1:10627?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-gRPC-tls',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'grpc + tls + insecure',
|
||||||
|
'trojan://TI3PakvtP4@127.0.0.1:10435?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-gRPC-tls-insecure',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'grpc + tls + ech',
|
||||||
|
'trojan://mbzoVKL27h@127.0.0.1:38681?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCq72Ru3VbFlDpKttl3LccmInu8R2oAsCr8wzyxB0vZZQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-gRPC-tls-ech',
|
||||||
|
],
|
||||||
|
|
||||||
|
// HTTPUpgrade
|
||||||
|
[
|
||||||
|
'httpupgrade + none',
|
||||||
|
'trojan://uc44gBwOKQ@127.0.0.1:29085?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=none#trojan-httpupgrade-none',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'httpupgrade + tls',
|
||||||
|
'trojan://MhNxbcVB14@127.0.0.1:32700?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-httpupgrade-tls',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'httpupgrade + tls + insecure',
|
||||||
|
'trojan://7SOQFUpLob@127.0.0.1:28474?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-httpupgrade-tls-insecure',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'httpupgrade + tls + ech',
|
||||||
|
'trojan://ou8pLSyx9N@127.0.0.1:17737?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACB%2FlkIkit%2BblFzE7PtbYDVF3NXK8olXJ5a7YwY%2Biy9QQwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-httpupgrade-tls-ech',
|
||||||
|
],
|
||||||
|
|
||||||
|
// XHTTP
|
||||||
|
[
|
||||||
|
'xhttp + none',
|
||||||
|
'trojan://VEetltxLtw@127.0.0.1:59072?type=xhttp&path=%2Fxhttppath&host=google.com&mode=auto&security=none#trojan-xhttp',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const invalidUrls = [
|
||||||
|
['No prefix', 'uuid@host:443?type=tcp&security=tls'],
|
||||||
|
['No password', 'trojan://@127.0.0.1:443?type=tcp&security=tls'],
|
||||||
|
['No host', 'trojan://pass@:443?type=tcp&security=tls'],
|
||||||
|
['No port', 'trojan://pass@127.0.0.1?type=tcp&security=tls'],
|
||||||
|
['Invalid port', 'trojan://pass@127.0.0.1:abc?type=tcp&security=tls'],
|
||||||
|
[
|
||||||
|
'tcp + reality + unexpected spaces',
|
||||||
|
'trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni= google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('validateTrojanUrl', () => {
|
||||||
|
describe.each(validUrls)('Valid URL: %s', (_desc, url) => {
|
||||||
|
it(`returns valid=true for "${url}"`, () => {
|
||||||
|
const res = validateTrojanUrl(url);
|
||||||
|
expect(res.valid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => {
|
||||||
|
it(`returns valid=false for "${url}"`, () => {
|
||||||
|
const res = validateTrojanUrl(url);
|
||||||
|
expect(res.valid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects invalid port range', () => {
|
||||||
|
const res = validateTrojanUrl(
|
||||||
|
'trojan://pass@127.0.0.1:99999?type=tcp&security=tls',
|
||||||
|
);
|
||||||
|
expect(res.valid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,32 +1,57 @@
|
|||||||
import { ValidationResult } from './types';
|
import { ValidationResult } from './types';
|
||||||
|
|
||||||
// TODO refactor current validation and add tests
|
|
||||||
export function validateTrojanUrl(url: string): ValidationResult {
|
export function validateTrojanUrl(url: string): ValidationResult {
|
||||||
if (!url.startsWith('trojan://')) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _('Invalid Trojan URL: must start with trojan://'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!url || /\s/.test(url)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _('Invalid Trojan URL: must not contain spaces'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
if (!url.startsWith('trojan://')) {
|
||||||
|
|
||||||
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: _('Invalid Trojan URL: must start with trojan://'),
|
||||||
'Invalid Trojan URL: must contain username, hostname and port',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!url || /\s/.test(url)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: _('Invalid Trojan URL: must not contain spaces'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = url.slice('trojan://'.length);
|
||||||
|
const [mainPart] = body.split('#');
|
||||||
|
const [userHostPort] = mainPart.split('?');
|
||||||
|
|
||||||
|
const [userPart, hostPortPart] = userHostPort.split('@');
|
||||||
|
|
||||||
|
if (!userHostPort)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid Trojan URL: missing credentials and host',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!userPart)
|
||||||
|
return { valid: false, message: 'Invalid Trojan URL: missing password' };
|
||||||
|
|
||||||
|
if (!hostPortPart)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid Trojan URL: missing hostname and port',
|
||||||
|
};
|
||||||
|
|
||||||
|
const [host, port] = hostPortPart.split(':');
|
||||||
|
|
||||||
|
if (!host)
|
||||||
|
return { valid: false, message: 'Invalid Trojan URL: missing hostname' };
|
||||||
|
|
||||||
|
if (!port)
|
||||||
|
return { valid: false, message: 'Invalid Trojan URL: missing port' };
|
||||||
|
|
||||||
|
const portNum = Number(port);
|
||||||
|
|
||||||
|
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid Trojan URL: invalid port number',
|
||||||
|
};
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return { valid: false, message: _('Invalid Trojan URL: parsing failed') };
|
return { valid: false, message: _('Invalid Trojan URL: parsing failed') };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,63 @@
|
|||||||
import { ValidationResult } from './types';
|
import { ValidationResult } from './types';
|
||||||
|
import { parseQueryString } from '../helpers';
|
||||||
|
|
||||||
export function validateVlessUrl(url: string): ValidationResult {
|
export function validateVlessUrl(url: string): ValidationResult {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
if (!url.startsWith('vless://'))
|
||||||
|
|
||||||
if (!url || /\s/.test(url)) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _('Invalid VLESS URL: must not contain spaces'),
|
message: 'Invalid VLESS URL: must start with vless://',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedUrl.protocol !== 'vless:') {
|
if (/\s/.test(url))
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _('Invalid VLESS URL: must start with vless://'),
|
message: 'Invalid VLESS URL: must not contain spaces',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedUrl.username) {
|
const body = url.slice('vless://'.length);
|
||||||
return { valid: false, message: _('Invalid VLESS URL: missing UUID') };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedUrl.hostname) {
|
const [mainPart] = body.split('#');
|
||||||
return { valid: false, message: _('Invalid VLESS URL: missing server') };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedUrl.port) {
|
const [userHostPort, queryString] = mainPart.split('?');
|
||||||
return { valid: false, message: _('Invalid VLESS URL: missing port') };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (!userHostPort)
|
||||||
isNaN(+parsedUrl.port) ||
|
|
||||||
+parsedUrl.port < 1 ||
|
|
||||||
+parsedUrl.port > 65535
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: missing host and UUID',
|
||||||
'Invalid VLESS URL: invalid port number. Must be between 1 and 65535',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedUrl.search) {
|
const [userPart, hostPortPart] = userHostPort.split('@');
|
||||||
|
|
||||||
|
if (!userPart)
|
||||||
|
return { valid: false, message: 'Invalid VLESS URL: missing UUID' };
|
||||||
|
|
||||||
|
if (!hostPortPart)
|
||||||
|
return { valid: false, message: 'Invalid VLESS URL: missing server' };
|
||||||
|
|
||||||
|
const [host, port] = hostPortPart.split(':');
|
||||||
|
|
||||||
|
if (!host)
|
||||||
|
return { valid: false, message: 'Invalid VLESS URL: missing hostname' };
|
||||||
|
|
||||||
|
if (!port)
|
||||||
|
return { valid: false, message: 'Invalid VLESS URL: missing port' };
|
||||||
|
|
||||||
|
const portNum = Number(port);
|
||||||
|
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _('Invalid VLESS URL: missing query parameters'),
|
message: 'Invalid VLESS URL: invalid port number',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams(parsedUrl.search);
|
if (!queryString)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid VLESS URL: missing query parameters',
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = parseQueryString(queryString);
|
||||||
|
|
||||||
const type = params.get('type');
|
|
||||||
const validTypes = [
|
const validTypes = [
|
||||||
'tcp',
|
'tcp',
|
||||||
'raw',
|
'raw',
|
||||||
@@ -64,45 +69,31 @@ export function validateVlessUrl(url: string): ValidationResult {
|
|||||||
'ws',
|
'ws',
|
||||||
'kcp',
|
'kcp',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!type || !validTypes.includes(type)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws',
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const security = params.get('security');
|
|
||||||
const validSecurities = ['tls', 'reality', 'none'];
|
const validSecurities = ['tls', 'reality', 'none'];
|
||||||
|
|
||||||
if (!security || !validSecurities.includes(security)) {
|
if (!params.type || !validTypes.includes(params.type))
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: unsupported or missing type',
|
||||||
'Invalid VLESS URL: security must be one of tls, reality, none',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (security === 'reality') {
|
if (!params.security || !validSecurities.includes(params.security))
|
||||||
if (!params.get('pbk')) {
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid VLESS URL: unsupported or missing security',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.security === 'reality') {
|
||||||
|
if (!params.pbk)
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: missing pbk for reality',
|
||||||
'Invalid VLESS URL: missing pbk parameter for reality security',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
if (!params.fp)
|
||||||
if (!params.get('fp')) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: missing fp for reality',
|
||||||
'Invalid VLESS URL: missing fp parameter for reality security',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: _('Valid') };
|
return { valid: true, message: _('Valid') };
|
||||||
|
|||||||
98
install.sh
98
install.sh
@@ -4,6 +4,10 @@ REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest"
|
|||||||
DOWNLOAD_DIR="/tmp/podkop"
|
DOWNLOAD_DIR="/tmp/podkop"
|
||||||
COUNT=3
|
COUNT=3
|
||||||
|
|
||||||
|
# Cached flag to switch between ipk or apk package managers
|
||||||
|
PKG_IS_APK=0
|
||||||
|
command -v apk >/dev/null 2>&1 && PKG_IS_APK=1
|
||||||
|
|
||||||
rm -rf "$DOWNLOAD_DIR"
|
rm -rf "$DOWNLOAD_DIR"
|
||||||
mkdir -p "$DOWNLOAD_DIR"
|
mkdir -p "$DOWNLOAD_DIR"
|
||||||
|
|
||||||
@@ -11,20 +15,65 @@ msg() {
|
|||||||
printf "\033[32;1m%s\033[0m\n" "$1"
|
printf "\033[32;1m%s\033[0m\n" "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pkg_is_installed () {
|
||||||
|
local pkg_name="$1"
|
||||||
|
|
||||||
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
||||||
|
# grep -q should work without change based on example from documentation
|
||||||
|
# apk list --installed --providers dnsmasq
|
||||||
|
# <dnsmasq> dnsmasq-full-2.90-r3 x86_64 {feeds/base/package/network/services/dnsmasq} (GPL-2.0) [installed]
|
||||||
|
apk list --installed | grep -q "$pkg_name"
|
||||||
|
else
|
||||||
|
opkg list-installed | grep -q "$pkg_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg_remove() {
|
||||||
|
local pkg_name="$1"
|
||||||
|
|
||||||
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
||||||
|
# TODO: check --force-depends flag
|
||||||
|
# Nothing here: https://openwrt.org/docs/guide-user/additional-software/opkg-to-apk-cheatsheet
|
||||||
|
apk del "$pkg_name"
|
||||||
|
else
|
||||||
|
opkg remove --force-depends "$pkg_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg_list_update() {
|
||||||
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
||||||
|
apk update
|
||||||
|
else
|
||||||
|
opkg update
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg_install() {
|
||||||
|
local pkg_file="$1"
|
||||||
|
|
||||||
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
||||||
|
# Can't install without flag based on info from documentation
|
||||||
|
# If you're installing a non-standard (self-built) package, use the --allow-untrusted option:
|
||||||
|
apk add --allow-untrusted "$pkg_file"
|
||||||
|
else
|
||||||
|
opkg install "$pkg_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
check_system
|
check_system
|
||||||
sing_box
|
sing_box
|
||||||
|
|
||||||
/usr/sbin/ntpd -q -p 194.190.168.1 -p 216.239.35.0 -p 216.239.35.4 -p 162.159.200.1 -p 162.159.200.123
|
/usr/sbin/ntpd -q -p 194.190.168.1 -p 216.239.35.0 -p 216.239.35.4 -p 162.159.200.1 -p 162.159.200.123
|
||||||
|
|
||||||
opkg update || { echo "opkg update failed"; exit 1; }
|
pkg_list_update || { echo "Packages list update failed"; exit 1; }
|
||||||
|
|
||||||
if [ -f "/etc/init.d/podkop" ]; then
|
if [ -f "/etc/init.d/podkop" ]; then
|
||||||
msg "Podkop is already installed. Upgraded..."
|
msg "Podkop is already installed. Upgraded..."
|
||||||
else
|
else
|
||||||
msg "Installed podkop..."
|
msg "Installed podkop..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v curl &> /dev/null; then
|
if command -v curl &> /dev/null; then
|
||||||
check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest")
|
check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest")
|
||||||
|
|
||||||
@@ -34,11 +83,18 @@ main() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local grep_url_pattern
|
||||||
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
||||||
|
grep_url_pattern='https://[^"[:space:]]*\.apk'
|
||||||
|
else
|
||||||
|
grep_url_pattern='https://[^"[:space:]]*\.ipk'
|
||||||
|
fi
|
||||||
|
|
||||||
download_success=0
|
download_success=0
|
||||||
while read -r url; do
|
while read -r url; do
|
||||||
filename=$(basename "$url")
|
filename=$(basename "$url")
|
||||||
filepath="$DOWNLOAD_DIR/$filename"
|
filepath="$DOWNLOAD_DIR/$filename"
|
||||||
|
|
||||||
attempt=0
|
attempt=0
|
||||||
while [ $attempt -lt $COUNT ]; do
|
while [ $attempt -lt $COUNT ]; do
|
||||||
msg "Download $filename (count $((attempt+1)))..."
|
msg "Download $filename (count $((attempt+1)))..."
|
||||||
@@ -53,40 +109,40 @@ main() {
|
|||||||
rm -f "$filepath"
|
rm -f "$filepath"
|
||||||
attempt=$((attempt+1))
|
attempt=$((attempt+1))
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $attempt -eq $COUNT ]; then
|
if [ $attempt -eq $COUNT ]; then
|
||||||
msg "Failed to download $filename after $COUNT attempts"
|
msg "Failed to download $filename after $COUNT attempts"
|
||||||
fi
|
fi
|
||||||
done < <(wget -qO- "$REPO" | grep -o 'https://[^"[:space:]]*\.ipk')
|
done < <(wget -qO- "$REPO" | grep -o "$grep_url_pattern")
|
||||||
|
|
||||||
if [ $download_success -eq 0 ]; then
|
if [ $download_success -eq 0 ]; then
|
||||||
msg "No packages were downloaded successfully"
|
msg "No packages were downloaded successfully"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for pkg in podkop luci-app-podkop; do
|
for pkg in podkop luci-app-podkop; do
|
||||||
file=$(ls "$DOWNLOAD_DIR" | grep "^$pkg" | head -n 1)
|
file=$(ls "$DOWNLOAD_DIR" | grep "^$pkg" | head -n 1)
|
||||||
if [ -n "$file" ]; then
|
if [ -n "$file" ]; then
|
||||||
msg "Installing $file"
|
msg "Installing $file"
|
||||||
opkg install "$DOWNLOAD_DIR/$file"
|
pkg_install "$DOWNLOAD_DIR/$file"
|
||||||
sleep 3
|
sleep 3
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
ru=$(ls "$DOWNLOAD_DIR" | grep "luci-i18n-podkop-ru" | head -n 1)
|
ru=$(ls "$DOWNLOAD_DIR" | grep "luci-i18n-podkop-ru" | head -n 1)
|
||||||
if [ -n "$ru" ]; then
|
if [ -n "$ru" ]; then
|
||||||
if opkg list-installed | grep -q luci-i18n-podkop-ru; then
|
if pkg_is_installed luci-i18n-podkop-ru; then
|
||||||
msg "Upgraded ru translation..."
|
msg "Upgraded ru translation..."
|
||||||
opkg remove luci-i18n-podkop*
|
pkg_remove luci-i18n-podkop*
|
||||||
opkg install "$DOWNLOAD_DIR/$ru"
|
pkg_install "$DOWNLOAD_DIR/$ru"
|
||||||
else
|
else
|
||||||
msg "Русский язык интерфейса ставим? y/n (Need a Russian translation?)"
|
msg "Русский язык интерфейса ставим? y/n (Need a Russian translation?)"
|
||||||
while true; do
|
while true; do
|
||||||
read -r -p '' RUS
|
read -r -p '' RUS
|
||||||
case $RUS in
|
case $RUS in
|
||||||
y)
|
y)
|
||||||
opkg remove luci-i18n-podkop*
|
pkg_remove luci-i18n-podkop*
|
||||||
opkg install "$DOWNLOAD_DIR/$ru"
|
pkg_install "$DOWNLOAD_DIR/$ru"
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
n)
|
n)
|
||||||
@@ -133,15 +189,17 @@ check_system() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if opkg list-installed | grep -q https-dns-proxy; then
|
if pkg_is_installed https-dns-proxy; then
|
||||||
msg "Сonflicting package detected: https-dns-proxy. Remove?"
|
msg "Сonflicting package detected: https-dns-proxy. Remove?"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -r -p '' DNSPROXY
|
read -r -p '' DNSPROXY
|
||||||
case $DNSPROXY in
|
case $DNSPROXY in
|
||||||
|
|
||||||
yes|y|Y|yes)
|
yes|y|Y)
|
||||||
opkg remove --force-depends luci-app-https-dns-proxy https-dns-proxy luci-i18n-https-dns-proxy*
|
pkg_remove luci-app-https-dns-proxy
|
||||||
|
pkg_remove https-dns-proxy
|
||||||
|
pkg_remove luci-i18n-https-dns-proxy*
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -154,7 +212,7 @@ check_system() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sing_box() {
|
sing_box() {
|
||||||
if ! opkg list-installed | grep -q "^sing-box"; then
|
if ! pkg_is_installed "^sing-box"; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -165,8 +223,8 @@ sing_box() {
|
|||||||
msg "sing-box version $sing_box_version is older than required $required_version"
|
msg "sing-box version $sing_box_version is older than required $required_version"
|
||||||
msg "Removing old version..."
|
msg "Removing old version..."
|
||||||
service podkop stop
|
service podkop stop
|
||||||
opkg remove sing-box --force-depends
|
pkg_remove sing-box
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main
|
main
|
||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-podkop
|
PKG_NAME:=luci-app-podkop
|
||||||
|
|
||||||
PKG_VERSION := $(if $(PKG_VERSION),$(PKG_VERSION),dev_$(shell date +%d%m%Y))
|
PKG_VERSION := $(if $(PODKOP_VERSION),$(PODKOP_VERSION),0.$(shell date +%d%m%Y))
|
||||||
|
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
@@ -19,4 +19,12 @@ LUCI_LANGUAGES:=en ru
|
|||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot signature
|
define Package/$(PKG_NAME)/install
|
||||||
|
$(INSTALL_DIR) $(1)$(HTDOCS)
|
||||||
|
$(CP) $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/
|
||||||
|
$(INSTALL_DIR) $(1)/
|
||||||
|
$(CP) $(PKG_BUILD_DIR)/root/* $(1)/
|
||||||
|
sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' $(1)$(HTDOCS)/luci-static/resources/view/podkop/main.js || true
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||||
@@ -12,7 +12,7 @@ function createAdditionalSection(mainSection) {
|
|||||||
form.Flag,
|
form.Flag,
|
||||||
'yacd',
|
'yacd',
|
||||||
_('Yacd enable'),
|
_('Yacd enable'),
|
||||||
`<a href="${main.getBaseUrl()}:9090/ui" target="_blank">${main.getBaseUrl()}:9090/ui</a>`,
|
`<a href="${main.getClashUIUrl()}" target="_blank">${main.getClashUIUrl()}</a>`,
|
||||||
);
|
);
|
||||||
o.default = '0';
|
o.default = '0';
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
|||||||
@@ -764,7 +764,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validation = main.validateIPV4(value);
|
const validation = main.validateSubnet(value);
|
||||||
|
|
||||||
if (validation.valid) {
|
if (validation.valid) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ function showConfigModal(command, title) {
|
|||||||
let formattedOutput = '';
|
let formattedOutput = '';
|
||||||
|
|
||||||
if (command === 'global_check') {
|
if (command === 'global_check') {
|
||||||
safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', (res) => {
|
safeExec('/usr/bin/podkop', [command, `${main.PODKOP_LUCI_APP_VERSION}`], 'P0_PRIORITY', (res) => {
|
||||||
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
|
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -918,18 +918,11 @@ async function updateDiagnostics() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
safeExec(
|
updateTextElement(
|
||||||
'/usr/bin/podkop',
|
'luci-version',
|
||||||
['show_luci_version'],
|
document.createTextNode(
|
||||||
'P2_PRIORITY',
|
`${main.PODKOP_LUCI_APP_VERSION}`
|
||||||
(result) => {
|
)
|
||||||
updateTextElement(
|
|
||||||
'luci-version',
|
|
||||||
document.createTextNode(
|
|
||||||
result.stdout ? result.stdout.trim() : _('Unknown'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
safeExec(
|
safeExec(
|
||||||
|
|||||||
@@ -210,165 +210,6 @@ function validateShadowsocksUrl(url) {
|
|||||||
return { valid: true, message: _("Valid") };
|
return { valid: true, message: _("Valid") };
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/validators/validateVlessUrl.ts
|
|
||||||
function validateVlessUrl(url) {
|
|
||||||
try {
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
if (!url || /\s/.test(url)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _("Invalid VLESS URL: must not contain spaces")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (parsedUrl.protocol !== "vless:") {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _("Invalid VLESS URL: must start with vless://")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!parsedUrl.username) {
|
|
||||||
return { valid: false, message: _("Invalid VLESS URL: missing UUID") };
|
|
||||||
}
|
|
||||||
if (!parsedUrl.hostname) {
|
|
||||||
return { valid: false, message: _("Invalid VLESS URL: missing server") };
|
|
||||||
}
|
|
||||||
if (!parsedUrl.port) {
|
|
||||||
return { valid: false, message: _("Invalid VLESS URL: missing port") };
|
|
||||||
}
|
|
||||||
if (isNaN(+parsedUrl.port) || +parsedUrl.port < 1 || +parsedUrl.port > 65535) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: invalid port number. Must be between 1 and 65535"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!parsedUrl.search) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _("Invalid VLESS URL: missing query parameters")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams(parsedUrl.search);
|
|
||||||
const type = params.get("type");
|
|
||||||
const validTypes = [
|
|
||||||
"tcp",
|
|
||||||
"raw",
|
|
||||||
"udp",
|
|
||||||
"grpc",
|
|
||||||
"http",
|
|
||||||
"httpupgrade",
|
|
||||||
"xhttp",
|
|
||||||
"ws",
|
|
||||||
"kcp"
|
|
||||||
];
|
|
||||||
if (!type || !validTypes.includes(type)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const security = params.get("security");
|
|
||||||
const validSecurities = ["tls", "reality", "none"];
|
|
||||||
if (!security || !validSecurities.includes(security)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: security must be one of tls, reality, none"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (security === "reality") {
|
|
||||||
if (!params.get("pbk")) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: missing pbk parameter for reality security"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!params.get("fp")) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: missing fp parameter for reality security"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { valid: true, message: _("Valid") };
|
|
||||||
} catch (_e) {
|
|
||||||
return { valid: false, message: _("Invalid VLESS URL: parsing failed") };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/validators/validateOutboundJson.ts
|
|
||||||
function validateOutboundJson(value) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value);
|
|
||||||
if (!parsed.type || !parsed.server || !parsed.server_port) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
'Outbound JSON must contain at least "type", "server" and "server_port" fields'
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { valid: true, message: _("Valid") };
|
|
||||||
} catch {
|
|
||||||
return { valid: false, message: _("Invalid JSON format") };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/validators/validateTrojanUrl.ts
|
|
||||||
function validateTrojanUrl(url) {
|
|
||||||
if (!url.startsWith("trojan://")) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _("Invalid Trojan URL: must start with trojan://")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!url || /\s/.test(url)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _("Invalid Trojan URL: must not contain spaces")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid Trojan URL: must contain username, hostname and port"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
return { valid: false, message: _("Invalid Trojan URL: parsing failed") };
|
|
||||||
}
|
|
||||||
return { valid: true, message: _("Valid") };
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/validators/validateProxyUrl.ts
|
|
||||||
function validateProxyUrl(url) {
|
|
||||||
if (url.startsWith("ss://")) {
|
|
||||||
return validateShadowsocksUrl(url);
|
|
||||||
}
|
|
||||||
if (url.startsWith("vless://")) {
|
|
||||||
return validateVlessUrl(url);
|
|
||||||
}
|
|
||||||
if (url.startsWith("trojan://")) {
|
|
||||||
return validateTrojanUrl(url);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _("URL must start with vless:// or ss:// or trojan://")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/helpers/getBaseUrl.ts
|
// src/helpers/getBaseUrl.ts
|
||||||
function getBaseUrl() {
|
function getBaseUrl() {
|
||||||
const { protocol, hostname } = window.location;
|
const { protocol, hostname } = window.location;
|
||||||
@@ -592,6 +433,7 @@ var STATUS_COLORS = {
|
|||||||
ERROR: "#f44336",
|
ERROR: "#f44336",
|
||||||
WARNING: "#ff9800"
|
WARNING: "#ff9800"
|
||||||
};
|
};
|
||||||
|
var PODKOP_LUCI_APP_VERSION = "__COMPILED_VERSION_VARIABLE__";
|
||||||
var FAKEIP_CHECK_DOMAIN = "fakeip.podkop.fyi";
|
var FAKEIP_CHECK_DOMAIN = "fakeip.podkop.fyi";
|
||||||
var IP_CHECK_DOMAIN = "ip.podkop.fyi";
|
var IP_CHECK_DOMAIN = "ip.podkop.fyi";
|
||||||
var REGIONAL_OPTIONS = [
|
var REGIONAL_OPTIONS = [
|
||||||
@@ -763,19 +605,215 @@ async function onMount(id) {
|
|||||||
|
|
||||||
// src/helpers/getClashApiUrl.ts
|
// src/helpers/getClashApiUrl.ts
|
||||||
function getClashApiUrl() {
|
function getClashApiUrl() {
|
||||||
const { protocol, hostname } = window.location;
|
const { hostname } = window.location;
|
||||||
return `${protocol}//${hostname}:9090`;
|
return `http://${hostname}:9090`;
|
||||||
}
|
}
|
||||||
function getClashWsUrl() {
|
function getClashWsUrl() {
|
||||||
const { hostname } = window.location;
|
const { hostname } = window.location;
|
||||||
return `ws://${hostname}:9090`;
|
return `ws://${hostname}:9090`;
|
||||||
}
|
}
|
||||||
|
function getClashUIUrl() {
|
||||||
|
const { hostname } = window.location;
|
||||||
|
return `http://${hostname}:9090/ui`;
|
||||||
|
}
|
||||||
|
|
||||||
// src/helpers/splitProxyString.ts
|
// src/helpers/splitProxyString.ts
|
||||||
function splitProxyString(str) {
|
function splitProxyString(str) {
|
||||||
return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean);
|
return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/helpers/preserveScrollForPage.ts
|
||||||
|
function preserveScrollForPage(renderFn) {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
renderFn();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo({ top: scrollY });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/helpers/parseQueryString.ts
|
||||||
|
function parseQueryString(query) {
|
||||||
|
const clean = query.startsWith("?") ? query.slice(1) : query;
|
||||||
|
return clean.split("&").filter(Boolean).reduce(
|
||||||
|
(acc, pair) => {
|
||||||
|
const [rawKey, rawValue = ""] = pair.split("=");
|
||||||
|
if (!rawKey) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const key = decodeURIComponent(rawKey);
|
||||||
|
const value = decodeURIComponent(rawValue);
|
||||||
|
return { ...acc, [key]: value };
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/validators/validateVlessUrl.ts
|
||||||
|
function validateVlessUrl(url) {
|
||||||
|
try {
|
||||||
|
if (!url.startsWith("vless://"))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: must start with vless://"
|
||||||
|
};
|
||||||
|
if (/\s/.test(url))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: must not contain spaces"
|
||||||
|
};
|
||||||
|
const body = url.slice("vless://".length);
|
||||||
|
const [mainPart] = body.split("#");
|
||||||
|
const [userHostPort, queryString] = mainPart.split("?");
|
||||||
|
if (!userHostPort)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: missing host and UUID"
|
||||||
|
};
|
||||||
|
const [userPart, hostPortPart] = userHostPort.split("@");
|
||||||
|
if (!userPart)
|
||||||
|
return { valid: false, message: "Invalid VLESS URL: missing UUID" };
|
||||||
|
if (!hostPortPart)
|
||||||
|
return { valid: false, message: "Invalid VLESS URL: missing server" };
|
||||||
|
const [host, port] = hostPortPart.split(":");
|
||||||
|
if (!host)
|
||||||
|
return { valid: false, message: "Invalid VLESS URL: missing hostname" };
|
||||||
|
if (!port)
|
||||||
|
return { valid: false, message: "Invalid VLESS URL: missing port" };
|
||||||
|
const portNum = Number(port);
|
||||||
|
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: invalid port number"
|
||||||
|
};
|
||||||
|
if (!queryString)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: missing query parameters"
|
||||||
|
};
|
||||||
|
const params = parseQueryString(queryString);
|
||||||
|
const validTypes = [
|
||||||
|
"tcp",
|
||||||
|
"raw",
|
||||||
|
"udp",
|
||||||
|
"grpc",
|
||||||
|
"http",
|
||||||
|
"httpupgrade",
|
||||||
|
"xhttp",
|
||||||
|
"ws",
|
||||||
|
"kcp"
|
||||||
|
];
|
||||||
|
const validSecurities = ["tls", "reality", "none"];
|
||||||
|
if (!params.type || !validTypes.includes(params.type))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: unsupported or missing type"
|
||||||
|
};
|
||||||
|
if (!params.security || !validSecurities.includes(params.security))
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: unsupported or missing security"
|
||||||
|
};
|
||||||
|
if (params.security === "reality") {
|
||||||
|
if (!params.pbk)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: missing pbk for reality"
|
||||||
|
};
|
||||||
|
if (!params.fp)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid VLESS URL: missing fp for reality"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { valid: true, message: _("Valid") };
|
||||||
|
} catch (_e) {
|
||||||
|
return { valid: false, message: _("Invalid VLESS URL: parsing failed") };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/validators/validateOutboundJson.ts
|
||||||
|
function validateOutboundJson(value) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (!parsed.type || !parsed.server || !parsed.server_port) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: _(
|
||||||
|
'Outbound JSON must contain at least "type", "server" and "server_port" fields'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { valid: true, message: _("Valid") };
|
||||||
|
} catch {
|
||||||
|
return { valid: false, message: _("Invalid JSON format") };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/validators/validateTrojanUrl.ts
|
||||||
|
function validateTrojanUrl(url) {
|
||||||
|
try {
|
||||||
|
if (!url.startsWith("trojan://")) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: _("Invalid Trojan URL: must start with trojan://")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!url || /\s/.test(url)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: _("Invalid Trojan URL: must not contain spaces")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const body = url.slice("trojan://".length);
|
||||||
|
const [mainPart] = body.split("#");
|
||||||
|
const [userHostPort] = mainPart.split("?");
|
||||||
|
const [userPart, hostPortPart] = userHostPort.split("@");
|
||||||
|
if (!userHostPort)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid Trojan URL: missing credentials and host"
|
||||||
|
};
|
||||||
|
if (!userPart)
|
||||||
|
return { valid: false, message: "Invalid Trojan URL: missing password" };
|
||||||
|
if (!hostPortPart)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid Trojan URL: missing hostname and port"
|
||||||
|
};
|
||||||
|
const [host, port] = hostPortPart.split(":");
|
||||||
|
if (!host)
|
||||||
|
return { valid: false, message: "Invalid Trojan URL: missing hostname" };
|
||||||
|
if (!port)
|
||||||
|
return { valid: false, message: "Invalid Trojan URL: missing port" };
|
||||||
|
const portNum = Number(port);
|
||||||
|
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid Trojan URL: invalid port number"
|
||||||
|
};
|
||||||
|
} catch (_e) {
|
||||||
|
return { valid: false, message: _("Invalid Trojan URL: parsing failed") };
|
||||||
|
}
|
||||||
|
return { valid: true, message: _("Valid") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/validators/validateProxyUrl.ts
|
||||||
|
function validateProxyUrl(url) {
|
||||||
|
if (url.startsWith("ss://")) {
|
||||||
|
return validateShadowsocksUrl(url);
|
||||||
|
}
|
||||||
|
if (url.startsWith("vless://")) {
|
||||||
|
return validateVlessUrl(url);
|
||||||
|
}
|
||||||
|
if (url.startsWith("trojan://")) {
|
||||||
|
return validateTrojanUrl(url);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: _("URL must start with vless:// or ss:// or trojan://")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// src/clash/methods/createBaseApiRequest.ts
|
// src/clash/methods/createBaseApiRequest.ts
|
||||||
async function createBaseApiRequest(fetchFn) {
|
async function createBaseApiRequest(fetchFn) {
|
||||||
try {
|
try {
|
||||||
@@ -925,6 +963,9 @@ async function getDashboardSections() {
|
|||||||
const outbound = proxies.find(
|
const outbound = proxies.find(
|
||||||
(proxy) => proxy.code === `${section[".name"]}-out`
|
(proxy) => proxy.code === `${section[".name"]}-out`
|
||||||
);
|
);
|
||||||
|
const parsedOutbound = JSON.parse(section.outbound_json);
|
||||||
|
const parsedTag = parsedOutbound?.tag ? decodeURIComponent(parsedOutbound?.tag) : void 0;
|
||||||
|
const proxyDisplayName = parsedTag || outbound?.value?.name || "";
|
||||||
return {
|
return {
|
||||||
withTagSelect: false,
|
withTagSelect: false,
|
||||||
code: outbound?.code || section[".name"],
|
code: outbound?.code || section[".name"],
|
||||||
@@ -932,7 +973,7 @@ async function getDashboardSections() {
|
|||||||
outbounds: [
|
outbounds: [
|
||||||
{
|
{
|
||||||
code: outbound?.code || section[".name"],
|
code: outbound?.code || section[".name"],
|
||||||
displayName: decodeURIComponent(JSON.parse(section.outbound_json)?.tag) || outbound?.value?.name || "",
|
displayName: proxyDisplayName,
|
||||||
latency: outbound?.value?.history?.[0]?.delay || 0,
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||||
type: outbound?.value?.type || "",
|
type: outbound?.value?.type || "",
|
||||||
selected: true
|
selected: true
|
||||||
@@ -1008,7 +1049,7 @@ async function getPodkopStatus() {
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: "/usr/bin/podkop",
|
command: "/usr/bin/podkop",
|
||||||
args: ["get_status"],
|
args: ["get_status"],
|
||||||
timeout: 1e3
|
timeout: 1e4
|
||||||
});
|
});
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
||||||
@@ -1021,7 +1062,7 @@ async function getSingboxStatus() {
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: "/usr/bin/podkop",
|
command: "/usr/bin/podkop",
|
||||||
args: ["get_sing_box_status"],
|
args: ["get_sing_box_status"],
|
||||||
timeout: 1e3
|
timeout: 1e4
|
||||||
});
|
});
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
||||||
@@ -1207,6 +1248,7 @@ var initialStore = {
|
|||||||
sectionsWidget: {
|
sectionsWidget: {
|
||||||
loading: true,
|
loading: true,
|
||||||
failed: false,
|
failed: false,
|
||||||
|
latencyFetching: false,
|
||||||
data: []
|
data: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1232,7 +1274,10 @@ function renderFailedState() {
|
|||||||
class: "pdk_dashboard-page__outbound-section centered",
|
class: "pdk_dashboard-page__outbound-section centered",
|
||||||
style: "height: 127px"
|
style: "height: 127px"
|
||||||
},
|
},
|
||||||
E("span", {}, _("Dashboard currently unavailable"))
|
E("span", {}, [
|
||||||
|
E("span", {}, _("Dashboard currently unavailable")),
|
||||||
|
E("div", { style: "text-align: center;" }, `API: ${getClashApiUrl()}`)
|
||||||
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function renderLoadingState() {
|
function renderLoadingState() {
|
||||||
@@ -1245,7 +1290,8 @@ function renderLoadingState() {
|
|||||||
function renderDefaultState({
|
function renderDefaultState({
|
||||||
section,
|
section,
|
||||||
onChooseOutbound,
|
onChooseOutbound,
|
||||||
onTestLatency
|
onTestLatency,
|
||||||
|
latencyFetching
|
||||||
}) {
|
}) {
|
||||||
function testLatency() {
|
function testLatency() {
|
||||||
if (section.withTagSelect) {
|
if (section.withTagSelect) {
|
||||||
@@ -1260,10 +1306,10 @@ function renderDefaultState({
|
|||||||
if (!outbound.latency) {
|
if (!outbound.latency) {
|
||||||
return "pdk_dashboard-page__outbound-grid__item__latency--empty";
|
return "pdk_dashboard-page__outbound-grid__item__latency--empty";
|
||||||
}
|
}
|
||||||
if (outbound.latency < 200) {
|
if (outbound.latency < 800) {
|
||||||
return "pdk_dashboard-page__outbound-grid__item__latency--green";
|
return "pdk_dashboard-page__outbound-grid__item__latency--green";
|
||||||
}
|
}
|
||||||
if (outbound.latency < 400) {
|
if (outbound.latency < 1500) {
|
||||||
return "pdk_dashboard-page__outbound-grid__item__latency--yellow";
|
return "pdk_dashboard-page__outbound-grid__item__latency--yellow";
|
||||||
}
|
}
|
||||||
return "pdk_dashboard-page__outbound-grid__item__latency--red";
|
return "pdk_dashboard-page__outbound-grid__item__latency--red";
|
||||||
@@ -1301,7 +1347,7 @@ function renderDefaultState({
|
|||||||
},
|
},
|
||||||
section.displayName
|
section.displayName
|
||||||
),
|
),
|
||||||
E(
|
latencyFetching ? E("div", { class: "skeleton", style: "width: 99px; height: 28px" }) : E(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
class: "btn dashboard-sections-grid-item-test-latency",
|
class: "btn dashboard-sections-grid-item-test-latency",
|
||||||
@@ -1568,8 +1614,12 @@ async function fetchDashboardSections() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const { data, success } = await getDashboardSections();
|
const { data, success } = await getDashboardSections();
|
||||||
|
if (!success) {
|
||||||
|
console.log("[fetchDashboardSections]: failed to fetch", getClashApiUrl());
|
||||||
|
}
|
||||||
store.set({
|
store.set({
|
||||||
sectionsWidget: {
|
sectionsWidget: {
|
||||||
|
latencyFetching: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
failed: !success,
|
failed: !success,
|
||||||
data
|
data
|
||||||
@@ -1577,17 +1627,28 @@ async function fetchDashboardSections() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const [podkop, singbox] = await Promise.all([
|
try {
|
||||||
getPodkopStatus(),
|
const [podkop, singbox] = await Promise.all([
|
||||||
getSingboxStatus()
|
getPodkopStatus(),
|
||||||
]);
|
getSingboxStatus()
|
||||||
store.set({
|
]);
|
||||||
servicesInfoWidget: {
|
store.set({
|
||||||
loading: false,
|
servicesInfoWidget: {
|
||||||
failed: false,
|
loading: false,
|
||||||
data: { singbox: singbox.running, podkop: podkop.enabled }
|
failed: false,
|
||||||
}
|
data: { singbox: singbox.running, podkop: podkop.enabled }
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[fetchServicesInfo]: failed to fetchServices", err);
|
||||||
|
store.set({
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { singbox: 0, podkop: 0 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function connectToClashSockets() {
|
async function connectToClashSockets() {
|
||||||
socket.subscribe(
|
socket.subscribe(
|
||||||
@@ -1603,6 +1664,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
"[fetchDashboardSections]: failed to connect",
|
||||||
|
getClashWsUrl()
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
bandwidthWidget: {
|
bandwidthWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -1636,6 +1701,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
"[fetchDashboardSections]: failed to connect",
|
||||||
|
getClashWsUrl()
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
trafficTotalWidget: {
|
trafficTotalWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -1659,20 +1728,35 @@ async function handleChooseOutbound(selector, tag) {
|
|||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
}
|
}
|
||||||
async function handleTestGroupLatency(tag) {
|
async function handleTestGroupLatency(tag) {
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
...store.get().sectionsWidget,
|
||||||
|
latencyFetching: true
|
||||||
|
}
|
||||||
|
});
|
||||||
await triggerLatencyGroupTest(tag);
|
await triggerLatencyGroupTest(tag);
|
||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
...store.get().sectionsWidget,
|
||||||
|
latencyFetching: false
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async function handleTestProxyLatency(tag) {
|
async function handleTestProxyLatency(tag) {
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
...store.get().sectionsWidget,
|
||||||
|
latencyFetching: true
|
||||||
|
}
|
||||||
|
});
|
||||||
await triggerLatencyProxyTest(tag);
|
await triggerLatencyProxyTest(tag);
|
||||||
await fetchDashboardSections();
|
await fetchDashboardSections();
|
||||||
}
|
store.set({
|
||||||
function replaceTestLatencyButtonsWithSkeleton() {
|
sectionsWidget: {
|
||||||
document.querySelectorAll(".dashboard-sections-grid-item-test-latency").forEach((el) => {
|
...store.get().sectionsWidget,
|
||||||
const newDiv = document.createElement("div");
|
latencyFetching: false
|
||||||
newDiv.className = "skeleton";
|
}
|
||||||
newDiv.style.width = "99px";
|
|
||||||
newDiv.style.height = "28px";
|
|
||||||
el.replaceWith(newDiv);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function renderSectionsWidget() {
|
async function renderSectionsWidget() {
|
||||||
@@ -1692,17 +1776,20 @@ async function renderSectionsWidget() {
|
|||||||
onTestLatency: () => {
|
onTestLatency: () => {
|
||||||
},
|
},
|
||||||
onChooseOutbound: () => {
|
onChooseOutbound: () => {
|
||||||
}
|
},
|
||||||
|
latencyFetching: sectionsWidget.latencyFetching
|
||||||
|
});
|
||||||
|
return preserveScrollForPage(() => {
|
||||||
|
container.replaceChildren(renderedWidget);
|
||||||
});
|
});
|
||||||
return container.replaceChildren(renderedWidget);
|
|
||||||
}
|
}
|
||||||
const renderedWidgets = sectionsWidget.data.map(
|
const renderedWidgets = sectionsWidget.data.map(
|
||||||
(section) => renderSections({
|
(section) => renderSections({
|
||||||
loading: sectionsWidget.loading,
|
loading: sectionsWidget.loading,
|
||||||
failed: sectionsWidget.failed,
|
failed: sectionsWidget.failed,
|
||||||
section,
|
section,
|
||||||
|
latencyFetching: sectionsWidget.latencyFetching,
|
||||||
onTestLatency: (tag) => {
|
onTestLatency: (tag) => {
|
||||||
replaceTestLatencyButtonsWithSkeleton();
|
|
||||||
if (section.withTagSelect) {
|
if (section.withTagSelect) {
|
||||||
return handleTestGroupLatency(tag);
|
return handleTestGroupLatency(tag);
|
||||||
}
|
}
|
||||||
@@ -1713,7 +1800,9 @@ async function renderSectionsWidget() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return container.replaceChildren(...renderedWidgets);
|
return preserveScrollForPage(() => {
|
||||||
|
container.replaceChildren(...renderedWidgets);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async function renderBandwidthWidget() {
|
async function renderBandwidthWidget() {
|
||||||
console.log("renderBandwidthWidget");
|
console.log("renderBandwidthWidget");
|
||||||
@@ -1877,6 +1966,7 @@ return baseclass.extend({
|
|||||||
FAKEIP_CHECK_DOMAIN,
|
FAKEIP_CHECK_DOMAIN,
|
||||||
FETCH_TIMEOUT,
|
FETCH_TIMEOUT,
|
||||||
IP_CHECK_DOMAIN,
|
IP_CHECK_DOMAIN,
|
||||||
|
PODKOP_LUCI_APP_VERSION,
|
||||||
REGIONAL_OPTIONS,
|
REGIONAL_OPTIONS,
|
||||||
STATUS_COLORS,
|
STATUS_COLORS,
|
||||||
TabService,
|
TabService,
|
||||||
@@ -1891,6 +1981,7 @@ return baseclass.extend({
|
|||||||
getClashConfig,
|
getClashConfig,
|
||||||
getClashGroupDelay,
|
getClashGroupDelay,
|
||||||
getClashProxies,
|
getClashProxies,
|
||||||
|
getClashUIUrl,
|
||||||
getClashVersion,
|
getClashVersion,
|
||||||
getClashWsUrl,
|
getClashWsUrl,
|
||||||
getConfigSections,
|
getConfigSections,
|
||||||
@@ -1902,7 +1993,9 @@ return baseclass.extend({
|
|||||||
injectGlobalStyles,
|
injectGlobalStyles,
|
||||||
maskIP,
|
maskIP,
|
||||||
onMount,
|
onMount,
|
||||||
|
parseQueryString,
|
||||||
parseValueList,
|
parseValueList,
|
||||||
|
preserveScrollForPage,
|
||||||
renderDashboard,
|
renderDashboard,
|
||||||
splitProxyString,
|
splitProxyString,
|
||||||
triggerLatencyGroupTest,
|
triggerLatencyGroupTest,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=podkop
|
PKG_NAME:=podkop
|
||||||
|
|
||||||
PKG_VERSION := $(if $(PKG_VERSION),$(PKG_VERSION),dev_$(shell date +%d%m%Y))
|
PKG_VERSION := $(if $(PODKOP_VERSION),$(PODKOP_VERSION),0.$(shell date +%d%m%Y))
|
||||||
|
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@ endef
|
|||||||
define Package/podkop/install
|
define Package/podkop/install
|
||||||
$(INSTALL_DIR) $(1)/etc/init.d
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
$(INSTALL_BIN) ./files/etc/init.d/podkop $(1)/etc/init.d/podkop
|
$(INSTALL_BIN) ./files/etc/init.d/podkop $(1)/etc/init.d/podkop
|
||||||
sed -i "s/VERSION_FROM_MAKEFILE/$(PKG_VERSION)/g" $(1)/etc/init.d/podkop
|
|
||||||
|
|
||||||
$(INSTALL_DIR) $(1)/etc/config
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
$(INSTALL_CONF) ./files/etc/config/podkop $(1)/etc/config/podkop
|
$(INSTALL_CONF) ./files/etc/config/podkop $(1)/etc/config/podkop
|
||||||
@@ -58,6 +57,8 @@ define Package/podkop/install
|
|||||||
|
|
||||||
$(INSTALL_DIR) $(1)/usr/lib/podkop
|
$(INSTALL_DIR) $(1)/usr/lib/podkop
|
||||||
$(CP) ./files/usr/lib/* $(1)/usr/lib/podkop/
|
$(CP) ./files/usr/lib/* $(1)/usr/lib/podkop/
|
||||||
|
|
||||||
|
sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' $(1)/usr/lib/podkop/constants.sh
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(eval $(call BuildPackage,podkop))
|
$(eval $(call BuildPackage,podkop))
|
||||||
|
|||||||
@@ -1784,13 +1784,7 @@ show_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_version() {
|
show_version() {
|
||||||
local version=$(opkg list-installed podkop | awk '{print $3}')
|
echo "$PODKOP_VERSION"
|
||||||
echo "$version"
|
|
||||||
}
|
|
||||||
|
|
||||||
show_luci_version() {
|
|
||||||
local version=$(opkg list-installed luci-app-podkop | awk '{print $3}')
|
|
||||||
echo "$version"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show_sing_box_version() {
|
show_sing_box_version() {
|
||||||
@@ -1967,11 +1961,14 @@ find_working_resolver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
global_check() {
|
global_check() {
|
||||||
|
local PODKOP_LUCI_VERSION="Unknown"
|
||||||
|
[ -n "$1" ] && PODKOP_LUCI_VERSION="$1"
|
||||||
|
|
||||||
print_global "📡 Global check run!"
|
print_global "📡 Global check run!"
|
||||||
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
print_global "🛠️ System info"
|
print_global "🛠️ System info"
|
||||||
print_global "🕳️ Podkop: $(opkg list-installed podkop | awk '{print $3}')"
|
print_global "🕳️ Podkop: ${PODKOP_VERSION}"
|
||||||
print_global "🕳️ LuCI App: $(opkg list-installed luci-app-podkop | awk '{print $3}')"
|
print_global "🕳️ LuCI App: ${PODKOP_LUCI_VERSION}"
|
||||||
print_global "📦 Sing-box: $(sing-box version | head -n 1 | awk '{print $3}')"
|
print_global "📦 Sing-box: $(sing-box version | head -n 1 | awk '{print $3}')"
|
||||||
print_global "🛜 OpenWrt: $(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2)"
|
print_global "🛜 OpenWrt: $(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2)"
|
||||||
print_global "🛜 Device: $(cat /tmp/sysinfo/model)"
|
print_global "🛜 Device: $(cat /tmp/sysinfo/model)"
|
||||||
@@ -2133,7 +2130,6 @@ Available commands:
|
|||||||
show_config Display current podkop configuration
|
show_config Display current podkop configuration
|
||||||
show_version Show podkop version
|
show_version Show podkop version
|
||||||
show_sing_box_config Show sing-box configuration
|
show_sing_box_config Show sing-box configuration
|
||||||
show_luci_version Show LuCI app version
|
|
||||||
show_sing_box_version Show sing-box version
|
show_sing_box_version Show sing-box version
|
||||||
show_system_info Show system information
|
show_system_info Show system information
|
||||||
get_status Get podkop service status
|
get_status Get podkop service status
|
||||||
@@ -2192,9 +2188,6 @@ show_version)
|
|||||||
show_sing_box_config)
|
show_sing_box_config)
|
||||||
show_sing_box_config
|
show_sing_box_config
|
||||||
;;
|
;;
|
||||||
show_luci_version)
|
|
||||||
show_luci_version
|
|
||||||
;;
|
|
||||||
show_sing_box_version)
|
show_sing_box_version)
|
||||||
show_sing_box_version
|
show_sing_box_version
|
||||||
;;
|
;;
|
||||||
@@ -2211,10 +2204,10 @@ check_dns_available)
|
|||||||
check_dns_available
|
check_dns_available
|
||||||
;;
|
;;
|
||||||
global_check)
|
global_check)
|
||||||
global_check
|
global_check "${2:-}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
show_help
|
show_help
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
|
|
||||||
|
PODKOP_VERSION="__COMPILED_VERSION_VARIABLE__"
|
||||||
## Common
|
## Common
|
||||||
PODKOP_CONFIG="/etc/config/podkop"
|
PODKOP_CONFIG="/etc/config/podkop"
|
||||||
RESOLV_CONF="/etc/resolv.conf"
|
RESOLV_CONF="/etc/resolv.conf"
|
||||||
|
|||||||
7
sdk/Dockerfile-sdk-apk
Normal file
7
sdk/Dockerfile-sdk-apk
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM openwrt/sdk:x86-64-SNAPSHOT
|
||||||
|
WORKDIR /builder
|
||||||
|
RUN ./setup.sh \
|
||||||
|
&& ./scripts/feeds update -a \
|
||||||
|
&& ./scripts/feeds install luci-base \
|
||||||
|
&& mkdir -p /builder/package/feeds/utilities/ \
|
||||||
|
&& mkdir -p /builder/package/feeds/luci/
|
||||||
6
sdk/Dockerfile-sdk-ipk
Normal file
6
sdk/Dockerfile-sdk-ipk
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM openwrt/sdk:x86_64-v24.10.3
|
||||||
|
WORKDIR /builder
|
||||||
|
RUN ./scripts/feeds update -a \
|
||||||
|
&& ./scripts/feeds install luci-base \
|
||||||
|
&& mkdir -p /builder/package/feeds/utilities/ \
|
||||||
|
&& mkdir -p /builder/package/feeds/luci/
|
||||||
Reference in New Issue
Block a user