Skip to content

Commit 633a256

Browse files
committed
fix: ipv6 connect peer and block host
1 parent 44a6451 commit 633a256

File tree

10 files changed

+124
-78
lines changed

10 files changed

+124
-78
lines changed

.changeset/rich-oranges-pay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'renterd': minor
3+
---
4+
5+
The host blocklist dialog now supports adding IPv6 addresses.

.changeset/tame-cougars-sit.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'hostd': minor
3+
'renterd': minor
4+
'walletd': minor
5+
---
6+
7+
The connect to peer dialog now supports IPv6 addresses. Closes https://github.com/SiaFoundation/web/issues/624

libs/design-system/src/app/SyncerConnectPeerDialog.tsx

Lines changed: 85 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,62 @@
11
'use client'
22

33
import { Paragraph } from '../core/Paragraph'
4-
import { triggerSuccessToast } from '../lib/toast'
5-
import {
6-
FormFieldFormik,
7-
FormSubmitButtonFormik,
8-
} from '../components/FormFormik'
4+
import { triggerErrorToast, triggerSuccessToast } from '../lib/toast'
95
import { Response } from '@siafoundation/react-core'
10-
import { useFormik } from 'formik'
11-
import * as Yup from 'yup'
12-
import { hostnameOrIpRegex } from '../lib/ipRegex'
6+
import { hostnameOrIpRegex, ipv6Regex } from '../lib/ipRegex'
137
import { Dialog } from '../core/Dialog'
8+
import { ConfigFields } from '../form/configurationFields'
9+
import { useDialogFormHelpers } from '../form/useDialogFormHelpers'
10+
import { useForm } from 'react-hook-form'
11+
import { useCallback } from 'react'
12+
import { FormSubmitButton } from '../components/Form'
13+
import { FieldNumber } from '../form/FieldNumber'
14+
import { FieldText } from '../form/FieldText'
1415

15-
const initialValues = {
16-
port: 9981,
17-
ip: '',
16+
function getDefaultValues() {
17+
return {
18+
ip: '',
19+
port: 9981,
20+
}
1821
}
1922

20-
const validationSchema = Yup.object().shape({
21-
port: Yup.number()
22-
.required('Required')
23-
.min(0, 'Out of valid range')
24-
.max(65535, 'Out of valid range'),
25-
ip: Yup.string()
26-
.required('Required')
27-
.test('ip', 'Invalid hostname or IP address', (v) =>
28-
hostnameOrIpRegex().test(v || '')
29-
),
30-
})
23+
type Values = ReturnType<typeof getDefaultValues>
24+
25+
function getFields(): ConfigFields<Values, never> {
26+
return {
27+
port: {
28+
type: 'number',
29+
title: 'Port',
30+
placeholder: '9981',
31+
autoComplete: 'off',
32+
disableGroupSeparators: true,
33+
validation: {
34+
validate: {
35+
min: (value) =>
36+
Number(value) >= 0 || 'must be greater than or equal to 0',
37+
max: (value) =>
38+
Number(value) <= 65535 || 'must be less than or equal to 65,535',
39+
},
40+
},
41+
},
42+
ip: {
43+
type: 'text',
44+
title: 'Address',
45+
placeholder: 'host.acme.com or 127.0.0.1',
46+
autoComplete: 'off',
47+
validation: {
48+
validate: {
49+
valid: (v) =>
50+
hostnameOrIpRegex().test(String(v) || '') ||
51+
'invalid hostname or IP address',
52+
},
53+
},
54+
},
55+
}
56+
}
57+
58+
const defaultValues = getDefaultValues()
59+
const fields = getFields()
3160

3261
export type SyncerConnectPeerDialogParams = void
3362

@@ -45,73 +74,58 @@ export function SyncerConnectPeerDialog({
4574
connect,
4675
onOpenChange,
4776
}: Props) {
48-
const formik = useFormik({
49-
initialValues,
50-
validationSchema,
51-
onSubmit: async (values, actions) => {
52-
const netAddress = `${values.ip}:${values.port}`
77+
const form = useForm({
78+
mode: 'all',
79+
defaultValues,
80+
})
81+
82+
const { handleOpenChange, closeAndReset } = useDialogFormHelpers({
83+
form,
84+
onOpenChange,
85+
defaultValues,
86+
})
87+
88+
const onSubmit = useCallback(
89+
async (values: Values) => {
90+
let netAddress = `${values.ip}:${values.port}`
91+
if (ipv6Regex().test(values.ip)) {
92+
netAddress = `[${values.ip}]:${values.port}`
93+
}
5394
const response = await connect(netAddress)
5495
if (response.error) {
55-
const formattedError = response.error.replace(
56-
`invalid peer address: address ${netAddress}:`,
57-
''
58-
)
59-
actions.setStatus({ error: formattedError })
96+
triggerErrorToast({ title: response.error })
6097
} else {
6198
triggerSuccessToast({ title: 'Connected to peer' })
62-
actions.resetForm()
63-
onOpenChange(false)
99+
closeAndReset()
64100
}
65101
},
66-
})
102+
[closeAndReset, connect]
103+
)
67104

68105
return (
69106
<Dialog
70107
trigger={trigger}
71108
title="Connect peer"
72109
open={open}
73-
onOpenChange={(open) => {
74-
if (!open) {
75-
formik.resetForm()
76-
}
77-
onOpenChange(open)
78-
}}
110+
onOpenChange={handleOpenChange}
79111
contentVariants={{
80112
className: 'w-[400px]',
81113
}}
114+
onSubmit={form.handleSubmit(onSubmit)}
115+
controls={
116+
<div className="px-1">
117+
<FormSubmitButton form={form} size="medium" className="w-full">
118+
Connect
119+
</FormSubmitButton>
120+
</div>
121+
}
82122
>
83123
<div className="flex flex-col gap-4">
84124
<Paragraph size="14">Connect to a peer by IP address.</Paragraph>
85-
<form onSubmit={formik.handleSubmit}>
86-
<div className="flex flex-col gap-4">
87-
<FormFieldFormik
88-
formik={formik}
89-
title="Address"
90-
name="ip"
91-
placeholder="host.acme.com or 127.0.0.1"
92-
autoComplete="off"
93-
type="text"
94-
variants={{
95-
size: 'medium',
96-
}}
97-
/>
98-
<FormFieldFormik
99-
formik={formik}
100-
title="Port"
101-
name="port"
102-
disableGroupSeparators
103-
placeholder="9981"
104-
autoComplete="off"
105-
type="number"
106-
variants={{
107-
size: 'medium',
108-
}}
109-
/>
110-
<FormSubmitButtonFormik formik={formik} size="medium">
111-
Connect
112-
</FormSubmitButtonFormik>
113-
</div>
114-
</form>
125+
<div className="flex flex-col gap-4">
126+
<FieldText form={form} fields={fields} name="ip" size="medium" />
127+
<FieldNumber form={form} fields={fields} name="port" size="medium" />
128+
</div>
115129
</div>
116130
</Dialog>
117131
)

libs/design-system/src/components/Form.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,15 @@ type FormSubmitProps<Values extends FieldValues> = {
102102
size?: React.ComponentProps<typeof Button>['size']
103103
variant?: React.ComponentProps<typeof Button>['variant']
104104
children: React.ReactNode
105+
className?: string
105106
withStatusError?: boolean
106107
}
107108

108109
export function FormSubmitButton<Values extends FieldValues>({
109110
form,
110111
size = 'medium',
111112
variant = 'accent',
113+
className,
112114
children,
113115
}: FormSubmitProps<Values>) {
114116
return (
@@ -117,6 +119,7 @@ export function FormSubmitButton<Values extends FieldValues>({
117119
<Text color="red">{formik.status.error}</Text>
118120
)} */}
119121
<Button
122+
className={className}
120123
size={size}
121124
variant={variant}
122125
state={form.formState.isSubmitting ? 'waiting' : undefined}

libs/design-system/src/form/ConfigurationNumber.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export function ConfigurationNumber<
2525
decimalsLimit = 2,
2626
after,
2727
placeholder: _placeholder,
28+
disableGroupSeparators,
29+
autoComplete,
2830
units,
2931
prefix,
3032
} = field
@@ -52,6 +54,8 @@ export function ConfigurationNumber<
5254
units={units}
5355
prefix={prefix}
5456
decimalsLimit={decimalsLimit}
57+
disableGroupSeparators={disableGroupSeparators}
58+
autoComplete={autoComplete}
5559
placeholder={placeholder}
5660
state={
5761
error

libs/design-system/src/form/ConfigurationText.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function ConfigurationText<
3030
name={name}
3131
placeholder={placeholder}
3232
type={type}
33+
autoComplete={field.autoComplete}
3334
state={
3435
error
3536
? 'invalid'

libs/design-system/src/form/FieldNumber.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export function FieldNumber<
3232
units={units}
3333
size={size}
3434
decimalsLimit={decimalsLimit}
35+
disableGroupSeparators={field.disableGroupSeparators}
36+
autoComplete={field.autoComplete}
3537
placeholder={placeholder ? new BigNumber(placeholder) : undefined}
3638
state={
3739
error

libs/design-system/src/form/FieldText.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export function FieldText<
3535
name={name}
3636
placeholder={field.placeholder}
3737
size={size}
38-
autoComplete={autoComplete}
38+
autoComplete={
39+
autoComplete !== undefined ? autoComplete : field.autoComplete
40+
}
3941
type={field.type}
4042
readOnly={field.readOnly}
4143
spellCheck={spellCheck}

libs/design-system/src/form/configurationFields.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export type ConfigField<
6161

6262
validation: RegisterOptions<Values>
6363
trigger?: Path<Values>[]
64+
65+
// other
66+
disableGroupSeparators?: boolean
67+
autoComplete?: string
6468
}
6569

6670
export type ConfigFields<

libs/design-system/src/lib/ipRegex.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
// Simpler combined regex https://regex101.com/r/0WMysi/2
1+
const domain = '((?!-)[A-Za-z0-9-]+([-.]{1}[a-z0-9]+)*.[A-Za-z]{2,6})'
2+
const ipv4 =
3+
'((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))'
4+
const ipv6 =
5+
'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))'
6+
7+
// Playground: https://regex101.com/r/1ojZzJ/1
28
export const hostnameOrIpRegex = () =>
3-
new RegExp(
4-
'^(((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|((([a-zA-Z]|[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]).)+([A-Za-z|[A-Za-z][A-Za-z0-9‌​-]*[A-Za-z0-9])))$',
5-
// '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$',
6-
'g'
7-
)
9+
new RegExp(`^(${domain}|${ipv4}|${ipv6})$`, 'g')
10+
11+
export const ipv6Regex = () => new RegExp(`^${ipv6}$`, 'g')

0 commit comments

Comments
 (0)