admin/internet: check domain button
Massimo Melina committed
Sep 16, 2023 at 14:22 UTC
a9a6d857355162b0a1265ecde590c7cf48cdcd46
6 files changed
+44
-18
admin/src/InternetPage.ts
+10
-1
@@ -3,7 +3,7 @@ import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, Linea
3
import { CardMembership, HomeWorkTwoTone, Lock, PublicTwoTone, RouterTwoTone, Send } from '@mui/icons-material'
4
import { apiCall, useApiEx } from './api'
5
import { closeDialog, DAY, formatTimestamp, with_ } from '@hfs/shared'
6
-import { Flex, LinkBtn, manipulateConfig } from './misc'
6
+import { Flex, LinkBtn, manipulateConfig, isIP, Btn } from './misc'
7
import { alertDialog, confirmDialog, promptDialog, toast } from './dialog'
8
import { Form, NumberField } from '@hfs/mui-grid-form'
9
import md from './md'
@@ -95,6 +95,9 @@ export default function InternetPage() {
95
}
96
97
function baseUrlBox() {
98
+ const url = status.data?.baseUrl
99
+ const hostname = url && new URL(url).hostname
100
+ const domain = !isIP(hostname) && hostname
101
return status.element || h(Card, {}, h(CardContent, {},
102
h(Box, { fontSize: 'x-large', mb: 2 }, "Address / Domain"),
103
h(Flex, { flexWrap: 'wrap', alignItems: 'center' },
@@ -103,6 +106,12 @@ export default function InternetPage() {
106
size: 'small',
107
onClick() { changeBaseUrl().then(status.reload) }
108
}, "Change"),
109
+ domain && h(Btn, {
110
+ size: 'small',
111
+ variant: 'outlined',
112
+ onClick: () => apiCall('check_domain', { domain })
113
+ .then(() => alertDialog("Domain seems ok", 'success'))
114
+ }, "Check"),
115
)
116
))
117
}
admin/src/misc.ts
+3
-3
@@ -82,7 +82,7 @@ export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, l
82
})
83
84
interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'> {
85
- icon: SvgIconComponent
85
+ icon?: SvgIconComponent
86
title?: ReactNode
87
disabled?: boolean | string
88
progress?: boolean | number
@@ -101,9 +101,9 @@ export function Btn({ icon, title, onClick, disabled, progress, link, tooltipPro
101
onClick = () => window.open(link)
102
let ret: ReturnType<FC> = h(LoadingButton, {
103
variant: 'contained',
104
- startIcon: h(icon),
104
+ startIcon: icon && h(icon),
105
loading: Boolean(loading || progress),
106
- loadingPosition: 'start',
106
+ loadingPosition: icon && 'start',
107
loadingIndicator: typeof progress !== 'number' ? undefined
108
: h(CircularProgress, { size: '1rem', value: progress*100, variant: 'determinate' }),
109
disabled,
src/api.net.ts
+15
-5
@@ -13,7 +13,7 @@ import { cert, getCertObject, getIps, getServerStatus, privateKey } from './list
13
import { getProjectInfo } from './github'
14
import { httpString } from './util-http'
15
import { exec } from 'child_process'
16
-import { debounceAsync, HOUR, MINUTE, objSameKeys, repeat } from './misc'
16
+import { apiAssertTypes, debounceAsync, HOUR, MINUTE, objSameKeys, repeat } from './misc'
17
import acme from 'acme-client'
18
import fs from 'fs/promises'
19
import { Dict } from './misc'
@@ -94,10 +94,17 @@ export const acmeMiddleware: Middleware = (ctx, next) => { // koa format
94
return next()
95
}
96
97
-async function generateSSLCert(domain: string, email?: string) {
97
+async function checkDomain(domain: string) {
98
const { address: domainIp } = await lookup(domain).catch(e => {
99
throw e.code !== 'ENOTFOUND' ? e : new ApiError(HTTP_FAILED_DEPENDENCY, "this domain doesn't exist")
100
})
101
+ const { publicIp } = await getNatInfo() // do this before stopping the server
102
+ if (publicIp !== domainIp)
103
+ throw new ApiError(HTTP_FAILED_DEPENDENCY, `please configure your domain to point to ${publicIp} (currently on ${domainIp}) --- a change can take hours to be effective`)
104
+}
105
+
106
+async function generateSSLCert(domain: string, email?: string) {
107
+ await checkDomain(domain)
108
// will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
109
const { http } = await getServerStatus()
110
const tempSrv = http.listening && http.port === 80 ? undefined : createServer(acmeListener)
@@ -108,9 +115,7 @@ async function generateSSLCert(domain: string, email?: string) {
115
acmeMiddlewareEnabled = true
116
console.debug('acme challenge server ready')
117
try {
111
- const { publicIp, upnp, externalPort } = await getNatInfo() // do this before stopping the server
112
- if (publicIp !== domainIp)
113
- throw new ApiError(HTTP_FAILED_DEPENDENCY, `please configure your domain to point to ${publicIp} (currently on ${domainIp})`)
118
+ const { upnp, externalPort } = await getNatInfo() // do this before stopping the server
119
let check = await checkPort(domain, 80) // some check services may not consider the domain, but we already verified that
120
if (check && !check.success && upnp && externalPort !== 80) { // consider a short-lived mapping
121
// @ts-ignore
@@ -184,6 +189,11 @@ async function checkPort(ip: string, port: number) {
189
const apis: ApiHandlers = {
190
get_nat: getNatInfo,
191
192
+ check_domain({ domain }) {
193
+ apiAssertTypes({ string: domain })
194
+ return checkDomain(domain)
195
+ },
196
+
197
async map_port({ external, internal }) {
198
const { upnp, externalPort, internalPort } = await getNatInfo()
199
if (!upnp)
src/cross.ts
+4
@@ -231,3 +231,7 @@ export function formatTimestamp(x: string) {
231
export function isPrimitive(x: unknown): x is boolean | string | number | undefined | null {
232
return !x || Object(x) !== x
233
}
234
+
235
+export function isIP(address: string) {
236
+ return /^([.:\da-f]+)$/i.test(address)
237
+}
\ No newline at end of file
src/frontEndApis.ts
+1
-8
@@ -18,6 +18,7 @@ import { hasPermission, urlToNode } from './vfs'
18
import { mkdir, rename, rm } from 'fs/promises'
19
import { dirname, join } from 'path'
20
import { getUploadMeta } from './upload'
21
+import { apiAssertTypes } from './misc'
22
23
export const frontEndApis: ApiHandlers = {
24
get_file_list,
@@ -118,11 +119,3 @@ export function notifyClient(ctx: Koa.Context, name: string, data: any) {
119
}
120
121
const NOTIFICATION_PREFIX = 'notificationChannel:'
121
-
122
-function apiAssertTypes(paramsByType: { [type:string]: { [name:string]: any } }) {
123
- for (const [types,params] of Object.entries(paramsByType))
124
- for (const type of types.split('_'))
125
- for (const [name,val] of Object.entries(params))
126
- if (typeof val !== type)
127
- throw new ApiError(HTTP_BAD_REQUEST, 'bad ' + name)
128
-}
\ No newline at end of file
src/misc.ts
+11
-1
@@ -13,6 +13,8 @@ import { Readable } from 'stream'
13
import { matcher } from 'micromatch'
14
import { SocketAddress, BlockList } from 'node:net'
15
import debounceAsync from './debounceAsync'
16
+import { ApiError } from './apiMiddleware'
17
+import { HTTP_BAD_REQUEST } from './const'
18
export { debounceAsync }
19
20
type ProcessExitHandler = (signal:string) => any
@@ -70,7 +72,7 @@ export function makeNetMatcher(mask: string, emptyMaskReturns=false) {
72
all[0] = all[0]!.slice(1)
73
const bl = new BlockList()
74
for (const x of all) {
73
- const m = /^([.:\d]+)(?:\/(\d+)|-(.+)|)$/.exec(x)
75
+ const m = /^([.:\da-f]+)(?:\/(\d+)|-(.+)|)$/i.exec(x)
76
if (!m) {
77
console.warn("error in network mask", x)
78
continue
@@ -149,3 +151,11 @@ export class AsapStream<T> extends Readable {
151
Promise.allSettled(this.promises).then(() => this.push(null))
152
}
153
}
154
+
155
+export function apiAssertTypes(paramsByType: { [type:string]: { [name:string]: any } }) {
156
+ for (const [types,params] of Object.entries(paramsByType))
157
+ for (const type of types.split('_'))
158
+ for (const [name,val] of Object.entries(params))
159
+ if (typeof val !== type)
160
+ throw new ApiError(HTTP_BAD_REQUEST, 'bad ' + name)
161
+}
\ No newline at end of file