forked from jason5ng32/MyIP
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
477 lines (433 loc) · 22.5 KB
/
Copy pathindex.html
File metadata and controls
477 lines (433 loc) · 22.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="keywords"
content="MyIP, IP tool, IP check, IP Leak Check, IP quality check, IP lookup, DNS leak test, WebRTC leak test, browser fingerprint check, speed test, global latency test, MTR test, DNS lookup, Whois search" />
<meta name="description"
content="A better and open-source IP toolbox: check your IP address & geolocation, test IP for WebRTC and DNS IP leaks, run an IP quality check, browser fingerprint check, plus speed test, global latency test, MTR test, Whois search, and more.">
<meta name="robots" content="index, follow" />
<meta name="author" content="Jason Ng" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0e0e0e" media="(prefers-color-scheme: dark)" />
<meta name="background-color" content="#ffffff" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<!-- Avoid auto-linking phone numbers -->
<meta name="format-detection" content="telephone=no" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/logos/ios-logo-512.png" />
<title>IPCheck.ing - Check My IP Address and Geolocation - IP Leak Test - DNS Leak Test - IP Quality Check - Test Network Speed - All in one IP Toolbox</title>
<link rel="icon" href="/logos/logo.svg">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<!-- @site-url:open -->
<link rel="canonical" href="__SITE_URL__/" />
<meta property="og:url" content="__SITE_URL__/" />
<!-- @site-url:close -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="IPCheck.ing" />
<meta property="og:title" content="IPCheck.ing — All in one IP Toolbox" />
<meta property="og:description"
content="IP lookup, ASN history, connectivity tests, WebRTC and DNS leak detection, speed test, MTR, Whois, and browser fingerprint — in one open-source web app." />
<!-- @site-url:open -->
<meta property="og:image" content="__SITE_URL__/logos/og.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="IPCheck.ing logo with the tagline: All in one IP Toolbox" />
<!-- @site-url:close -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="IPCheck.ing — All in one IP Toolbox" />
<meta name="twitter:description"
content="IP lookup, ASN history, connectivity tests, WebRTC and DNS leak detection, speed test, MTR, Whois, and browser fingerprint — in one open-source web app." />
<!-- @site-url:open -->
<meta name="twitter:image" content="__SITE_URL__/logos/og.png" />
<!-- @site-url:close -->
<!-- WebSite JSON-LD: helps Google and AI engines recognize the site as a single entity instead of a loose collection of pages. -->
<!-- @site-url:open -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"@id": "__SITE_URL__/#website",
"name": "IPCheck.ing",
"url": "__SITE_URL__/",
"description": "Open-source IP toolbox: lookup, connectivity tests, WebRTC and DNS leak detection, speed test, MTR, Whois, and browser fingerprint.",
"inLanguage": ["en", "zh-CN", "zh-TW", "fr", "ru", "pt-BR"],
"publisher": { "@type": "Person", "name": "Jason Ng" }
}
</script>
<!-- @site-url:close -->
<!-- Seed the `.dark` class on <html> BEFORE first paint.
Reads the stored theme preference (field `theme`, values:
"auto" / "light" / "dark") via `jnReadPrefs`, falling back to the OS
`prefers-color-scheme` when on "auto" or when there's no stored
preference yet. Runs synchronously — must come before the inline
<style> block below and before the Vue bundle loads, so the boot
screen and the eventually-mounted app agree on the theme from
frame 1. Preferences.vue owns the class after mount via
`store.setDarkMode`. -->
<script>
(function () {
// Gates the initial-hidden `#app` rule; App.vue clears it for the
// scale/fade-in. If this script never runs, #app renders normally.
document.documentElement.setAttribute('data-booting', '');
// Stored preferences. The key mirrors PREFS_STORAGE_KEY in
// frontend/data/default-preferences.js — keep them in sync. Also used
// by the boot-quips script in <body> to pick the copy language.
window.jnReadPrefs = function () {
try {
return JSON.parse(localStorage.getItem('userPreferences_v7') || '{}') || {};
} catch (e) {
/* malformed entry or storage disabled — fall back to defaults */
return {};
}
};
var theme = window.jnReadPrefs().theme || 'auto';
var isDark =
theme === 'dark' ||
(theme === 'auto' &&
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) document.documentElement.classList.add('dark');
})();
</script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
/* Reserve space at the top of the body for: (a) the iOS safe-area
(transparent status bar) — `env()` resolves to 0 on non-notch
platforms, and (b) the fixed Nav (3.5rem = h-14 in Nav.vue). The Nav
used to be `position: sticky` and lived in the normal flow, so no
padding was needed; switching it to `fixed` lets the body rubber-band
bounce on iOS without dragging the Nav along, but now we have to
compensate for the Nav's lost flow height here. */
body {
padding-top: calc(env(safe-area-inset-top) + 3.5rem);
}
/* Standalone pages (/tools/:slug, /privacy, /r/:id) have their own in-flow
sticky header instead of the homepage's fixed Nav, and that header carries
the iOS safe-area inset as its own padding (StandalonePageHeader.vue) so
it keeps clearing the Dynamic Island once sticky pins it to the viewport
top. The body therefore reserves nothing here. App.vue toggles this class
on the standalone routes. */
body.jn-standalone-page {
padding-top: 0;
}
html {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
tab-size: 4;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
scrollbar-gutter: stable;
background: #ffffff;
color: #0e0e0e;
}
/* Dark mode is gated strictly on the `.dark` class — *never* on
`prefers-color-scheme` alone — so this inline CSS never fights with
Tailwind's dark variant or with Preferences.vue's explicit light/dark
selection. The `.dark` class is seeded by the script block below
before first paint, so boot-screen rendering already reflects the
right theme without a flash. */
html.dark {
background: #0e0e0e;
color: #fafafa;
}
/* Boot screen — shown until App.vue runs the hand-off sequence.
Fixed overlay (not an in-flow 100vh block) so mobile chrome doesn't
push it into a scroll, and so Vue can mount into `#app` underneath
while the overlay is still visible.
25vh top padding anchors the logo to the upper third so phone
notches / status bars don't visually push it low. */
.jn-loading {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 16px;
padding: 25vh 24px 24px;
text-align: center;
background: #ffffff;
}
html.dark .jn-loading {
background: #0e0e0e;
}
/* Hand-off, driven from App.vue:
stage-1 — non-logo children (progress bar, quips) fade out, logo stays put.
stage-2 — the logo scales down to 0 and fades.
stage-3 — overlay removed, `#app` runs `.jn-app-enter` below. */
.jn-loading>svg,
.jn-loading>div,
.jn-loading>p {
transition: opacity 180ms ease-out,
transform 200ms cubic-bezier(0.55, 0, 0.7, 0);
transform-origin: center center;
}
.jn-loading.jn-loading-stage-1> :not(svg) {
opacity: 0;
}
.jn-loading.jn-loading-stage-2>svg {
opacity: 0;
transform: scale(0);
}
.jn-loading svg {
width: 96px;
height: 96px;
fill: currentColor;
}
.jn-loading p {
margin: 0;
font-size: 14px;
color: #6b7280;
line-height: 1.5;
}
.jn-loading a {
color: inherit;
text-decoration: underline;
}
/* Fake progress bar — width is driven by the boot-quips script below,
easing toward a 92% cap; the real hand-off never waits for it. */
.jn-progress {
width: 220px;
max-width: 70vw;
height: 6px;
border-radius: 999px;
background: #e5e7eb;
overflow: hidden;
}
html.dark .jn-progress {
background: #27272a;
}
/* blue-500 — the brand action color, same value in light and dark. */
.jn-progress-fill {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: #3b82f6;
transition: width 600ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.jn-progress-fill {
transition: none;
}
}
/* Screen-reader-only text — the rotating quips are aria-hidden so they
don't spam the live region every second; this line is announced once
instead. */
.jn-visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
/* `#app` starts hidden; App.vue reveals it immediately at mount and it
fades in UNDER the opaque boot overlay */
html[data-booting] #app {
opacity: 0;
}
#app {
transition: opacity 320ms ease-out;
}
#app.jn-app-enter {
opacity: 1;
transform: none;
}
</style>
<!-- Pre-Vue runtime:
1. Stash the PWA install-prompt event so the app can reuse it later
(the event fires exactly once, well before Vue mounts).
2. After 12s, reveal the slow-network fallback hint. -->
<script>
window.ipcheckInstallPromptEvent = null;
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
window.ipcheckInstallPromptEvent = event;
});
setTimeout(() => {
const hint = document.getElementById('jn-loading-slow-hint');
if (hint) hint.hidden = false;
}, 12000);
</script>
</head>
<body>
<!-- Pre-Vue boot screen. App.vue drives the hand-off via `#jn-loading` —
the id is a load-bearing contract, keep it in sync. -->
<div id="jn-loading" class="jn-loading" role="status" aria-live="polite">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511 512.35" aria-hidden="true"
shape-rendering="geometricPrecision" fill-rule="evenodd" clip-rule="evenodd">
<path id="Logo"
d="M162.62,21.9026663 C157.13,27.3326663 151.99,33.9226663 147.2,41.6126663 C129.83,69.4326663 116.87,111.602666 107.28,164.772666 C50.98,175.412666 16.22,198.912666 17.38,222.912666 C18.42,244.652666 45.84,261.322666 87.05,272.832666 C84.34,281.212666 84.98,282.652666 88.65,292.962666 C57.87,305.942666 25.71,345.362666 0,379.892666 L100.03,447.502666 L64.71,512.352666 L449.12,512.352666 L411.86,447.502666 L511,378.632666 C481.92,337.782666 446.81,303.072666 424.88,293.652666 C429.51,281.632666 430.32,279.532666 426.44,272.862666 C467.65,261.142666 494.67,244.022666 494.61,221.392666 C494.55,196.712666 459.11,173.012666 406.3,164.772666 C393.66,111.272666 381.08,69.1526663 365.07,41.5026663 C362.16,36.4826663 359.14,31.9326663 355.98,27.8826663 C308.32,-33.2373337 291.62,25.1926663 257.84,25.1226663 C218.67,25.0426663 213.69,-28.5673337 162.62,21.9026663 Z M223.27,419.802666 C223.27,416.232666 226.17,413.332666 229.74,413.332666 L240.26,413.332666 C241.64,413.332666 242.92,413.772666 243.96,414.502666 C247.73,416.602666 251.42,417.832666 254.97,417.922666 C256.256726,417.95538 257.551379,417.837477 258.853959,417.554552 C261.134997,417.059103 263.440344,416.057596 265.77,414.472666 C266.837339,413.754341 268.093464,413.368108 269.38,413.362666 L282.16,413.332666 C285.73,413.332666 288.62,416.232666 288.62,419.802666 C288.62,423.372666 285.73,426.272666 282.16,426.272666 L271.21,426.272666 C265.75,429.542666 260.23,430.942666 254.67,430.802666 C249.23,430.662666 243.89,429.032666 238.66,426.272666 L229.74,426.272666 C226.17,426.272666 223.27,423.372666 223.27,419.802666 Z M160.62,279.492666 C221.05,301.232666 281.49,300.872666 341.92,281.322666 C283.47,286.072666 219.13,284.942666 160.62,279.492666 Z M267.04,306.612666 L244.93,306.612666 C210.81,384.892666 122.79,350.782666 142.77,277.672666 C135.46,276.872666 128.26,275.992666 121.21,275.052666 L120.89,276.932666 L120.3,280.492666 L116.82,301.362666 C86.43,294.642666 103.46,373.132666 131.08,366.232666 C135.3,378.412666 136.442197,389.44711 142.34,398.422666 C171.368144,442.599077 205.648487,494.791988 256.040415,494.791988 C306.432342,494.791988 341.594767,438.778379 368.99,398.422666 C375.513955,388.812277 376.14,383.142666 380.53,369.392666 C405.97,373.452666 426.88,296.082666 395.87,301.762666 L392.68,280.712666 L392.13,277.062666 L391.9,275.522666 C384.43,276.682666 376.78,277.722666 368.99,278.632666 C389.88,349.262666 300.46,385.132666 267.04,306.612666 Z M284.280512,176.166601 L284.280512,67.1666006 C366.547618,67.1666006 361.40053,176.166601 284.280512,176.166601 Z M170.780508,409.666601 C182.571845,404.08157 211.029823,401.289054 256.154442,401.289054 C301.27906,401.289054 329.487749,404.08157 340.780508,409.666601 C340.780508,398.413948 339.802073,389.918194 323.84137,384.371704 C288.837937,372.207694 222.824258,373.335802 189.15409,384.371704 C170.816498,390.382125 170.780508,397.067154 170.780508,409.666601 Z M206.779566,446.766602 C218.286957,452.470688 235.027101,455.322731 257,455.322731 C278.972899,455.322731 295.566401,452.470688 306.780508,446.766602 C306.780508,458.019255 306.204953,466.515009 296.816215,472.061498 C286.632212,478.077798 271.985838,480.842479 257.386147,480.76502 C242.467564,480.685868 227.597726,477.639061 217.587657,472.061498 C206.800736,466.051077 206.779566,459.366048 206.779566,446.766602 Z M256.254843,67.1117579 L229.254843,87.6169826 C229.254843,124.271828 229.254843,160.370447 229.254843,204.775426 L256.254843,208.111758 C256.254843,143.245746 256.254843,107.726499 256.254843,67.1117579 Z M207.254843,67.1117579 L180.254843,87.6169826 C180.254843,124.271828 180.254843,160.370447 180.254843,204.775426 L207.254843,208.111758 C207.254843,143.245746 207.254843,107.726499 207.254843,67.1117579 Z" />
</svg>
<p class="jn-visually-hidden">Loading, it may take a while for the first time. Please wait…</p>
<div class="jn-progress" aria-hidden="true">
<span class="jn-progress-fill" id="jn-loading-progress"></span>
</div>
<p id="jn-loading-quip" aria-hidden="true"></p>
<p id="jn-loading-slow-hint" hidden>
Waiting too long? Try <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a>, or refresh the page.
</p>
</div>
<!-- Boot quips + fake progress. The copy lives inline (not in vue-i18n)
because it runs before Vue loads; every full locale ships here (beta ones
are welcome but not required) and the language pick mirrors
frontend/locales/i18n.js (stored prefs → ?hl= → browser language → en).
tests/index-html-i18n.test.js holds both to common/locale-registry.js.
The progress is time-driven make-believe: one
quip every 1.5s, width easing toward a 92% cap. App.vue's hand-off
removes the overlay the moment Vue mounts and is never delayed by the
script — returning visitors with a warm cache may only glimpse quip #1. -->
<script>
(() => {
const QUIPS = {
en: [
'Installing network card drivers…',
'Lining up the RJ45 connector…',
'Plugging the fiber into the router…',
'Blowing the dust out of the Ethernet port…',
'Rebooting the modem (attempt #1)…',
'Three-way handshaking with your ISP…',
'Asking the DNS where you actually are…',
'Almost there, promise…',
],
zh: [
'正在安装网卡驱动…',
'正在把水晶头对准 RJ45 接口…',
'正在把光纤插进路由器…',
'正在吹掉网线接头上的灰…',
'正在重启光猫(第 1 次)…',
'正在与运营商三次握手…',
'正在问 DNS 你到底在哪…',
'马上就好,真的…',
],
'zh-TW': [
'正在安裝網路卡驅動程式…',
'正在把水晶頭對準 RJ45 接頭…',
'正在把光纖插進路由器…',
'正在吹掉網路線接頭上的灰塵…',
'正在重新啟動數據機(第 1 次)…',
'正在與電信業者三次握手…',
'正在問 DNS 你到底在哪…',
'馬上就好,真的…',
],
fr: [
'Installation du pilote de la carte réseau…',
'Alignement de la fiche RJ45…',
'Branchement de la fibre sur le routeur…',
'Dépoussiérage du port Ethernet…',
'Redémarrage de la box (essai n° 1)…',
'Triple poignée de main avec le FAI…',
'On demande au DNS où vous êtes…',
"C'est presque prêt, promis…",
],
ru: [
'Устанавливаем драйвер сетевой карты…',
'Целимся коннектором в порт RJ45…',
'Подключаем оптоволокно к роутеру…',
'Выдуваем пыль из Ethernet-порта…',
'Перезагружаем модем (попытка №1)…',
'Тройное рукопожатие с провайдером…',
'Спрашиваем у DNS, где вы на самом деле…',
'Почти готово, честно…',
],
'pt-BR': [
'Instalando os drivers da placa de rede…',
'Alinhando o conector RJ45…',
'Conectando a fibra ao roteador…',
'Soprando a poeira da porta Ethernet…',
'Reiniciando o modem (tentativa nº 1)…',
'Triplo aperto de mão com seu provedor…',
'Perguntando ao DNS onde você realmente está…',
'Quase lá, prometemos…',
],
};
// Slow-network hint (revealed after 12s by the head script). Stored as
// whole sentences because the link position differs per language; the
// markup's English text stays as the no-script fallback.
const SLOW_HINTS = {
en: 'Waiting too long? Try <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a>, or refresh the page.',
zh: '等得太久?试试 <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a>,或刷新页面。',
'zh-TW': '等太久了?試試 <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a>,或重新整理頁面。',
fr: 'C’est un peu long ? Essayez <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a>, ou actualisez la page.',
ru: 'Слишком долго? Попробуйте <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a> или обновите страницу.',
'pt-BR': 'Esperando demais? Experimente o <a href="https://lite.ipcheck.ing/">IPCheck.ing Lite</a> ou atualize a página.',
};
// Hand-copy of matchLocale() in common/locale-registry.js — index.html
// ships outside the bundle and can't import it. Resolves a BCP-47 tag in
// three steps: exact, then the base language, then any locale of the same
// family, `codes` order deciding between siblings.
// tests/index-html-i18n.test.js runs both implementations side by side;
// change one and change the other.
const matchLang = (tag, codes) => {
if (!tag) return null;
const wanted = String(tag).toLowerCase();
const base = wanted.split('-')[0];
return codes.find((code) => code.toLowerCase() === wanted)
?? codes.find((code) => code.toLowerCase() === base)
?? codes.find((code) => code.toLowerCase().split('-')[0] === base)
?? null;
};
// Which languages the inline copy above covers — beta locales may not be
// among them and then resolve to a neighbour or to English.
const QUIP_LANGS = Object.keys(QUIPS);
const pickLang = () => {
const prefs = window.jnReadPrefs ? window.jnReadPrefs() : {};
const hl = new URLSearchParams(window.location.search).get('hl');
return matchLang(prefs.lang, QUIP_LANGS)
?? matchLang(hl, QUIP_LANGS)
?? matchLang(navigator.language, QUIP_LANGS)
?? 'en';
};
const lang = pickLang();
const quips = QUIPS[lang];
const hintEl = document.getElementById('jn-loading-slow-hint');
if (hintEl) hintEl.innerHTML = SLOW_HINTS[lang];
const quipEl = document.getElementById('jn-loading-quip');
const fillEl = document.getElementById('jn-loading-progress');
if (!quipEl || !fillEl) return;
let timer;
let step = 0;
const tick = () => {
// Overlay already removed by App.vue — stop ticking.
if (!document.getElementById('jn-loading')) {
clearInterval(timer);
return;
}
step += 1;
// Past the last quip the copy holds while the bar keeps creeping.
quipEl.textContent = quips[Math.min(step, quips.length) - 1];
fillEl.style.width = (92 * (1 - Math.pow(0.78, step))).toFixed(1) + '%';
};
tick();
timer = setInterval(tick, 1500);
})();
</script>
<noscript>
<p style="padding: 40px; text-align: center;">
IPCheck.ing requires JavaScript. Please enable it and refresh.
</p>
</noscript>
<!-- Vue mounts here. `vaul-drawer-wrapper` is the marker vaul-vue uses to scale
the page background when a bottom drawer is open (Advanced Tools panel). -->
<div id="app" vaul-drawer-wrapper></div>
<script type="module" src="/frontend/main.js"></script>
</body>
</html>