@samitouri / QOSami-HFS / commits / a9a6d857

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