From 941e9a078fe070b1783ce94f16d20e119d4b6a0b Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 24 Feb 2024 21:38:00 +0800 Subject: [PATCH] add network settings page (#234) * add network page * adjust the layout * adjust the layout * add proxy control * adjust the layout * add webhook component * adjust the layout * adjust the layout * add interface component * add network API * fetch network settings * save IP mode * save proxy settings * save webhook settings * save webhook settings * save webhook settings * fix the warnings --- internal/provider/linode/http.go | 12 ++- internal/server/controllers/network.go | 73 ++++++++++++++ internal/server/server.go | 4 + web/api/network.ts | 55 +++++++++++ web/app/domains/page.tsx | 3 - web/app/network/page.tsx | 129 +++++++++++++++++++++++++ web/components/icons.tsx | 7 ++ web/components/ip-interface.tsx | 37 +++++++ web/components/ip-mode.tsx | 66 +++++++++++++ web/components/proxy.tsx | 80 +++++++++++++++ web/components/resolver.tsx | 37 +++++++ web/components/stat.tsx | 2 +- web/components/webhook.tsx | 79 +++++++++++++++ web/config/site.ts | 6 +- 14 files changed, 581 insertions(+), 9 deletions(-) create mode 100644 internal/server/controllers/network.go create mode 100644 web/api/network.ts create mode 100644 web/app/network/page.tsx create mode 100644 web/components/ip-interface.tsx create mode 100644 web/components/ip-mode.tsx create mode 100644 web/components/proxy.tsx create mode 100644 web/components/resolver.tsx create mode 100644 web/components/webhook.tsx diff --git a/internal/provider/linode/http.go b/internal/provider/linode/http.go index e59614b8..c8aad1cc 100644 --- a/internal/provider/linode/http.go +++ b/internal/provider/linode/http.go @@ -16,10 +16,14 @@ import ( func CreateHTTPClient(conf *settings.Settings) (*http.Client, error) { transport := &http.Transport{} - transport, err := applyProxy(conf.Socks5Proxy, transport) - if err != nil { - log.Infof("Error connecting to proxy: '%s'", err) - log.Info("Continuing without proxy") + var err error + + if conf.UseProxy && conf.Socks5Proxy != "" { + transport, err = applyProxy(conf.Socks5Proxy, transport) + if err != nil { + log.Errorf("Error connecting to proxy: '%s'", err) + log.Info("Continuing without proxy") + } } if conf.LoginToken == "" { diff --git a/internal/server/controllers/network.go b/internal/server/controllers/network.go new file mode 100644 index 00000000..07e4c18d --- /dev/null +++ b/internal/server/controllers/network.go @@ -0,0 +1,73 @@ +package controllers + +import ( + "github.com/TimothyYe/godns/internal/settings" + "github.com/TimothyYe/godns/internal/utils" + "github.com/gofiber/fiber/v2" + log "github.com/sirupsen/logrus" +) + +type NetworkSettings struct { + IPMode string `json:"ip_mode"` + IPUrls []string `json:"ip_urls"` + IPV6Urls []string `json:"ipv6_urls"` + UseProxy bool `json:"use_proxy"` + SkipSSLVerify bool `json:"skip_ssl_verify"` + Socks5Proxy string `json:"socks5_proxy"` + Webhook settings.Webhook `json:"webhook,omitempty"` + Resolver string `json:"resolver"` + IPInterface string `json:"ip_interface"` +} + +func (c *Controller) GetNetworkSettings(ctx *fiber.Ctx) error { + settings := NetworkSettings{ + IPMode: c.config.IPType, + IPUrls: c.config.IPUrls, + IPV6Urls: c.config.IPV6Urls, + UseProxy: c.config.UseProxy, + SkipSSLVerify: c.config.SkipSSLVerify, + Socks5Proxy: c.config.Socks5Proxy, + Webhook: c.config.Webhook, + Resolver: c.config.Resolver, + IPInterface: c.config.IPInterface, + } + + return ctx.JSON(settings) +} + +func (c *Controller) UpdateNetworkSettings(ctx *fiber.Ctx) error { + var settings NetworkSettings + if err := ctx.BodyParser(&settings); err != nil { + log.Errorf("Failed to parse request body: %s", err.Error()) + return ctx.Status(400).SendString(err.Error()) + } + + if settings.IPMode == utils.IPV4 && len(settings.IPUrls) == 0 { + return ctx.Status(400).SendString("IP URLs cannot be empty") + } + + if settings.IPMode == utils.IPV6 && len(settings.IPV6Urls) == 0 { + return ctx.Status(400).SendString("IPv6 URLs cannot be empty") + } + + c.config.IPType = settings.IPMode + if settings.IPMode == utils.IPV6 { + c.config.IPV6Urls = settings.IPV6Urls + } else { + c.config.IPUrls = settings.IPUrls + } + + c.config.UseProxy = settings.UseProxy + c.config.SkipSSLVerify = settings.SkipSSLVerify + c.config.Socks5Proxy = settings.Socks5Proxy + c.config.Webhook = settings.Webhook + c.config.Resolver = settings.Resolver + c.config.IPInterface = settings.IPInterface + + if err := c.config.SaveSettings(c.configPath); err != nil { + log.Errorf("Failed to save settings: %s", err.Error()) + return ctx.Status(500).SendString("Failed to save network settings") + } + + return ctx.JSON(settings) +} diff --git a/internal/server/server.go b/internal/server/server.go index 7cb8954c..ce17dd89 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -117,6 +117,10 @@ func (s *Server) initRoutes() { route.Get("/provider/settings", s.controller.GetProviderSettings) route.Put("/provider", s.controller.UpdateProvider) + // Network related routes + route.Get("/network", s.controller.GetNetworkSettings) + route.Put("/network", s.controller.UpdateNetworkSettings) + // Serve embedded files s.app.Use("/", filesystem.New(filesystem.Config{ Root: http.FS(embeddedFiles), diff --git a/web/api/network.ts b/web/api/network.ts new file mode 100644 index 00000000..23685804 --- /dev/null +++ b/web/api/network.ts @@ -0,0 +1,55 @@ +import { get_api_server } from "./env"; + +export interface WebHook { + enabled: boolean; + url: string; + request_body: string; +} + +export interface NetworkSettings { + ip_mode: string; + ip_urls?: string[]; + ipv6_urls?: string[]; + use_proxy: boolean; + skip_ssl_verify: boolean; + socks5_proxy: string; + webhook: WebHook; + resolver: string; + ip_interface: string; +} + +export async function get_network_settings(credentials: string): Promise { + if (credentials) { + const resp = await fetch(get_api_server() + '/api/v1/network', { + method: 'GET', + headers: { + 'Authorization': `Basic ${credentials}` + } + }) + + if (resp.status === 200) { + return resp.json(); + } + } + + return {} as NetworkSettings; +} + +export async function update_network_settings(credentials: string, settings: NetworkSettings): Promise { + if (credentials) { + const resp = await fetch(get_api_server() + '/api/v1/network', { + method: 'PUT', + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(settings) + }) + + if (resp.status === 200) { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/web/app/domains/page.tsx b/web/app/domains/page.tsx index 45c61345..1d5478a1 100644 --- a/web/app/domains/page.tsx +++ b/web/app/domains/page.tsx @@ -30,9 +30,6 @@ export default function Domains() {
- {/*
*/} - {/* Provider */} - {/*
*/}
diff --git a/web/app/network/page.tsx b/web/app/network/page.tsx new file mode 100644 index 00000000..3d0de3aa --- /dev/null +++ b/web/app/network/page.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { IpMode } from "@/components/ip-mode"; +import { Proxy } from "@/components/proxy"; +import { WebHook } from "@/components/webhook"; +import { Resolver } from "@/components/resolver"; +import { IPInterface } from "@/components/ip-interface"; +import { useRouter } from "next/navigation"; +import { CommonContext } from "@/components/user"; +import { useEffect, useState, useContext } from "react"; +import { get_network_settings, NetworkSettings, update_network_settings } from "@/api/network"; +import { get_info } from "@/api/info"; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +export default function Network() { + const router = useRouter(); + const userStore = useContext(CommonContext); + const { credentials, setCurrentPage, saveVersion } = userStore; + const [settings, setSettings] = useState({} as NetworkSettings); + + useEffect(() => { + if (!credentials) { + router.push('/login'); + return; + } + setCurrentPage('Network'); + get_info(credentials).then((info) => { + saveVersion(info.version); + }); + + get_network_settings(credentials).then((settings) => { + setSettings(settings); + }); + }, [credentials, router, setCurrentPage, saveVersion]); + + + return ( +
+ +
+
+ { + setSettings({ + ...settings, + ip_mode: data.IPMode, + ip_urls: data.IPUrls, + ipv6_urls: data.IPV6Urls + }); + }} + /> + { + setSettings({ + ...settings, + use_proxy: data.EnableProxy, + skip_ssl_verify: data.SkipSSLVerify, + socks5_proxy: data.Socks5Proxy + }); + }} + /> + { + settings.webhook ? + { + setSettings({ + ...settings, + webhook: { + enabled: data.Enabled, + url: data.Url, + request_body: data.RequestBody + } + }); + }} + /> : null + } + { + setSettings({ + ...settings, + resolver: data.Resolver + }); + }} + /> + { + setSettings({ + ...settings, + ip_interface: data.IPInterface + }); + }} + /> +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/components/icons.tsx b/web/components/icons.tsx index 70f2b3eb..e95757f7 100644 --- a/web/components/icons.tsx +++ b/web/components/icons.tsx @@ -98,6 +98,13 @@ export const MoonFilledIcon = ({ ); +export const InterfaceIcon = ({ +}) => ( + +); + export const SunFilledIcon = ({ size = 24, width, diff --git a/web/components/ip-interface.tsx b/web/components/ip-interface.tsx new file mode 100644 index 00000000..4373cb5e --- /dev/null +++ b/web/components/ip-interface.tsx @@ -0,0 +1,37 @@ +import { InterfaceIcon } from "./icons"; + +interface IPInterfaceProps { + IPInterface: string; + onIPInterfaceChange?: (data: IPInterfaceProps) => void; +} + +export const IPInterface = (props: IPInterfaceProps) => { + return ( +
+
+
IP Inerface
+
+
+ Set the network interface +
+ +
+
+ { + if (props.onIPInterfaceChange) { + props.onIPInterfaceChange({ + IPInterface: e.target.value + }); + } + }} + /> +
+
+
+ ) +} \ No newline at end of file diff --git a/web/components/ip-mode.tsx b/web/components/ip-mode.tsx new file mode 100644 index 00000000..ef39f108 --- /dev/null +++ b/web/components/ip-mode.tsx @@ -0,0 +1,66 @@ +import { SettingsIcon } from "@/components/icons"; +import { isIP } from "net"; +import { useState } from "react"; +interface IpModeProps { + IPMode: string; + IPUrls?: string[]; + IPV6Urls?: string[]; + onIpModeChagne?: (data: IpModeProps) => void; +} + +export const IpMode = (props: IpModeProps) => { + let isIPV6 = props.IPMode === 'IPV6' ? true : false; + + return ( +
+
+
+ +
+
IP Mode
+
{props.IPMode}
+
The current IP mode
+
+ Enable IPv6 + { + isIPV6 = !isIPV6; + if (props.onIpModeChagne) { + props.onIpModeChagne({ + IPMode: isIPV6 ? 'IPV6' : 'IPV4', + IPUrls: props.IPUrls, + IPV6Urls: props.IPV6Urls, + }); + } + }} + onChange={() => { }} + /> +
+
+ +
+
URLs
+
+