admin/internet: internet reachability test
Massimo Melina committed
Aug 17, 2023 at 19:24 UTC
830159b438a44ac12485daa4bed7b86fbe2b19b9
2 files changed
+51
-32
admin/src/InternetPage.ts
+38
-19
@@ -1,38 +1,57 @@
1
-import { createElement as h } from 'react'
2
-import { Box, Button, Link } from '@mui/material'
1
+import { createElement as h, useState } from 'react'
2
+import { Alert, Box, Button, CircularProgress, LinearProgress, Link } from '@mui/material'
3
import { HomeWorkTwoTone, PublicTwoTone, RouterTwoTone } from '@mui/icons-material'
4
import { apiCall, useApiEx } from './api'
5
import { closeDialog, with_ } from '@hfs/shared'
6
import { Flex } from './misc'
7
-import { confirmDialog, promptDialog, toast, waitDialog } from './dialog'
7
+import { alertDialog, confirmDialog, promptDialog, toast, waitDialog } from './dialog'
8
import { NumberField } from '@hfs/mui-grid-form'
9
import md from './md'
10
11
export default function InternetPage() {
12
+ const [checkResult, setCheckResult] = useState<boolean | undefined>()
13
+ const [checking, setChecking] = useState(false)
14
const { data: status } = useApiEx('get_status')
15
const localColor = with_([status?.http?.error, status?.https?.error], ([h, s]) =>
16
h && s ? 'error' : h || s ? 'warning' : 'success')
15
- const { data: nat, reload } = useApiEx('get_nat')
16
- const port = nat?.port
17
+ const { data: nat, reload, error } = useApiEx('get_nat')
18
+ const port = nat?.internalPort
19
const wrongMap = nat?.mapped && nat.mapped.private.port !== port
20
return h(Box, {},
21
h(Box, { mb: 2 }, "This page helps you making your server work on the internet"),
20
- h(Flex, { justifyContent: 'space-around', alignItems: 'center', maxWidth: '40em' },
21
- h(Device, { name: "Local network", icon: HomeWorkTwoTone, color: localColor, ip: nat?.local_ip,
22
- below: port && h(Box, { fontSize: 'smaller' }, "port ", port),
23
- }),
24
- h(Sep),
25
- h(Device, {
26
- name: "Router", icon: RouterTwoTone, ip: nat?.gateway_ip,
27
- color: nat?.mapped && (wrongMap ? 'warning' : 'success'),
28
- below: h(Link, { fontSize: 'smaller', display: 'block', onClick: configure, sx: { cursor: 'pointer' } },
29
- "port ", wrongMap ? 'is wrong' : nat?.mapped ? nat.mapped.public.port : "unknown"),
30
- }),
31
- h(Sep),
32
- h(Device, { name: "Internet", icon: PublicTwoTone, ip: nat?.public_ip }),
33
- ),
22
+ error ? h(Alert, { severity: 'warning' }, "Cannot analyze network because UPnP unavailable")
23
+ : !nat ? h(CircularProgress)
24
+ : h(Flex, { justifyContent: 'space-around', alignItems: 'center', maxWidth: '40em' },
25
+ h(Device, { name: "Local network", icon: HomeWorkTwoTone, color: localColor, ip: nat?.localIp,
26
+ below: port && h(Box, { fontSize: 'smaller' }, "port ", port),
27
+ }),
28
+ h(Sep),
29
+ h(Device, {
30
+ name: "Router", icon: RouterTwoTone, ip: nat?.gatewayIp,
31
+ color: nat?.mapped && (wrongMap ? 'warning' : 'success'),
32
+ below: h(Link, { fontSize: 'smaller', display: 'block', onClick: configure, sx: { cursor: 'pointer' } },
33
+ "port ", wrongMap ? 'is wrong' : nat?.mapped ? nat.mapped.public.port : "unknown"),
34
+ }),
35
+ h(Sep),
36
+ h(Device, { name: "Internet", icon: PublicTwoTone, ip: nat?.publicIp,
37
+ color: checkResult === undefined ? undefined : checkResult ? 'success' : 'warning',
38
+ below: checking ? h(LinearProgress, { sx: { height: '1em' } }) : h(Box, { fontSize: 'smaller' }, checkResult ? "Working!" : checkResult === false ? "Failed!" : '',
39
+ ' ',
40
+ h(Link, { onClick: verify, sx: { cursor: 'pointer' } }, "Verify")
41
+ )
42
+ }),
43
+ ),
44
)
45
46
+ async function verify() {
47
+ setChecking(true)
48
+ const { success } = await apiCall('check_server', {}).finally(() => setChecking(false))
49
+ setCheckResult(success)
50
+ if (success)
51
+ return toast("Your server is responding correctly over the internet", 'success')
52
+ alertDialog("We couldn't reach your server from the internet", 'warning')
53
+ }
54
+
55
async function configure() {
56
if (wrongMap) {
57
if (!await confirmDialog(`There is a port mapping but it is pointing to the wrong port (${nat.mapped.private.port})`, { confirmText: "Fix it" })) return
src/api.net.ts
+13
-13
@@ -2,7 +2,7 @@
2
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
import { Client } from 'nat-upnp'
5
-import { HTTP_SERVICE_UNAVAILABLE, HTTP_SERVER_ERROR } from './const'
5
+import { HTTP_SERVICE_UNAVAILABLE } from './const'
6
import axios from 'axios'
7
import {parse} from 'node-html-parser'
8
import _ from 'lodash'
@@ -25,16 +25,17 @@ async function get_nat() {
25
throw new ApiError(HTTP_SERVICE_UNAVAILABLE, 'upnp failed')
26
})
27
const status = await getServerStatus()
28
- const port = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port
28
+ const internalPort = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port
29
const mappings = await client.getMappings().catch(() => null)
30
- const mapped = _.find(mappings, x => x.private.host === address && x.private.port === port || x.description === 'hfs')
30
+ const mapped = _.find(mappings, x => x.private.host === address && x.private.port === internalPort || x.description === 'hfs')
31
return {
32
- local_ip: address,
33
- gateway_ip: new URL(gateway.description).hostname,
34
- public_ip: await client.getPublicIp().catch(() => null),
32
+ localIp: address,
33
+ gatewayIp: new URL(gateway.description).hostname,
34
+ publicIp: await client.getPublicIp().catch(() => null),
35
mapped,
36
mappings,
37
- port,
37
+ internalPort,
38
+ externalPort: mapped?.public.port,
39
}
40
}
41
@@ -42,23 +43,22 @@ const apis: ApiHandlers = {
43
get_nat,
44
45
async map_port({ external }) {
45
- const { mapped, port } = await get_nat()
46
+ const { mapped, internalPort } = await get_nat()
47
const client = new Client()
48
if (mapped)
49
await client.removeMapping({ private: mapped.private.port, public: mapped.public.port, protocol: 'tcp' })
50
if (external)
50
- await client.createMapping({ private: port, public: external, description: 'hfs', ttl: 0 })
51
+ await client.createMapping({ private: internalPort, public: external, description: 'hfs', ttl: 0 })
52
return {}
53
},
54
55
async check_server() {
56
const noop = () => null
56
- const client = new Client()
57
- const publicIp = await client.getPublicIp().catch(noop)
57
+ const { publicIp, internalPort, externalPort } = await get_nat()
58
if (!publicIp) return new ApiError(HTTP_SERVICE_UNAVAILABLE, 'cannot detect public ip')
59
- const {http, https} = await getServerStatus()
60
- const port = https.port > 0 ? https.port : http.port
59
const prjInfo = await getProjectInfo()
60
+ const port = externalPort || internalPort
61
+ console.log(`checking server ${publicIp}:${port}`)
62
for (const svc of _.shuffle<PortScannerService>(prjInfo.checkServerServices)) {
63
const api = (axios as any)[svc.method]
64
const body = svc.body?.replace('$IP', publicIp).replace('$PORT', port) || ''