Compare commits

...

36 Commits

Author SHA1 Message Date
Kirill Sobakin
984ae5f2a9 Merge pull request #213 from itdoginfo/fix/diagnostic
fix: correct luci-version displaying
2025-10-10 14:46:59 +03:00
divocat
7a62898541 fix: correct luci-version displaying 2025-10-10 14:41:35 +03:00
itdoginfo
7911d1d29f Draft false 2025-10-10 14:23:37 +03:00
Kirill Sobakin
bc673b7881 Merge pull request #212 from itdoginfo/fix/dashboard
fix: correct vless/trojan validation on some browsers
2025-10-10 14:21:25 +03:00
divocat
0493565c5f fix: implement query params parsing func 2025-10-10 14:06:19 +03:00
itdoginfo
4cd1094395 Check ipk without v 2025-10-09 19:53:37 +03:00
itdoginfo
e87b431d86 Check v* 2025-10-09 19:33:27 +03:00
itdoginfo
b9ee917abf Fix PODKOP_VERSION for ipk 2025-10-09 19:20:35 +03:00
divocat
715a278af8 fix: force http for yacd enable link 2025-10-09 18:23:35 +03:00
divocat
9bc2b5ffef fix: correct link validation & some points on dash 2025-10-09 18:23:35 +03:00
divocat
9d89258c0c fix: correct vless/trojan validation on some browsers 2025-10-09 18:23:35 +03:00
itdoginfo
52d1c5d95f Fix PKG_VERSION -> PODKOP_VERSION 2025-10-09 18:15:54 +03:00
itdoginfo
587e5245d3 Test without v* 2025-10-09 16:01:31 +03:00
itdoginfo
e7578d61bc Rm v* 2025-10-09 15:34:23 +03:00
itdoginfo
9918b71a82 Fix version tag 3 2025-10-09 15:32:01 +03:00
itdoginfo
f48c4ff2bb Fix version tag 2 2025-10-09 15:08:37 +03:00
itdoginfo
e77bcc386a Fix version tag 2025-10-09 14:59:22 +03:00
itdoginfo
455c19ab2e Fix #211 2025-10-09 14:40:45 +03:00
Kirill Sobakin
914e1792f3 Merge pull request #211 from SaltyMonkey/build-process-improvements
chore: Build process improvements
2025-10-09 11:13:59 +03:00
SaltyMonkey
826245a89a fix: Minor changes and bugfixes, ci fix 2025-10-09 00:56:15 +03:00
SaltyMonkey
b5cfc017fe chore: Automatic build process rewrite
* Added apk packages support
* Move to matrix builds
* Minor versions update for some actions just in case
* Automatic release with ipk/apk packages
2025-10-08 23:15:52 +03:00
SaltyMonkey
267fd2b793 refactor: Added .gitattributes for better dev life at win and linux 2025-10-08 22:26:16 +03:00
SaltyMonkey
c0b400dfb0 refactor: New docker files for build process 2025-10-08 22:25:06 +03:00
SaltyMonkey
752636347e refactor: Remove old docker files 2025-10-08 22:20:33 +03:00
SaltyMonkey
28aeb29c51 refactor: Update luci-app-podkop package
* Removed direct package manager calls
* Removed commands related to optional luci package
* Update external global_check call with version pass
* Removed useless external calls in version check cases
* Improved build process support: version will be automatically set at installation time from package metadata and will be readable from JS as constant
2025-10-08 22:09:48 +03:00
SaltyMonkey
6ff543d7fb refactor: Update podkop package
* Removed direct package manager calls
* Removed commands related to optional luci package
* Added optional parameter for global_check for cases when function called by LuCI package
* Removed useless external calls in version check cases
* Improved build process support: version will be automatically set at installation time from package metadata and will be readable from script itself
2025-10-08 21:57:46 +03:00
SaltyMonkey
b89fe33296 chore: Added apk package manager support for install script 2025-10-08 21:42:19 +03:00
Kirill Sobakin
3d63a82815 Merge pull request #209 from itdoginfo/fix/dashboard
fix: force http for clash api
2025-10-08 00:05:04 +03:00
divocat
934f802879 fix: force http for clash api 2025-10-08 00:02:41 +03:00
Kirill Sobakin
4d0755e4c0 Merge pull request #208 from itdoginfo/fix/dashboard
feat: change get latency class coloring
2025-10-07 23:48:29 +03:00
divocat
88ee7b4a54 feat: change get latency class coloring 2025-10-07 23:45:07 +03:00
Kirill Sobakin
0eb575d171 Merge pull request #207 from itdoginfo/fix/dashboard
fix: dashboard behavior on corner cases
2025-10-07 23:41:06 +03:00
divocat
9a46d731c9 fix: correct dashboard displaying 2025-10-07 23:33:57 +03:00
divocat
a45ab62885 fix: correct section display name for json outbound 2025-10-07 22:56:40 +03:00
Kirill Sobakin
b7bad57299 Merge pull request #206 from itdoginfo/feat/all_traffic_ip_cidr
feat: add support IP/CIDR format in LuCI for all_traffic_ip
2025-10-07 22:33:43 +03:00
divocat
4ac755bd36 feat: add support IP/CIDR format in LuCI for all_traffic_ip 2025-10-07 22:26:29 +03:00
32 changed files with 948 additions and 412 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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
View 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
View 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

View File

@@ -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';

View File

@@ -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`;
}

View File

@@ -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';

View 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>,
);
}

View File

@@ -0,0 +1,9 @@
export function preserveScrollForPage(renderFn: () => void) {
const scrollY = window.scrollY;
renderFn();
requestAnimationFrame(() => {
window.scrollTo({ top: scrollY });
});
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,6 +49,7 @@ async function fetchDashboardSections() {
} }
async function fetchServicesInfo() { async function fetchServicesInfo() {
try {
const [podkop, singbox] = await Promise.all([ const [podkop, singbox] = await Promise.all([
getPodkopStatus(), getPodkopStatus(),
getSingboxStatus(), getSingboxStatus(),
@@ -51,6 +62,17 @@ async function fetchServicesInfo() {
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,24 +160,40 @@ 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);
}); });
} }
@@ -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() {

View File

@@ -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', {}, [
E('span', {}, _('Dashboard currently unavailable')), 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,7 +101,9 @@ export 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',

View File

@@ -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: [],
}, },
}; };

View File

@@ -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);
});
});

View 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);
});
});

View File

@@ -1,7 +1,7 @@
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 {
try {
if (!url.startsWith('trojan://')) { if (!url.startsWith('trojan://')) {
return { return {
valid: false, valid: false,
@@ -16,17 +16,42 @@ export function validateTrojanUrl(url: string): ValidationResult {
}; };
} }
try { const body = url.slice('trojan://'.length);
const parsedUrl = new URL(url); const [mainPart] = body.split('#');
const [userHostPort] = mainPart.split('?');
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { const [userPart, hostPortPart] = userHostPort.split('@');
if (!userHostPort)
return { return {
valid: false, valid: false,
message: _( message: 'Invalid Trojan URL: missing credentials and host',
'Invalid Trojan URL: must contain username, hostname and port', };
),
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') };
} }

View File

@@ -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 { return {
valid: false, valid: false,
message: _( message: 'Invalid VLESS URL: unsupported or missing security',
'Invalid VLESS URL: missing pbk parameter for reality security',
),
}; };
}
if (!params.get('fp')) { 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 fp parameter for reality security', };
), if (!params.fp)
return {
valid: false,
message: 'Invalid VLESS URL: missing fp for reality',
}; };
}
} }
return { valid: true, message: _('Valid') }; return { valid: true, message: _('Valid') };

View File

@@ -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,13 +15,58 @@ 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..."
@@ -34,6 +83,13 @@ 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")
@@ -57,7 +113,7 @@ main() {
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"
@@ -68,25 +124,25 @@ main() {
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,7 +223,7 @@ 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
} }

View File

@@ -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)))

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(
'/usr/bin/podkop',
['show_luci_version'],
'P2_PRIORITY',
(result) => {
updateTextElement( updateTextElement(
'luci-version', 'luci-version',
document.createTextNode( document.createTextNode(
result.stdout ? result.stdout.trim() : _('Unknown'), `${main.PODKOP_LUCI_APP_VERSION}`
), )
);
},
); );
safeExec( safeExec(

View File

@@ -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,6 +1627,7 @@ async function fetchDashboardSections() {
}); });
} }
async function fetchServicesInfo() { async function fetchServicesInfo() {
try {
const [podkop, singbox] = await Promise.all([ const [podkop, singbox] = await Promise.all([
getPodkopStatus(), getPodkopStatus(),
getSingboxStatus() getSingboxStatus()
@@ -1588,6 +1639,16 @@ async function fetchServicesInfo() {
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() {
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({
sectionsWidget: {
...store.get().sectionsWidget,
latencyFetching: false
} }
function replaceTestLatencyButtonsWithSkeleton() {
document.querySelectorAll(".dashboard-sections-grid-item-test-latency").forEach((el) => {
const newDiv = document.createElement("div");
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,

View File

@@ -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))

View File

@@ -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,7 +2204,7 @@ check_dns_available)
check_dns_available check_dns_available
;; ;;
global_check) global_check)
global_check global_check "${2:-}"
;; ;;
*) *)
show_help show_help

View File

@@ -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
View 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
View 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/