Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions server/http/controller_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,31 @@ func matchV2ClientQuery(item model.ClientInfoResp, q string) bool {
}

func matchV2ProxyQuery(item model.V2ProxyResp, q string) bool {
return containsV2Query(q,
values := []string{
item.Name,
item.Type,
item.User,
item.ClientID,
item.Status.State,
)
}

switch spec := item.Spec.(type) {
case *model.TCPOutConf:
values = append(values, strconv.Itoa(spec.RemotePort))
Comment thread
fatedier marked this conversation as resolved.
case *model.UDPOutConf:
values = append(values, strconv.Itoa(spec.RemotePort))
case *model.HTTPOutConf:
values = append(values, spec.CustomDomains...)
values = append(values, spec.SubDomain)
case *model.HTTPSOutConf:
values = append(values, spec.CustomDomains...)
values = append(values, spec.SubDomain)
case *model.TCPMuxOutConf:
values = append(values, spec.CustomDomains...)
values = append(values, spec.SubDomain)
}

return containsV2Query(q, values...)
}

func containsV2Query(q string, values ...string) bool {
Expand Down
80 changes: 80 additions & 0 deletions server/http/controller_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,86 @@ func TestAPIV2ProxyListDetailAndUsers(t *testing.T) {
}
}

func TestMatchV2ProxyQueryMatchesSpecFields(t *testing.T) {
tests := []struct {
name string
item model.V2ProxyResp
q string
want bool
}{
{
name: "tcp remote port",
item: model.V2ProxyResp{Name: "tcp-proxy", Type: "tcp", Spec: &model.TCPOutConf{
RemotePort: 6000,
}},
q: "6000",
want: true,
},
{
name: "udp remote port",
item: model.V2ProxyResp{Name: "udp-proxy", Type: "udp", Spec: &model.UDPOutConf{
RemotePort: 7000,
}},
q: "7000",
want: true,
},
{
name: "remote port does not match colon form",
item: model.V2ProxyResp{Name: "tcp-proxy", Type: "tcp", Spec: &model.TCPOutConf{
RemotePort: 6000,
}},
q: ":6000",
want: false,
},
{
name: "http custom domain",
item: model.V2ProxyResp{Name: "http-proxy", Type: "http", Spec: &model.HTTPOutConf{
DomainConfig: v1.DomainConfig{CustomDomains: []string{"app.example.com"}},
}},
q: "app.example.com",
want: true,
},
{
name: "https subdomain",
item: model.V2ProxyResp{Name: "https-proxy", Type: "https", Spec: &model.HTTPSOutConf{
DomainConfig: v1.DomainConfig{SubDomain: "portal"},
}},
q: "portal",
want: true,
},
{
name: "subdomain does not match expanded host",
item: model.V2ProxyResp{Name: "https-proxy", Type: "https", Spec: &model.HTTPSOutConf{
DomainConfig: v1.DomainConfig{SubDomain: "portal"},
}},
q: "portal.example.com",
want: false,
},
{
name: "tcpmux custom domain",
item: model.V2ProxyResp{Name: "tcpmux-proxy", Type: "tcpmux", Spec: &model.TCPMuxOutConf{
DomainConfig: v1.DomainConfig{CustomDomains: []string{"mux.example.com"}},
}},
q: "mux.example.com",
want: true,
},
{
name: "nil spec does not match spec fields",
item: model.V2ProxyResp{Name: "offline-proxy", Type: "tcp", Spec: nil},
q: "6000",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := matchV2ProxyQuery(tt.item, tt.q); got != tt.want {
t.Fatalf("matchV2ProxyQuery() = %v, want %v", got, tt.want)
}
})
}
}

func TestLegacyAPIResponsesRemainBare(t *testing.T) {
controller := newV2TestController(t)
router := newV2TestRouter(controller)
Expand Down
20 changes: 18 additions & 2 deletions web/frps/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { http } from './http'
import type { ClientInfoData } from '../types/client'
import { buildQueryString, http } from './http'
import type { V2Page } from './http'
import type { ClientInfoData, ClientListV2Params } from '../types/client'

export const getClients = () => {
return http.get<ClientInfoData[]>('../api/clients')
}

export const getClientsV2 = (params: ClientListV2Params = {}) => {
return http.getV2<V2Page<ClientInfoData>>(
`../api/v2/clients${buildQueryString({
page: params.page,
pageSize: params.pageSize,
status:
params.status && params.status !== 'all' ? params.status : undefined,
q: params.q || undefined,
user: params.user,
clientID: params.clientID || undefined,
runID: params.runID || undefined,
})}`,
)
}

export const getClient = (key: string) => {
return http.get<ClientInfoData>(`../api/clients/${key}`)
}
61 changes: 61 additions & 0 deletions web/frps/src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ class HTTPError extends Error {
}
}

export interface V2Envelope<T> {
code: number
msg: string
data: T
}

export interface V2Page<T> {
total: number
page: number
pageSize: number
items: T[]
}

type QueryParamValue = string | number | boolean | null | undefined

async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
const defaultOptions: RequestInit = {
credentials: 'include',
Expand All @@ -34,9 +49,55 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
return response.json()
}

async function requestV2<T>(
url: string,
options: RequestInit = {},
): Promise<T> {
const defaultOptions: RequestInit = {
credentials: 'include',
}

const response = await fetch(url, { ...defaultOptions, ...options })
const envelope = (await response.json().catch(() => null)) as
| V2Envelope<T>
| null

if (!response.ok) {
throw new HTTPError(
response.status,
response.statusText,
envelope?.msg || `HTTP ${response.status}`,
)
}

if (!envelope || typeof envelope.code !== 'number') {
throw new Error('Invalid API v2 response')
}

if (envelope.code >= 400) {
throw new HTTPError(envelope.code, envelope.msg, envelope.msg)
}

return envelope.data
}

export const buildQueryString = (
params: Record<string, QueryParamValue>,
): string => {
const query = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value === null || value === undefined) continue
query.append(key, String(value))
}
const text = query.toString()
return text ? `?${text}` : ''
}

export const http = {
get: <T>(url: string, options?: RequestInit) =>
request<T>(url, { ...options, method: 'GET' }),
getV2: <T>(url: string, options?: RequestInit) =>
requestV2<T>(url, { ...options, method: 'GET' }),
post: <T>(url: string, body?: any, options?: RequestInit) =>
request<T>(url, {
...options,
Expand Down
39 changes: 38 additions & 1 deletion web/frps/src/api/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
import { http } from './http'
import { buildQueryString, http } from './http'
import type { V2Page } from './http'
import type {
GetProxyResponse,
ProxyListV2Params,
ProxyStatsInfo,
ProxyV2Info,
TrafficResponse,
} from '../types/proxy'

export const getProxiesByType = (type: string) => {
return http.get<GetProxyResponse>(`../api/proxy/${type}`)
}

export const getProxiesV2 = async (params: ProxyListV2Params = {}) => {
const page = await http.getV2<V2Page<ProxyV2Info>>(
`../api/v2/proxies${buildQueryString({
page: params.page,
pageSize: params.pageSize,
status:
params.status && params.status !== 'all' ? params.status : undefined,
q: params.q || undefined,
type: params.type || undefined,
user: params.user,
clientID: params.clientID || undefined,
})}`,
)

return {
...page,
items: page.items.map(toLegacyProxyStats),
}
}

const toLegacyProxyStats = (proxy: ProxyV2Info): ProxyStatsInfo => ({
name: proxy.name,
type: proxy.type,
conf: proxy.spec,
user: proxy.user,
clientID: proxy.clientID,
todayTrafficIn: proxy.status.todayTrafficIn,
todayTrafficOut: proxy.status.todayTrafficOut,
curConns: proxy.status.curConns,
lastStartTime: proxy.status.lastStartTime,
lastCloseTime: proxy.status.lastCloseTime,
status: proxy.status.phase,
})

export const getProxy = (type: string, name: string) => {
return http.get<ProxyStatsInfo>(`../api/proxy/${type}/${name}`)
}
Expand Down
10 changes: 10 additions & 0 deletions web/frps/src/types/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ export interface ClientInfoData {
disconnectedAt?: number
online: boolean
}

export interface ClientListV2Params {
page?: number
pageSize?: number
status?: 'all' | 'online' | 'offline'
q?: string
user?: string
clientID?: string
runID?: string
}
29 changes: 29 additions & 0 deletions web/frps/src/types/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface ProxyStatsInfo {
name: string
type?: string
conf: any
user: string
clientID: string
Expand All @@ -15,6 +16,34 @@ export interface GetProxyResponse {
proxies: ProxyStatsInfo[]
}

export interface ProxyListV2Params {
page?: number
pageSize?: number
status?: 'all' | 'online' | 'offline'
q?: string
type?: string
user?: string
clientID?: string
}

export interface ProxyV2Info {
name: string
type: string
user: string
clientID: string
spec: any
status: ProxyV2Status
}

export interface ProxyV2Status {
phase: string
todayTrafficIn: number
todayTrafficOut: number
curConns: number
lastStartTime: string
lastCloseTime: string
}

export interface TrafficResponse {
name: string
trafficIn: number[]
Expand Down
Loading