diff --git a/Makefile b/Makefile index 591d64b4..45e128fe 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ PWD := $(shell pwd) DOCKER_RUN_BASE := docker run -e UID=$(UID) -e GID=$(GID) -v $(PWD):/home/build/NanoKVM --rm # Build commands -GO_BUILD_CMD := cd /home/build/NanoKVM/server && go mod tidy && CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go build +GO_BUILD_CMD := cd /home/build/NanoKVM/server && go mod tidy && CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" CGO_LDFLAGS="-Wl,-rpath,\$$ORIGIN/dl_lib" go build SUPPORT_BUILD_CMD := . ./home/build/MaixCDK/bin/activate && cd /home/build/NanoKVM/support/sg2002 && ./build kvm_system && ./build kvm_system add_to_kvmapp .PHONY: help check-root builder-image rebuild-image check-image shell app support all clean diff --git a/kvmapp/system/init.d/S30eth b/kvmapp/system/init.d/S30eth index 409708ae..d92f737b 100755 --- a/kvmapp/system/init.d/S30eth +++ b/kvmapp/system/init.d/S30eth @@ -1,11 +1,125 @@ #!/bin/sh RESERVE_INET="192.168.90.1/22" +ETH_MODE_FILE="/etc/kvm/eth.mode" +ETH_PASS_FILE="/etc/kvm/eth.pass" +ETH_IDENTITY_FILE="/etc/kvm/eth.identity" +ETH_EAP_FILE="/etc/kvm/eth.eap" +ETH_PHASE2_FILE="/etc/kvm/eth.phase2" +ETH_ANON_IDENTITY_FILE="/etc/kvm/eth.anonymous_identity" +ETH_CA_CERT_FILE="/etc/kvm/eth.ca_cert" +ETH_CLIENT_CERT_FILE="/etc/kvm/eth.client_cert" +ETH_PRIVATE_KEY_FILE="/etc/kvm/eth.private_key" +ETH_PRIVATE_KEY_PASSWD_FILE="/etc/kvm/eth.private_key_passwd" +ETH_DOMAIN_SUFFIX_MATCH_FILE="/etc/kvm/eth.domain_suffix_match" +ETH_WPA_CONF="/etc/wpa_supplicant-wired.conf" +ETH_WPA_PID="/run/wpa_supplicant.eth0.pid" +ETH_DHCP_PID="/run/udhcpc.eth0.pid" +ETH_WPA_BIN="/usr/sbin/wpa_supplicant-wired" clean_line() { echo "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/\r//g' } +read_file_trimmed() { + file="$1" + [ -e "$file" ] || return 0 + tr -d '\r' <"$file" | sed 's/[[:space:]]*$//' +} + +append_quoted_value() { + key="$1" + value="$2" + [ -n "$value" ] || return 0 + escaped=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g') + printf ' %s="%s"\n' "$key" "$escaped" +} + +supports_wired_driver() { + if [ -x "$ETH_WPA_BIN" ]; then + return 0 + fi + wpa_supplicant -h 2>&1 | grep -q "wired = Wired Ethernet driver" +} + +get_wpa_bin() { + if [ -x "$ETH_WPA_BIN" ]; then + echo "$ETH_WPA_BIN" + return 0 + fi + command -v wpa_supplicant +} + +gen_wired_enterprise_conf() { + pass="$1" + identity="$2" + eap="$3" + phase2="$4" + anonymous_identity="$5" + ca_cert="$6" + domain_suffix_match="$7" + client_cert="$8" + private_key="$9" + private_key_passwd="${10}" + + echo "ctrl_interface=/var/run/wpa_supplicant" + echo "ap_scan=0" + echo "fast_reauth=1" + echo + echo "network={" + echo " key_mgmt=IEEE8021X" + [ -n "$eap" ] && echo " eap=$eap" + append_quoted_value "identity" "$identity" + append_quoted_value "password" "$pass" + append_quoted_value "phase2" "$phase2" + append_quoted_value "anonymous_identity" "$anonymous_identity" + append_quoted_value "ca_cert" "$ca_cert" + append_quoted_value "domain_suffix_match" "$domain_suffix_match" + append_quoted_value "client_cert" "$client_cert" + append_quoted_value "private_key" "$private_key" + append_quoted_value "private_key_passwd" "$private_key_passwd" + echo "}" +} + +stop_wired_auth() { + if [ -e "$ETH_WPA_PID" ]; then + kill "$(cat "$ETH_WPA_PID")" 2>/dev/null || true + rm -f "$ETH_WPA_PID" + fi +} + +start_wired_auth() { + mode=$(read_file_trimmed "$ETH_MODE_FILE") + case "$mode" in + enterprise|8021x|wpa-eap) + if ! supports_wired_driver; then + echo "S30eth: wired 802.1X driver is unavailable; skipping wpa_supplicant startup" >&2 + return 0 + fi + pass=$(read_file_trimmed "$ETH_PASS_FILE") + identity=$(read_file_trimmed "$ETH_IDENTITY_FILE") + eap=$(read_file_trimmed "$ETH_EAP_FILE") + phase2=$(read_file_trimmed "$ETH_PHASE2_FILE") + anonymous_identity=$(read_file_trimmed "$ETH_ANON_IDENTITY_FILE") + ca_cert=$(read_file_trimmed "$ETH_CA_CERT_FILE") + domain_suffix_match=$(read_file_trimmed "$ETH_DOMAIN_SUFFIX_MATCH_FILE") + client_cert=$(read_file_trimmed "$ETH_CLIENT_CERT_FILE") + private_key=$(read_file_trimmed "$ETH_PRIVATE_KEY_FILE") + private_key_passwd=$(read_file_trimmed "$ETH_PRIVATE_KEY_PASSWD_FILE") + + [ -n "$eap" ] || eap="PEAP" + + gen_wired_enterprise_conf "$pass" "$identity" "$eap" "$phase2" \ + "$anonymous_identity" "$ca_cert" "$domain_suffix_match" \ + "$client_cert" "$private_key" "$private_key_passwd" >"$ETH_WPA_CONF" + chmod 600 "$ETH_WPA_CONF" + ip link set eth0 up + stop_wired_auth + "$(get_wpa_bin)" -B -D wired -i eth0 -c "$ETH_WPA_CONF" -P "$ETH_WPA_PID" + ;; + esac +} + calc_gw() { addr="$1" netid="$2" @@ -23,7 +137,9 @@ EOF } start() { + stop_wired_auth ip addr flush dev eth0 + start_wired_auth if [ -e /boot/eth.nodhcp ]; then while IFS= read -r line || [ -n "$line" ]; do line=$(clean_line "$line") @@ -57,7 +173,7 @@ start() { done < /boot/eth.nodhcp ip a show dev eth0 | grep inet > /dev/null || { - udhcpc -i eth0 -t 3 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid &>/dev/null + udhcpc -i eth0 -t 3 -T 1 -A 5 -b -p "$ETH_DHCP_PID" &>/dev/null ip a show dev eth0 | grep inet > /dev/null } || { inet=$RESERVE_INET @@ -65,14 +181,16 @@ start() { ip a add "$inet" brd + dev eth0 } || exit 1 else - udhcpc -i eth0 -t 10 -T 1 -A 5 -b -p /run/udhcpc.eth0.pid & + udhcpc -i eth0 -t 10 -T 1 -A 5 -b -p "$ETH_DHCP_PID" & fi } stop() { - [ ! -e "/run/udhcpc.eth0.pid" ] && exit 1 - kill "$(cat /run/udhcpc.eth0.pid)" - rm /run/udhcpc.eth0.pid + if [ -e "$ETH_DHCP_PID" ]; then + kill "$(cat "$ETH_DHCP_PID")" 2>/dev/null || true + rm -f "$ETH_DHCP_PID" + fi + stop_wired_auth } case "$1" in @@ -83,4 +201,4 @@ case "$1" in $0 start ;; *) exit 1 ;; -esac \ No newline at end of file +esac diff --git a/kvmapp/system/init.d/S30wifi b/kvmapp/system/init.d/S30wifi index 6694eca2..359a1876 100755 --- a/kvmapp/system/init.d/S30wifi +++ b/kvmapp/system/init.d/S30wifi @@ -3,6 +3,87 @@ . /etc/profile APFLAG_FILE=/tmp/wifiap +WIFI_STATIC_CONFIG=/boot/wifi.nodhcp + +escape_wpa_value() { + printf "%s" "$1" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +sanitize_wpa_token() { + printf "%s" "$1" | tr -cd 'A-Za-z0-9_-' +} + +clean_line() { + echo "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/\r//g' +} + +append_wpa_field() { + key="${1}" + value="${2}" + if [ -n "$value" ]; then + printf ' %s="%s"\n' "$key" "$(escape_wpa_value "$value")" + fi +} + +calc_gw() { + addr="$1" + netid="$2" + IFS=. read a b c d <>24)&255 )).$(( (gwnum>>16)&255 )).$(( (gwnum>>8)&255 )).$(( gwnum&255 ))" +} + +gen_wpa_enterprise_conf() { + ssid="${1}" + pass="${2}" + identity="${3}" + eap="${4:-PEAP}" + phase2="${5}" + anonymous_identity="${6}" + ca_cert="${7}" + domain_suffix_match="${8}" + client_cert="${9}" + private_key="${10}" + private_key_passwd="${11}" + eap=$(sanitize_wpa_token "$eap") + case "$eap" in + PEAP | TTLS) + if [ -z "$phase2" ]; then + phase2="auth=MSCHAPV2" + fi + ;; + *) + phase2="" + ;; + esac + + echo "ctrl_interface=/var/run/wpa_supplicant" + echo "ap_scan=1" + echo "fast_reauth=1" + echo "" + echo "network={" + append_wpa_field "ssid" "$ssid" + echo " key_mgmt=WPA-EAP" + echo " eap=${eap}" + append_wpa_field "identity" "$identity" + append_wpa_field "password" "$pass" + append_wpa_field "phase2" "$phase2" + append_wpa_field "anonymous_identity" "$anonymous_identity" + append_wpa_field "ca_cert" "$ca_cert" + append_wpa_field "client_cert" "$client_cert" + append_wpa_field "private_key" "$private_key" + append_wpa_field "private_key_passwd" "$private_key_passwd" + append_wpa_field "domain_suffix_match" "$domain_suffix_match" + echo "}" +} gen_hostapd_conf() { ssid="${1}" @@ -52,16 +133,27 @@ start() { echo "wifi mode: sta" ssid="" pass="" + mode="psk" + identity="" + eap="PEAP" + phase2="auth=MSCHAPV2" + anonymous_identity="" + ca_cert="" + client_cert="" + private_key="" + private_key_passwd="" + domain_suffix_match="" if [ -e /boot/wifi.ssid ] && [ -e /boot/wifi.pass ]; then echo "Updating WiFi credentials from /boot to /etc/kvm/" - rm -f /etc/kvm/wifi.ssid /etc/kvm/wifi.pass + rm -f /etc/kvm/wifi.ssid /etc/kvm/wifi.pass /etc/kvm/wifi.mode mv /boot/wifi.ssid /etc/kvm/wifi.ssid mv /boot/wifi.pass /etc/kvm/wifi.pass chown root:root /etc/kvm/wifi.ssid /etc/kvm/wifi.pass - chmod 644 /etc/kvm/wifi.ssid /etc/kvm/wifi.pass + chmod 644 /etc/kvm/wifi.ssid + chmod 600 /etc/kvm/wifi.pass fi if [ -e /etc/kvm/wifi.ssid ]; then ssid=$(cat /etc/kvm/wifi.ssid) @@ -69,10 +161,89 @@ start() { if [ -e /etc/kvm/wifi.pass ]; then pass=$(cat /etc/kvm/wifi.pass) fi - echo "ctrl_interface=/var/run/wpa_supplicant" >/etc/wpa_supplicant.conf - wpa_passphrase "$ssid" "$pass" >>/etc/wpa_supplicant.conf + if [ -e /etc/kvm/wifi.mode ]; then + mode=$(cat /etc/kvm/wifi.mode) + fi + if [ -e /etc/kvm/wifi.identity ]; then + identity=$(cat /etc/kvm/wifi.identity) + fi + if [ -e /etc/kvm/wifi.eap ]; then + eap=$(cat /etc/kvm/wifi.eap) + fi + if [ -e /etc/kvm/wifi.phase2 ]; then + phase2=$(cat /etc/kvm/wifi.phase2) + fi + if [ -e /etc/kvm/wifi.anonymous_identity ]; then + anonymous_identity=$(cat /etc/kvm/wifi.anonymous_identity) + fi + if [ -e /etc/kvm/wifi.ca_cert ]; then + ca_cert=$(cat /etc/kvm/wifi.ca_cert) + fi + if [ -e /etc/kvm/wifi.client_cert ]; then + client_cert=$(cat /etc/kvm/wifi.client_cert) + fi + if [ -e /etc/kvm/wifi.private_key ]; then + private_key=$(cat /etc/kvm/wifi.private_key) + fi + if [ -e /etc/kvm/wifi.private_key_passwd ]; then + private_key_passwd=$(cat /etc/kvm/wifi.private_key_passwd) + fi + if [ -e /etc/kvm/wifi.domain_suffix_match ]; then + domain_suffix_match=$(cat /etc/kvm/wifi.domain_suffix_match) + fi + + case "$mode" in + enterprise | 8021x | wpa-eap) + gen_wpa_enterprise_conf "$ssid" "$pass" "$identity" "$eap" "$phase2" "$anonymous_identity" "$ca_cert" "$domain_suffix_match" "$client_cert" "$private_key" "$private_key_passwd" >/etc/wpa_supplicant.conf + ;; + *) + echo "ctrl_interface=/var/run/wpa_supplicant" >/etc/wpa_supplicant.conf + wpa_passphrase "$ssid" "$pass" >>/etc/wpa_supplicant.conf + ;; + esac + chmod 600 /etc/wpa_supplicant.conf wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf - if [ ! -e /boot/wifi.nodhcp ]; then + ifconfig wlan0 up + ip route del default || true + ip addr flush dev wlan0 + if [ -e "$WIFI_STATIC_CONFIG" ]; then + while IFS= read -r line || [ -n "$line" ]; do + line=$(clean_line "$line") + [ -z "$line" ] && continue + set -- $line + inet="$1" + gw="$2" + [ -z "$inet" ] && continue + case "$inet" in + */*) + netid_check=${inet#*/} + if ! echo "$netid_check" | grep -qE '^[0-9]+$' || [ $netid_check -lt 1 ] 2>/dev/null || [ $netid_check -gt 32 ] 2>/dev/null; then + inet="${inet%/*}/16" + fi + ;; + *) + inet="$inet/16" + ;; + esac + addr=${inet%/*} + netid=${inet#*/} + [ -z "$gw" ] && gw=$(calc_gw "$addr" "$netid") + arping -Dqc2 -Iwlan0 "$addr" || continue + ip a add "$inet" brd + dev wlan0 + ip r add default via "$gw" dev wlan0 + echo -e "nameserver $gw" >> /etc/resolv.conf + break + done < "$WIFI_STATIC_CONFIG" + + ip a show dev wlan0 | grep inet > /dev/null || { + udhcpc -i wlan0 -t 3 -T 1 -A 5 -b -p /run/udhcpc.wlan0.pid &>/dev/null + ip a show dev wlan0 | grep inet > /dev/null + } || { + inet=192.168.90.1/22 + addr=${inet%/*} + ip a add "$inet" brd + dev wlan0 + } || exit 1 + else (udhcpc -i wlan0 -t 10 -T 1 -A 5 -b -p /run/udhcpc.wlan0.pid) & fi } diff --git a/server/proto/network.go b/server/proto/network.go index 966e6b23..b49d25d5 100644 --- a/server/proto/network.go +++ b/server/proto/network.go @@ -18,15 +18,84 @@ type SetMacNameReq struct { } type GetWifiRsp struct { - Supported bool `json:"supported"` - ApMode bool `json:"apMode"` - Connected bool `json:"connected"` - Ssid string `json:"ssid"` + Supported bool `json:"supported"` + ApMode bool `json:"apMode"` + Connected bool `json:"connected"` + Ssid string `json:"ssid"` + Mode string `json:"mode"` + IPMode string `json:"ipMode"` + Address string `json:"address"` + SubnetMask string `json:"subnetMask"` + Gateway string `json:"gateway"` + PasswordSet bool `json:"passwordSet"` + Identity string `json:"identity"` + EAP string `json:"eap"` + Phase2 string `json:"phase2"` + AnonymousIdentity string `json:"anonymousIdentity"` + CACert string `json:"caCert"` + ClientCert string `json:"clientCert"` + PrivateKey string `json:"privateKey"` + PrivateKeyPasswdSet bool `json:"privateKeyPasswdSet"` + DomainSuffixMatch string `json:"domainSuffixMatch"` } type ConnectWifiReq struct { - Ssid string `validate:"required"` - Password string `validate:"required"` + Ssid string `json:"ssid" form:"ssid"` + Password string `json:"password" form:"password"` + IPMode string `json:"ipMode" form:"ipMode"` + Address string `json:"address" form:"address"` + SubnetMask string `json:"subnetMask" form:"subnetMask"` + Gateway string `json:"gateway" form:"gateway"` + Mode string `json:"mode" form:"mode"` + Identity string `json:"identity" form:"identity"` + EAP string `json:"eap" form:"eap"` + Phase2 string `json:"phase2" form:"phase2"` + AnonymousIdentity string `json:"anonymousIdentity" form:"anonymousIdentity"` + CACert string `json:"caCert" form:"caCert"` + ClientCert string `json:"clientCert" form:"clientCert"` + PrivateKey string `json:"privateKey" form:"privateKey"` + PrivateKeyPasswd string `json:"privateKeyPasswd" form:"privateKeyPasswd"` + DomainSuffixMatch string `json:"domainSuffixMatch" form:"domainSuffixMatch"` +} + +type GetEthernetRsp struct { + Supported bool `json:"supported"` + Mode string `json:"mode"` + Configured bool `json:"configured"` + Connected bool `json:"connected"` + Interface string `json:"interface"` + IPMode string `json:"ipMode"` + Address string `json:"address"` + SubnetMask string `json:"subnetMask"` + Gateway string `json:"gateway"` + PasswordSet bool `json:"passwordSet"` + Identity string `json:"identity"` + EAP string `json:"eap"` + Phase2 string `json:"phase2"` + AnonymousIdentity string `json:"anonymousIdentity"` + CACert string `json:"caCert"` + ClientCert string `json:"clientCert"` + PrivateKey string `json:"privateKey"` + PrivateKeyPasswdSet bool `json:"privateKeyPasswdSet"` + DomainSuffixMatch string `json:"domainSuffixMatch"` +} + +type SetEthernetReq struct { + Mode string `json:"mode" form:"mode"` + IPMode string `json:"ipMode" form:"ipMode"` + Address string `json:"address" form:"address"` + SubnetMask string `json:"subnetMask" form:"subnetMask"` + Gateway string `json:"gateway" form:"gateway"` + Password string `json:"password" form:"password"` + Identity string `json:"identity" form:"identity"` + EAP string `json:"eap" form:"eap"` + Phase2 string `json:"phase2" form:"phase2"` + AnonymousIdentity string `json:"anonymousIdentity" form:"anonymousIdentity"` + CACert string `json:"caCert" form:"caCert"` + ClientCert string `json:"clientCert" form:"clientCert"` + PrivateKey string `json:"privateKey" form:"privateKey"` + PrivateKeyPasswd string `json:"privateKeyPasswd" form:"privateKeyPasswd"` + DomainSuffixMatch string `json:"domainSuffixMatch" form:"domainSuffixMatch"` } type GetDNSRsp struct { @@ -35,6 +104,7 @@ type GetDNSRsp struct { Effective []string `json:"effective"` DHCP []string `json:"dhcp"` Info DNSInfo `json:"info"` + Infos []DNSInfo `json:"infos"` } type SetDNSReq struct { @@ -48,5 +118,8 @@ type DNSInfo struct { Address string `json:"address"` SubnetMask string `json:"subnetMask"` Gateway string `json:"gateway"` + Signal string `json:"signal"` + RxRate string `json:"rxRate"` + TxRate string `json:"txRate"` SearchDomains []string `json:"searchDomains"` } diff --git a/server/proto/request.go b/server/proto/request.go index f0f3b39d..8dcf45ad 100644 --- a/server/proto/request.go +++ b/server/proto/request.go @@ -19,10 +19,6 @@ func ValidateRequest(req interface{}) error { return err } - if env == "" || env == "debug" { - log.Debugf("request: %+v\n", req) - } - return nil } diff --git a/server/router/network.go b/server/router/network.go index 9aeb4b6a..8e3b74c2 100644 --- a/server/router/network.go +++ b/server/router/network.go @@ -23,6 +23,8 @@ func networkRouter(r *gin.Engine) { api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information api.POST("/network/wifi/connect", service.ConnectWifi) // connect Wi-Fi api.POST("/network/wifi/disconnect", service.DisconnectWifi) // disconnect Wi-Fi + api.GET("/network/ethernet", service.GetEthernet) // get Ethernet 802.1X information + api.POST("/network/ethernet", service.SetEthernet) // set Ethernet 802.1X configuration api.GET("/network/dns", service.GetDNS) // get DNS configuration api.POST("/network/dns", service.SetDNS) // set DNS configuration diff --git a/server/service/network/dns.go b/server/service/network/dns.go index 8bce7a6f..9076d1e4 100644 --- a/server/service/network/dns.go +++ b/server/service/network/dns.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strconv" "strings" @@ -59,6 +60,10 @@ func (s *Service) GetDNS(c *gin.Context) { dhcpConfig, _ := readDHCPResolvConfig(canFallbackEffectiveForDHCP()) dhcp := dhcpConfig.Servers info := getDNSInfo() + infos := getDNSInfos() + if len(infos) == 0 && info.Interface != "" { + infos = []proto.DNSInfo{info} + } rsp.OkRspWithData(c, &proto.GetDNSRsp{ Mode: mode, @@ -66,8 +71,9 @@ func (s *Service) GetDNS(c *gin.Context) { Effective: effective, DHCP: dhcp, Info: info, + Infos: infos, }) - log.Debugf("get dns config: mode=%s servers=%v effective=%v dhcp=%v info=%+v", mode, servers, effective, dhcp, info) + log.Debugf("get dns config: mode=%s servers=%v effective=%v dhcp=%v info=%+v infos=%d", mode, servers, effective, dhcp, info, len(infos)) } func (s *Service) SetDNS(c *gin.Context) { @@ -379,6 +385,12 @@ func getDNSInfo() proto.DNSInfo { if ifaceName == "" { ifaceName = getFallbackIPv4Interface() } + if ifaceName != "" { + ifaceGateway := getInterfaceGateway(ifaceName) + if ifaceGateway != "" { + gateway = ifaceGateway + } + } info := proto.DNSInfo{ Interface: ifaceName, @@ -399,10 +411,106 @@ func getDNSInfo() proto.DNSInfo { info.Type = getDNSInterfaceType(iface.Name) info.Address, info.SubnetMask = getIPv4AddressInfo(*iface) + if strings.EqualFold(info.Type, "Wireless") { + info.Signal, info.RxRate, info.TxRate = getWirelessLinkInfo(iface.Name) + } return info } +func getDNSInfos() []proto.DNSInfo { + interfaces, err := net.Interfaces() + if err != nil { + return nil + } + + infos := make([]proto.DNSInfo, 0, len(interfaces)) + + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + + ifaceType := getDNSInterfaceType(iface.Name) + if ifaceType == "" { + continue + } + + address, subnetMask := getIPv4AddressInfo(iface) + if address == "" { + continue + } + + info := proto.DNSInfo{ + Interface: iface.Name, + Type: ifaceType, + Address: address, + SubnetMask: subnetMask, + Gateway: getInterfaceGateway(iface.Name), + } + if ifaceType == "Wireless" { + info.Signal, info.RxRate, info.TxRate = getWirelessLinkInfo(iface.Name) + } + + infos = append(infos, info) + } + + sort.SliceStable(infos, func(i, j int) bool { + if infos[i].Type != infos[j].Type { + return infos[i].Type == "Wired" + } + return infos[i].Interface < infos[j].Interface + }) + + return infos +} + +func getWirelessLinkInfo(ifaceName string) (string, string, string) { + output, err := exec.Command("iw", "dev", ifaceName, "link").CombinedOutput() + if err != nil { + return "", "", "" + } + + var signal string + var rxRate string + var txRate string + + scanner := bufio.NewScanner(strings.NewReader(string(output))) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + switch { + case strings.HasPrefix(line, "signal:"): + signal = strings.TrimSpace(strings.TrimPrefix(line, "signal:")) + case strings.HasPrefix(line, "rx bitrate:"): + rxRate = strings.TrimSpace(strings.TrimPrefix(line, "rx bitrate:")) + case strings.HasPrefix(line, "tx bitrate:"): + txRate = strings.TrimSpace(strings.TrimPrefix(line, "tx bitrate:")) + } + } + + return signal, rxRate, txRate +} + +func getInterfaceGateway(ifaceName string) string { + if strings.TrimSpace(ifaceName) == "" { + return "" + } + + out, err := exec.Command("ip", "route", "show", "dev", ifaceName).CombinedOutput() + if err != nil { + return "" + } + + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + if len(fields) >= 3 && fields[0] == "default" && fields[1] == "via" { + return fields[2] + } + } + + return "" +} + func getDefaultIPv4Route() (string, string) { file, err := os.Open("/proc/net/route") if err != nil { @@ -486,8 +594,7 @@ func getIPv4AddressInfo(iface net.Interface) (string, string) { continue } - ones, _ := ipNet.Mask.Size() - return fmt.Sprintf("%s/%d", ip.String(), ones), net.IP(ipNet.Mask).String() + return ip.String(), net.IP(ipNet.Mask).String() } return "", "" diff --git a/server/service/network/ethernet.go b/server/service/network/ethernet.go new file mode 100644 index 00000000..501d2f82 --- /dev/null +++ b/server/service/network/ethernet.go @@ -0,0 +1,468 @@ +package network + +import ( + "fmt" + "net" + "os" + "os/exec" + "strings" + "time" + + "NanoKVM-Server/proto" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +const ( + ethernetIface = "eth0" + ethernetModeOff = "off" + ethernetIPModeDHCP = "dhcp" + ethernetIPModeManual = "manual" + EthernetMode = "/etc/kvm/eth.mode" + EthernetPasswd = "/etc/kvm/eth.pass" + EthernetIdentity = "/etc/kvm/eth.identity" + EthernetEAP = "/etc/kvm/eth.eap" + EthernetPhase2 = "/etc/kvm/eth.phase2" + EthernetAnonymousIdentity = "/etc/kvm/eth.anonymous_identity" + EthernetCACert = "/etc/kvm/eth.ca_cert" + EthernetClientCert = "/etc/kvm/eth.client_cert" + EthernetPrivateKey = "/etc/kvm/eth.private_key" + EthernetPrivateKeyPasswd = "/etc/kvm/eth.private_key_passwd" + EthernetDomainSuffixMatch = "/etc/kvm/eth.domain_suffix_match" + EthernetStaticConfig = "/boot/eth.nodhcp" + EthernetScript = "/etc/init.d/S30eth" +) + +func (s *Service) GetEthernet(c *gin.Context) { + var rsp proto.Response + + mode := currentEthernetMode() + + rsp.OkRspWithData(c, &proto.GetEthernetRsp{ + Supported: supportsWired8021X(), + Mode: mode, + Configured: mode == "enterprise", + Connected: isEthernetConnected(), + Interface: ethernetIface, + IPMode: currentEthernetIPMode(), + Address: readEthernetStaticAddress(), + SubnetMask: readEthernetStaticSubnetMask(), + Gateway: readEthernetStaticGateway(), + PasswordSet: fileExists(EthernetPasswd), + Identity: readTrimmedFile(EthernetIdentity), + EAP: readTrimmedFile(EthernetEAP), + Phase2: readTrimmedFile(EthernetPhase2), + AnonymousIdentity: readTrimmedFile(EthernetAnonymousIdentity), + CACert: readTrimmedFile(EthernetCACert), + ClientCert: readTrimmedFile(EthernetClientCert), + PrivateKey: readTrimmedFile(EthernetPrivateKey), + PrivateKeyPasswdSet: fileExists(EthernetPrivateKeyPasswd), + DomainSuffixMatch: readTrimmedFile(EthernetDomainSuffixMatch), + }) +} + +func (s *Service) SetEthernet(c *gin.Context) { + var req proto.SetEthernetReq + var rsp proto.Response + + if err := parseEthernetRequest(c, &req); err != nil { + rsp.ErrRsp(c, -1, "invalid parameters") + return + } + + if err := setEthernet(req); err != nil { + rsp.ErrRsp(c, -2, "failed to configure ethernet") + return + } + + time.Sleep(5 * time.Second) + + rsp.OkRsp(c) + log.Debugf("set ethernet config: mode=%q identity_set=%t eap=%q", req.Mode, req.Identity != "", req.EAP) +} + +func parseEthernetRequest(c *gin.Context, req *proto.SetEthernetReq) error { + if err := c.ShouldBind(req); err != nil { + log.Errorf("parse ethernet request failed, err: %s", err) + return err + } + + req.Mode = normalizeEthernetMode(req.Mode) + req.IPMode = normalizeEthernetIPMode(req.IPMode) + req.Address = strings.TrimSpace(req.Address) + req.SubnetMask = strings.TrimSpace(req.SubnetMask) + req.Gateway = strings.TrimSpace(req.Gateway) + req.EAP = strings.TrimSpace(req.EAP) + req.Phase2 = strings.TrimSpace(req.Phase2) + req.Identity = strings.TrimSpace(req.Identity) + req.AnonymousIdentity = strings.TrimSpace(req.AnonymousIdentity) + req.CACert = strings.TrimSpace(req.CACert) + req.ClientCert = strings.TrimSpace(req.ClientCert) + req.PrivateKey = strings.TrimSpace(req.PrivateKey) + req.PrivateKeyPasswd = strings.TrimSpace(req.PrivateKeyPasswd) + req.DomainSuffixMatch = strings.TrimSpace(req.DomainSuffixMatch) + if err := validateEthernetIPConfig(req.IPMode, req.Address, req.SubnetMask, req.Gateway); err != nil { + return err + } + + if !isEnterpriseWiFiMode(req.Mode) { + req.Mode = ethernetModeOff + req.IPMode = normalizeEthernetIPMode(req.IPMode) + req.Password = "" + req.Identity = "" + req.EAP = "" + req.Phase2 = "" + req.AnonymousIdentity = "" + req.CACert = "" + req.ClientCert = "" + req.PrivateKey = "" + req.PrivateKeyPasswd = "" + req.DomainSuffixMatch = "" + return nil + } + + if hasLineBreak(req.Password, req.Identity, req.EAP, req.Phase2, req.AnonymousIdentity, req.CACert, req.ClientCert, req.PrivateKey, req.PrivateKeyPasswd, req.DomainSuffixMatch, req.Address, req.SubnetMask, req.Gateway) { + return fmt.Errorf("ethernet parameters must not contain line breaks") + } + if req.EAP == "" { + req.EAP = "PEAP" + } + req.EAP = strings.ToUpper(req.EAP) + if !isSupportedEAP(req.EAP) { + return fmt.Errorf("unsupported eap method") + } + if req.Phase2 == "" { + req.Phase2 = defaultPhase2(req.EAP) + } + if req.EAP != "PEAP" && req.EAP != "TTLS" { + req.Phase2 = "" + } + if req.EAP != "TLS" { + req.ClientCert = "" + req.PrivateKey = "" + req.PrivateKeyPasswd = "" + } + if req.Identity == "" { + return fmt.Errorf("identity is required for enterprise ethernet") + } + if usesPassword(req.EAP) && req.Password == "" && !fileExists(EthernetPasswd) { + return fmt.Errorf("password is required for %s", req.EAP) + } + if req.EAP == "TLS" && (req.ClientCert == "" || req.PrivateKey == "") { + return fmt.Errorf("client certificate and private key are required for TLS") + } + if req.EAP == "TLS" { + req.Password = "" + } + + return nil +} + +func setEthernet(req proto.SetEthernetReq) error { + if err := saveEthernetIPConfig(req.IPMode, req.Address, req.SubnetMask, req.Gateway); err != nil { + return err + } + + if req.Mode == ethernetModeOff { + _ = os.Remove(EthernetPasswd) + removeEnterpriseEthernetFiles() + return restartEthernet() + } + + if strings.TrimSpace(req.Password) != "" { + if err := os.WriteFile(EthernetPasswd, []byte(req.Password), 0o600); err != nil { + log.Errorf("failed to save ethernet password: %s", err) + return err + } + } else if usesPassword(req.EAP) && !fileExists(EthernetPasswd) { + return fmt.Errorf("missing existing ethernet password") + } + if err := os.WriteFile(EthernetMode, []byte(req.Mode), 0o644); err != nil { + log.Errorf("failed to save ethernet mode: %s", err) + return err + } + if err := writeEnterpriseEthernetFiles(req); err != nil { + return err + } + + return restartEthernet() +} + +func writeEnterpriseEthernetFiles(req proto.SetEthernetReq) error { + files := map[string]string{ + EthernetIdentity: req.Identity, + EthernetEAP: req.EAP, + EthernetPhase2: req.Phase2, + EthernetAnonymousIdentity: req.AnonymousIdentity, + EthernetCACert: req.CACert, + EthernetClientCert: req.ClientCert, + EthernetPrivateKey: req.PrivateKey, + EthernetDomainSuffixMatch: req.DomainSuffixMatch, + } + + for path, value := range files { + if strings.TrimSpace(value) == "" { + _ = os.Remove(path) + continue + } + if err := os.WriteFile(path, []byte(value), 0o600); err != nil { + log.Errorf("failed to save enterprise ethernet file %s: %s", path, err) + return err + } + } + + if strings.TrimSpace(req.PrivateKeyPasswd) != "" { + if err := os.WriteFile(EthernetPrivateKeyPasswd, []byte(req.PrivateKeyPasswd), 0o600); err != nil { + log.Errorf("failed to save enterprise ethernet file %s: %s", EthernetPrivateKeyPasswd, err) + return err + } + } else if req.EAP != "TLS" { + _ = os.Remove(EthernetPrivateKeyPasswd) + } + + return nil +} + +func removeEnterpriseEthernetFiles() { + for _, path := range []string{ + EthernetMode, + EthernetIdentity, + EthernetEAP, + EthernetPhase2, + EthernetAnonymousIdentity, + EthernetCACert, + EthernetClientCert, + EthernetPrivateKey, + EthernetPrivateKeyPasswd, + EthernetDomainSuffixMatch, + } { + _ = os.Remove(path) + } +} + +func restartEthernet() error { + cmd := exec.Command(EthernetScript, "restart") + output, err := cmd.CombinedOutput() + if err != nil { + log.Errorf("failed to restart ethernet: %s", output) + return err + } + + return nil +} + +func normalizeEthernetMode(mode string) string { + mode = strings.ToLower(strings.TrimSpace(mode)) + if mode == "" { + return ethernetModeOff + } + return mode +} + +func normalizeEthernetIPMode(mode string) string { + mode = strings.ToLower(strings.TrimSpace(mode)) + if mode == ethernetIPModeManual { + return ethernetIPModeManual + } + return ethernetIPModeDHCP +} + +func currentEthernetMode() string { + data, err := os.ReadFile(EthernetMode) + if err != nil { + return ethernetModeOff + } + + mode := strings.TrimSpace(string(data)) + if !isEnterpriseWiFiMode(mode) { + return ethernetModeOff + } + + return mode +} + +func readTrimmedFile(path string) string { + data, err := os.ReadFile(path) + if err != nil { + return "" + } + + return strings.TrimSpace(string(data)) +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func validateEthernetIPConfig(mode, address, subnetMask, gateway string) error { + if mode != ethernetIPModeManual { + return nil + } + if address == "" { + return fmt.Errorf("static ethernet address is required") + } + ip := net.ParseIP(address) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("invalid static ethernet address") + } + if subnetMask == "" { + return fmt.Errorf("static ethernet subnet mask is required") + } + ones, err := subnetMaskToPrefix(subnetMask) + if err != nil || ones < 1 || ones > 32 { + return fmt.Errorf("invalid static ethernet subnet") + } + if gateway != "" { + gwIP := net.ParseIP(gateway) + if gwIP == nil || gwIP.To4() == nil { + return fmt.Errorf("invalid static ethernet gateway") + } + } + return nil +} + +func currentEthernetIPMode() string { + if fileExists(EthernetStaticConfig) { + return ethernetIPModeManual + } + return ethernetIPModeDHCP +} + +func saveEthernetIPConfig(mode, address, subnetMask, gateway string) error { + if mode != ethernetIPModeManual { + if err := os.Remove(EthernetStaticConfig); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove ethernet static config: %w", err) + } + return nil + } + + prefix, err := subnetMaskToPrefix(subnetMask) + if err != nil { + return fmt.Errorf("failed to parse ethernet subnet mask: %w", err) + } + + line := fmt.Sprintf("%s/%d", address, prefix) + if gateway != "" { + line += " " + gateway + } + line += "\n" + if err := os.WriteFile(EthernetStaticConfig, []byte(line), 0o644); err != nil { + return fmt.Errorf("failed to write ethernet static config: %w", err) + } + return nil +} + +func readEthernetStaticAddress() string { + address, _, _ := parseEthernetStaticConfig() + return address +} + +func readEthernetStaticSubnetMask() string { + _, subnetMask, _ := parseEthernetStaticConfig() + return subnetMask +} + +func readEthernetStaticGateway() string { + _, _, gateway := parseEthernetStaticConfig() + return gateway +} + +func parseEthernetStaticConfig() (string, string, string) { + data, err := os.ReadFile(EthernetStaticConfig) + if err != nil { + return "", "", "" + } + fields := strings.Fields(string(data)) + if len(fields) == 0 { + return "", "", "" + } + address := strings.TrimSpace(fields[0]) + subnetMask := "" + gateway := "" + + if strings.Contains(address, "/") { + if ip, ipNet, err := net.ParseCIDR(address); err == nil && ip != nil && ip.To4() != nil { + address = ip.String() + subnetMask = net.IP(ipNet.Mask).String() + } + if len(fields) > 1 { + gateway = strings.TrimSpace(fields[1]) + } + return address, subnetMask, gateway + } + + if len(fields) > 1 { + second := strings.TrimSpace(fields[1]) + if isValidSubnetMask(second) { + subnetMask = second + if len(fields) > 2 { + gateway = strings.TrimSpace(fields[2]) + } + } else { + gateway = second + } + } + + return address, subnetMask, gateway +} + +func subnetMaskToPrefix(mask string) (int, error) { + ip := net.ParseIP(strings.TrimSpace(mask)) + if ip == nil || ip.To4() == nil { + return 0, fmt.Errorf("invalid subnet mask") + } + + ones, bits := net.IPMask(ip.To4()).Size() + if bits != 32 || ones < 1 || ones > 32 { + return 0, fmt.Errorf("invalid subnet mask") + } + + return ones, nil +} + +func isValidSubnetMask(mask string) bool { + _, err := subnetMaskToPrefix(mask) + return err == nil +} + +func supportsWired8021X() bool { + if _, err := os.Stat("/usr/sbin/wpa_supplicant-wired"); err == nil { + return true + } + + out, err := exec.Command("sh", "-c", "wpa_supplicant -h 2>&1").CombinedOutput() + if err != nil { + return false + } + + return strings.Contains(string(out), "wired = Wired Ethernet driver") +} + +func isEthernetConnected() bool { + iface, err := net.InterfaceByName(ethernetIface) + if err != nil { + return false + } + if iface.Flags&net.FlagUp == 0 { + return false + } + + addrs, err := iface.Addrs() + if err != nil { + return false + } + + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok || ipNet.IP == nil || ipNet.IP.IsLoopback() { + continue + } + if ipNet.IP.To4() != nil { + return true + } + } + + return false +} diff --git a/server/service/network/wifi.go b/server/service/network/wifi.go index d163d343..cdd86103 100644 --- a/server/service/network/wifi.go +++ b/server/service/network/wifi.go @@ -3,6 +3,7 @@ package network import ( "crypto/subtle" "fmt" + "net" "os" "os/exec" "strings" @@ -15,37 +16,60 @@ import ( ) const ( - WiFiExistFile = "/etc/kvm/wifi_exist" - WiFiApModeFile = "/tmp/wifiap" - WiFiSSID = "/etc/kvm/wifi.ssid" - WiFiPasswd = "/etc/kvm/wifi.pass" - WiFiConnect = "/kvmapp/kvm/wifi_try_connect" - WiFiStateFile = "/kvmapp/kvm/wifi_state" - WiFiScript = "/etc/init.d/S30wifi" - WiFiApPassFile = "/kvmapp/kvm/ap.pass" + WiFiExistFile = "/etc/kvm/wifi_exist" + WiFiApModeFile = "/tmp/wifiap" + WiFiIPModeDHCP = "dhcp" + WiFiIPModeManual = "manual" + WiFiSSID = "/etc/kvm/wifi.ssid" + WiFiPasswd = "/etc/kvm/wifi.pass" + WiFiMode = "/etc/kvm/wifi.mode" + WiFiIdentity = "/etc/kvm/wifi.identity" + WiFiEAP = "/etc/kvm/wifi.eap" + WiFiPhase2 = "/etc/kvm/wifi.phase2" + WiFiAnonymousIdentity = "/etc/kvm/wifi.anonymous_identity" + WiFiCACert = "/etc/kvm/wifi.ca_cert" + WiFiClientCert = "/etc/kvm/wifi.client_cert" + WiFiPrivateKey = "/etc/kvm/wifi.private_key" + WiFiPrivateKeyPasswd = "/etc/kvm/wifi.private_key_passwd" + WiFiDomainSuffixMatch = "/etc/kvm/wifi.domain_suffix_match" + WiFiStaticConfig = "/boot/wifi.nodhcp" + WiFiConnect = "/kvmapp/kvm/wifi_try_connect" + WiFiStateFile = "/kvmapp/kvm/wifi_state" + WiFiScript = "/etc/init.d/S30wifi" + WiFiApPassFile = "/kvmapp/kvm/ap.pass" ) func (s *Service) GetWifi(c *gin.Context) { var rsp proto.Response - data := &proto.GetWifiRsp{} - - data.Supported = isSupported() - if !data.Supported { - rsp.OkRspWithData(c, data) - return + data := &proto.GetWifiRsp{ + Supported: isSupported(), + ApMode: isAPMode(), + Connected: isConnected(), + Ssid: readTrimmedFile(WiFiSSID), + Mode: currentWiFiMode(), + IPMode: currentWiFiIPMode(), + Address: readWiFiStaticAddress(), + SubnetMask: readWiFiStaticSubnetMask(), + Gateway: readWiFiStaticGateway(), + PasswordSet: fileExists(WiFiPasswd), + Identity: readTrimmedFile(WiFiIdentity), + EAP: readTrimmedFile(WiFiEAP), + Phase2: readTrimmedFile(WiFiPhase2), + AnonymousIdentity: readTrimmedFile(WiFiAnonymousIdentity), + CACert: readTrimmedFile(WiFiCACert), + ClientCert: readTrimmedFile(WiFiClientCert), + PrivateKey: readTrimmedFile(WiFiPrivateKey), + PrivateKeyPasswdSet: fileExists(WiFiPrivateKeyPasswd), + DomainSuffixMatch: readTrimmedFile(WiFiDomainSuffixMatch), } - data.ApMode = isAPMode() - - data.Connected = isConnected() - if !data.Connected { + if !data.Supported { rsp.OkRspWithData(c, data) return } - data.Ssid = getWiFiSsid() - if data.Ssid == "" { + if data.Connected && data.Ssid == "" { data.Ssid = "Wi-Fi" } @@ -73,13 +97,13 @@ func (s *Service) ConnectWifiNoAuth(c *gin.Context) { return } - if err := proto.ParseFormRequest(c, &req); err != nil { + if err := parseConnectWifiRequest(c, &req); err != nil { time.Sleep(1 * time.Second) rsp.ErrRsp(c, -2, "invalid parameters") return } - if err := connect(req.Ssid, req.Password); err != nil { + if err := connect(req); err != nil { rsp.ErrRsp(c, -3, "failed to connect wifi") return } @@ -114,12 +138,12 @@ func (s *Service) ConnectWifi(c *gin.Context) { var req proto.ConnectWifiReq var rsp proto.Response - if err := proto.ParseFormRequest(c, &req); err != nil { + if err := parseConnectWifiRequest(c, &req); err != nil { rsp.ErrRsp(c, -1, "invalid parameters") return } - if err := connect(req.Ssid, req.Password); err != nil { + if err := connect(req); err != nil { rsp.ErrRsp(c, -2, "failed to connect wifi") return } @@ -158,24 +182,159 @@ func (s *Service) DisconnectWifi(c *gin.Context) { time.Sleep(5 * time.Second) + _ = os.Remove(WiFiStaticConfig) _ = os.Remove(WiFiSSID) _ = os.Remove(WiFiPasswd) + _ = os.Remove(WiFiMode) + removeEnterpriseWiFiFiles() rsp.OkRsp(c) log.Debugf("stop wifi successfully") } -func connect(ssid string, password string) error { - if err := os.WriteFile(WiFiSSID, []byte(ssid), 0o644); err != nil { +func parseConnectWifiRequest(c *gin.Context, req *proto.ConnectWifiReq) error { + if err := c.ShouldBind(req); err != nil { + log.Errorf("parse wifi request failed, err: %s", err) + return err + } + + req.Ssid = strings.TrimSpace(req.Ssid) + req.Mode = normalizeWiFiMode(req.Mode) + req.IPMode = normalizeWiFiIPMode(req.IPMode) + req.Address = strings.TrimSpace(req.Address) + req.SubnetMask = strings.TrimSpace(req.SubnetMask) + req.Gateway = strings.TrimSpace(req.Gateway) + req.EAP = strings.TrimSpace(req.EAP) + req.Phase2 = strings.TrimSpace(req.Phase2) + req.Identity = strings.TrimSpace(req.Identity) + req.AnonymousIdentity = strings.TrimSpace(req.AnonymousIdentity) + req.CACert = strings.TrimSpace(req.CACert) + req.ClientCert = strings.TrimSpace(req.ClientCert) + req.PrivateKey = strings.TrimSpace(req.PrivateKey) + req.PrivateKeyPasswd = strings.TrimSpace(req.PrivateKeyPasswd) + req.DomainSuffixMatch = strings.TrimSpace(req.DomainSuffixMatch) + + if req.Ssid == "" { + return fmt.Errorf("ssid is required") + } + if err := validateWiFiIPConfig(req.IPMode, req.Address, req.SubnetMask, req.Gateway); err != nil { + return err + } + if !isEnterpriseWiFiMode(req.Mode) && req.Password == "" && !fileExists(WiFiPasswd) { + return fmt.Errorf("password is required") + } + if hasLineBreak(req.Ssid, req.Password, req.Identity, req.EAP, req.Phase2, req.AnonymousIdentity, req.CACert, req.ClientCert, req.PrivateKey, req.PrivateKeyPasswd, req.DomainSuffixMatch, req.Address, req.SubnetMask, req.Gateway) { + return fmt.Errorf("wifi parameters must not contain line breaks") + } + + if req.EAP == "" { + req.EAP = "PEAP" + } + req.EAP = strings.ToUpper(req.EAP) + if !isSupportedEAP(req.EAP) { + return fmt.Errorf("unsupported eap method") + } + if req.Phase2 == "" { + req.Phase2 = defaultPhase2(req.EAP) + } + if req.EAP != "PEAP" && req.EAP != "TTLS" { + req.Phase2 = "" + } + if req.EAP != "TLS" { + req.ClientCert = "" + req.PrivateKey = "" + req.PrivateKeyPasswd = "" + } + + if isEnterpriseWiFiMode(req.Mode) { + if req.Identity == "" { + return fmt.Errorf("identity is required for enterprise wifi") + } + if usesPassword(req.EAP) && req.Password == "" && !fileExists(WiFiPasswd) { + return fmt.Errorf("password is required for %s", req.EAP) + } + if req.EAP == "TLS" && (req.ClientCert == "" || req.PrivateKey == "") { + return fmt.Errorf("client certificate and private key are required for TLS") + } + if req.EAP == "TLS" { + req.Password = "" + } + } + + log.Debugf("wifi connect request: ssid=%q mode=%q identity_set=%t", req.Ssid, req.Mode, req.Identity != "") + return nil +} + +func normalizeWiFiMode(mode string) string { + mode = strings.ToLower(strings.TrimSpace(mode)) + if mode == "" { + return "psk" + } + return mode +} + +func isEnterpriseWiFiMode(mode string) bool { + return mode == "enterprise" || mode == "8021x" || mode == "wpa-eap" +} + +func defaultPhase2(eap string) string { + switch eap { + case "PEAP", "TTLS": + return "auth=MSCHAPV2" + default: + return "" + } +} + +func usesPassword(eap string) bool { + return eap == "PEAP" || eap == "TTLS" || eap == "PWD" || eap == "LEAP" +} + +func isSupportedEAP(eap string) bool { + return eap == "PEAP" || eap == "TTLS" || eap == "TLS" || eap == "PWD" || eap == "LEAP" +} + +func hasLineBreak(values ...string) bool { + for _, value := range values { + if strings.ContainsAny(value, "\r\n") { + return true + } + } + return false +} + +func connect(req proto.ConnectWifiReq) error { + if err := saveWiFiIPConfig(req.IPMode, req.Address, req.SubnetMask, req.Gateway); err != nil { + return err + } + + if err := os.WriteFile(WiFiSSID, []byte(req.Ssid), 0o644); err != nil { log.Errorf("failed to save wifi ssid: %s", err) return err } - if err := os.WriteFile(WiFiPasswd, []byte(password), 0o644); err != nil { - log.Errorf("failed to save wifi password: %s", err) + if strings.TrimSpace(req.Password) != "" { + if err := os.WriteFile(WiFiPasswd, []byte(req.Password), 0o600); err != nil { + log.Errorf("failed to save wifi password: %s", err) + return err + } + } else if !fileExists(WiFiPasswd) { + return fmt.Errorf("missing existing wifi password") + } + + if err := os.WriteFile(WiFiMode, []byte(req.Mode), 0o644); err != nil { + log.Errorf("failed to save wifi mode: %s", err) return err } + if isEnterpriseWiFiMode(req.Mode) { + if err := writeEnterpriseWiFiFiles(req); err != nil { + return err + } + } else { + removeEnterpriseWiFiFiles() + } + if err := os.WriteFile(WiFiConnect, nil, 0o644); err != nil { log.Errorf("failed to connect wifi: %s", err) return err @@ -184,6 +343,185 @@ func connect(ssid string, password string) error { return nil } +func normalizeWiFiIPMode(mode string) string { + mode = strings.ToLower(strings.TrimSpace(mode)) + if mode == WiFiIPModeManual { + return WiFiIPModeManual + } + return WiFiIPModeDHCP +} + +func currentWiFiMode() string { + mode := readTrimmedFile(WiFiMode) + if isEnterpriseWiFiMode(mode) { + return "enterprise" + } + return "psk" +} + +func validateWiFiIPConfig(mode, address, subnetMask, gateway string) error { + if mode != WiFiIPModeManual { + return nil + } + if address == "" { + return fmt.Errorf("static wifi address is required") + } + ip := net.ParseIP(address) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("invalid static wifi address") + } + if subnetMask == "" { + return fmt.Errorf("static wifi subnet mask is required") + } + if _, err := subnetMaskToPrefix(subnetMask); err != nil { + return fmt.Errorf("invalid static wifi subnet") + } + if gateway != "" { + gwIP := net.ParseIP(gateway) + if gwIP == nil || gwIP.To4() == nil { + return fmt.Errorf("invalid static wifi gateway") + } + } + return nil +} + +func currentWiFiIPMode() string { + if fileExists(WiFiStaticConfig) { + return WiFiIPModeManual + } + return WiFiIPModeDHCP +} + +func saveWiFiIPConfig(mode, address, subnetMask, gateway string) error { + if mode != WiFiIPModeManual { + if err := os.Remove(WiFiStaticConfig); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove wifi static config: %w", err) + } + return nil + } + + prefix, err := subnetMaskToPrefix(subnetMask) + if err != nil { + return fmt.Errorf("failed to parse wifi subnet mask: %w", err) + } + + line := fmt.Sprintf("%s/%d", address, prefix) + if gateway != "" { + line += " " + gateway + } + line += "\n" + if err := os.WriteFile(WiFiStaticConfig, []byte(line), 0o644); err != nil { + return fmt.Errorf("failed to write wifi static config: %w", err) + } + return nil +} + +func readWiFiStaticAddress() string { + address, _, _ := parseWiFiStaticConfig() + return address +} + +func readWiFiStaticSubnetMask() string { + _, subnetMask, _ := parseWiFiStaticConfig() + return subnetMask +} + +func readWiFiStaticGateway() string { + _, _, gateway := parseWiFiStaticConfig() + return gateway +} + +func parseWiFiStaticConfig() (string, string, string) { + data, err := os.ReadFile(WiFiStaticConfig) + if err != nil { + return "", "", "" + } + fields := strings.Fields(string(data)) + if len(fields) == 0 { + return "", "", "" + } + address := strings.TrimSpace(fields[0]) + subnetMask := "" + gateway := "" + + if strings.Contains(address, "/") { + if ip, ipNet, err := net.ParseCIDR(address); err == nil && ip != nil && ip.To4() != nil { + address = ip.String() + subnetMask = net.IP(ipNet.Mask).String() + } + if len(fields) > 1 { + gateway = strings.TrimSpace(fields[1]) + } + return address, subnetMask, gateway + } + + if len(fields) > 1 { + second := strings.TrimSpace(fields[1]) + if isValidSubnetMask(second) { + subnetMask = second + if len(fields) > 2 { + gateway = strings.TrimSpace(fields[2]) + } + } else { + gateway = second + } + } + + return address, subnetMask, gateway +} + +func writeEnterpriseWiFiFiles(req proto.ConnectWifiReq) error { + files := map[string]string{ + WiFiIdentity: req.Identity, + WiFiEAP: req.EAP, + WiFiPhase2: req.Phase2, + WiFiAnonymousIdentity: req.AnonymousIdentity, + WiFiCACert: req.CACert, + WiFiClientCert: req.ClientCert, + WiFiPrivateKey: req.PrivateKey, + WiFiDomainSuffixMatch: req.DomainSuffixMatch, + } + + for path, value := range files { + if strings.TrimSpace(value) == "" { + _ = os.Remove(path) + continue + } + if err := os.WriteFile(path, []byte(value), 0o600); err != nil { + log.Errorf("failed to save enterprise wifi file %s: %s", path, err) + return err + } + } + + if strings.TrimSpace(req.PrivateKeyPasswd) != "" { + if err := os.WriteFile(WiFiPrivateKeyPasswd, []byte(req.PrivateKeyPasswd), 0o600); err != nil { + log.Errorf("failed to save enterprise wifi file %s: %s", WiFiPrivateKeyPasswd, err) + return err + } + } else if req.EAP != "TLS" { + _ = os.Remove(WiFiPrivateKeyPasswd) + } + + return nil +} + +func removeEnterpriseWiFiFiles() { + for _, path := range []string{ + WiFiMode, + WiFiIdentity, + WiFiEAP, + WiFiPhase2, + WiFiAnonymousIdentity, + WiFiCACert, + WiFiClientCert, + WiFiPrivateKey, + WiFiPrivateKeyPasswd, + WiFiDomainSuffixMatch, + } { + _ = os.Remove(path) + } +} + func isSupported() bool { _, err := os.Stat(WiFiExistFile) return err == nil diff --git a/web/src/api/network.ts b/web/src/api/network.ts index 00a4c381..b0805eab 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -1,6 +1,45 @@ import { http } from '@/lib/http.ts'; export type DNSMode = 'manual' | 'dhcp'; +export type WiFiSecurityMode = 'psk' | 'enterprise'; +export type EthernetSecurityMode = 'off' | 'enterprise'; +export type EthernetIPMode = 'manual' | 'dhcp'; +export type WiFiIPMode = EthernetIPMode; + +export type ConnectWifiOptions = { + mode?: WiFiSecurityMode; + ipMode?: WiFiIPMode; + address?: string; + subnetMask?: string; + gateway?: string; + identity?: string; + eap?: string; + phase2?: string; + anonymousIdentity?: string; + caCert?: string; + clientCert?: string; + privateKey?: string; + privateKeyPasswd?: string; + domainSuffixMatch?: string; +}; + +export type Ethernet8021XOptions = { + mode?: EthernetSecurityMode; + ipMode?: EthernetIPMode; + address?: string; + subnetMask?: string; + gateway?: string; + password?: string; + identity?: string; + eap?: string; + phase2?: string; + anonymousIdentity?: string; + caCert?: string; + clientCert?: string; + privateKey?: string; + privateKeyPasswd?: string; + domainSuffixMatch?: string; +}; // wake on lan export function wol(mac: string) { @@ -33,10 +72,16 @@ export function getWiFi() { } // connect wifi without auth (only available in wifi configuration mode) -export function connectWifiNoAuth(ssid: string, password: string, apPassword?: string) { +export function connectWifiNoAuth( + ssid: string, + password: string, + apPassword?: string, + options: ConnectWifiOptions = {} +) { const data = { ssid, - password + password, + ...options }; return http.post('/api/network/wifi', data, { headers: { @@ -59,10 +104,11 @@ export function verifyApLogin(apPassword: string) { } // connect wifi -export function connectWifi(ssid: string, password: string) { +export function connectWifi(ssid: string, password: string, options: ConnectWifiOptions = {}) { const data = { ssid, - password + password, + ...options }; return http.post('/api/network/wifi/connect', data); } @@ -72,6 +118,14 @@ export function disconnectWifi() { return http.post('/api/network/wifi/disconnect'); } +export function getEthernet() { + return http.get('/api/network/ethernet'); +} + +export function setEthernet(options: Ethernet8021XOptions = {}) { + return http.post('/api/network/ethernet', options); +} + export function getDNS() { return http.get('/api/network/dns'); } diff --git a/web/src/i18n/locales/ca.ts b/web/src/i18n/locales/ca.ts index 57b1a072..78c236aa 100644 --- a/web/src/i18n/locales/ca.ts +++ b/web/src/i18n/locales/ca.ts @@ -9,21 +9,21 @@ const ca = { }, auth: { login: 'Inici de sessió', - placeholderUsername: "Nom d'usuari", + placeholderUsername: "Nom d\'usuari", placeholderPassword: 'Contrasenya', placeholderPassword2: 'Torna a introduir la contrasenya', - noEmptyUsername: "Cal introduir el nom d'usuari", + noEmptyUsername: "Cal introduir el nom d\'usuari", noEmptyPassword: 'Cal introduir la contrasenya', noAccount: - "No s'ha pogut obtenir la informació de l'usuari, actualitza la pàgina web o restableix la contrasenya", - invalidUser: "Nom d'usuari o contrasenya invàlids", + "No s'ha pogut obtenir la informació de l\'usuari, actualitza la pàgina web o restableix la contrasenya", + invalidUser: "Nom d\'usuari o contrasenya invàlids", locked: 'Massa inicis de sessió, si us plau, torna-ho a provar més tard', globalLocked: 'Sistema sota protecció, torneu-ho a provar més tard', error: 'Error inesperat', changePassword: 'Canviar la contrasenya', changePasswordDesc: 'Per a la seguretat del dispositiu, canvia la contrasenya!', differentPassword: 'Les contrasenyes no coincideixen', - illegalUsername: "El nom d'usuari conté caràcters no permesos", + illegalUsername: "El nom d\'usuari conté caràcters no permesos", illegalPassword: 'La contrasenya conté caràcters no permesos', forgetPassword: 'Has oblidat la contrasenya', ok: "D'acord", @@ -381,8 +381,54 @@ const ca = { password: 'Contrasenya', joinBtn: 'Connecta', confirmBtn: "D'acord", - cancelBtn: 'Cancel·la' + cancelBtn: 'Cancel·la', + ipConfig: 'Configuració IP', + ipConfigDescription: 'Trieu DHCP o establiu una adreça estàtica per a aquesta connexió Wi-Fi', + security: 'Seguretat', + personal: 'Personal / WPA-PSK', + enterprise: 'Empresa / 802.1X', + identity: 'Identitat / Nom d\'usuari', + authentication: 'Autenticació', + innerAuthentication: 'Autenticació interna', + anonymousIdentity: 'Identitat anònima (opcional)', + caCert: 'Ruta del certificat CA (opcional)', + domainSuffixMatch: 'Coincidència de sufix de domini (opcional)', + clientCert: 'Ruta del certificat client', + privateKey: 'Ruta de la clau privada', + privateKeyPasswd: 'Contrasenya de la clau privada (opcional)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certificat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Xarxa amb cable', + description: 'Configurar IP i autenticació 802.1X opcional', + connect: 'Configurar xarxa amb cable', + connectDesc: 'Configurar IP per a {{iface}} i activar 802.1X si cal', + ipConfig: 'Configuració IP', + ipConfigDescription: 'Trieu DHCP o establiu una adreça estàtica per a aquesta interfície amb cable', + authTitle: 'Autenticació 802.1X', + authDescription: 'Activeu només si la vostra xarxa amb cable requereix autenticació per nom d\'usuari, contrasenya o certificat', + driverRequired: '802.1X amb cable necessita un wpa_supplicant del sistema amb suport de controlador wired', + addressPlaceholder: 'Adreça IP estàtica (ex: 192.168.1.10)', + gatewayPlaceholder: 'Passarel·la (opcional)', + passwordUnchanged: 'Sense canvis', + privateKeyPasswdUnchanged: 'Sense canvis', + off: 'Desactivat', + enterprise: 'Empresa / 802.1X', + active: '802.1X activat', + inactive: '802.1X configurat, esperant autenticació', + failed: 'Error en configurar la xarxa amb cable 802.1X. Torneu-ho a provar.' + }, + tls: { description: 'Activa el protocol HTTPS', tip: 'Atenció: Usar HTTPS pot augmentar la latència, sobretot amb vídeo MJPEG.' @@ -409,6 +455,11 @@ const ca = { ipAddress: 'Adreça IP', subnetMask: 'Màscara de subxarxa', router: 'Encaminador', + wired: 'Amb cable', + wireless: 'Sense fil', + signalStrength: 'Intensitat del senyal', + rxRate: 'Velocitat de recepció', + txRate: 'Velocitat de transmissió', none: 'Cap' } }, diff --git a/web/src/i18n/locales/cz.ts b/web/src/i18n/locales/cz.ts index 269ffcaa..f71fab9b 100644 --- a/web/src/i18n/locales/cz.ts +++ b/web/src/i18n/locales/cz.ts @@ -384,8 +384,54 @@ const cz = { password: 'Heslo', joinBtn: 'Připojit', confirmBtn: 'OK', - cancelBtn: 'Zrušit' + cancelBtn: 'Zrušit', + ipConfig: 'Konfigurace IP', + ipConfigDescription: 'Vyberte DHCP nebo nastavte statickou adresu pro toto připojení Wi-Fi', + security: 'Zabezpečení', + personal: 'Osobní / WPA-PSK', + enterprise: 'Firemní / 802.1X', + identity: 'Identita / Uživatelské jméno', + authentication: 'Ověření', + innerAuthentication: 'Vnitřní ověření', + anonymousIdentity: 'Anonymní identita (volitelné)', + caCert: 'Cesta k certifikátu CA (volitelné)', + domainSuffixMatch: 'Shoda přípony domény (volitelné)', + clientCert: 'Cesta klientského certifikátu', + privateKey: 'Cesta soukromého klíče', + privateKeyPasswd: 'Heslo soukromého klíče (volitelné)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certifikát)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Drátová síť', + description: 'Konfigurace IP a volitelného ověření 802.1X', + connect: 'Nakonfigurovat drátovou síť', + connectDesc: 'Nakonfigurujte IP pro {{iface}} a povolte 802.1X v případě potřeby', + ipConfig: 'Konfigurace IP', + ipConfigDescription: 'Vyberte DHCP nebo nastavte statickou adresu pro tento drátový interface', + authTitle: 'Ověření 802.1X', + authDescription: 'Povolte pouze pokud vaše drátová síť vyžaduje ověření pomocí uživatelského jména, hesla nebo certifikátu', + driverRequired: 'Drátové 802.1X vyžaduje systémový wpa_supplicant s podporou ovladače wired', + addressPlaceholder: 'Statická IP adresa (např. 192.168.1.10)', + gatewayPlaceholder: 'Brána (volitelné)', + passwordUnchanged: 'Beze změny', + privateKeyPasswdUnchanged: 'Beze změny', + off: 'Zakázáno', + enterprise: 'Firemní / 802.1X', + active: '802.1X povoleno', + inactive: '802.1X nakonfigurováno, čeká na ověření', + failed: 'Nepodařilo se nakonfigurovat drátovou síť 802.1X. Zkuste to prosím znovu.' + }, + tls: { description: 'Povolit protokol HTTPS', tip: 'Upozornění: Použití HTTPS může zvýšit latenci, zejména v režimu videa MJPEG.' @@ -412,6 +458,11 @@ const cz = { ipAddress: 'IP adresa', subnetMask: 'Maska podsítě', router: 'Router', + wired: 'Drátová', + wireless: 'Bezdrátová', + signalStrength: 'Síla signálu', + rxRate: 'Rychlost příjmu', + txRate: 'Rychlost odesílání', none: 'Žádné' } }, diff --git a/web/src/i18n/locales/da.ts b/web/src/i18n/locales/da.ts index fb2a52b5..71489962 100644 --- a/web/src/i18n/locales/da.ts +++ b/web/src/i18n/locales/da.ts @@ -383,8 +383,54 @@ const da = { password: 'Adgangskode', joinBtn: 'Tilslut', confirmBtn: 'OK', - cancelBtn: 'Annuller' + cancelBtn: 'Annuller', + ipConfig: 'IP-konfiguration', + ipConfigDescription: 'Vælg DHCP eller angiv en statisk adresse for denne Wi-Fi-forbindelse', + security: 'Sikkerhed', + personal: 'Personlig / WPA-PSK', + enterprise: 'Erhverv / 802.1X', + identity: 'Identitet / Brugernavn', + authentication: 'Godkendelse', + innerAuthentication: 'Intern godkendelse', + anonymousIdentity: 'Anonym identitet (valgfrit)', + caCert: 'CA-certifikatsti (valgfrit)', + domainSuffixMatch: 'Domænesuffiks-matchning (valgfrit)', + clientCert: 'Klientcertifikatsti', + privateKey: 'Privat nøglesti', + privateKeyPasswd: 'Adgangskode for privat nøgle (valgfrit)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certifikat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Kablet netværk', + description: 'Konfigurer IP-indstillinger og valgfri 802.1X-godkendelse', + connect: 'Konfigurer kablet netværk', + connectDesc: 'Konfigurer IP for {{iface}} og aktiver 802.1X ved behov', + ipConfig: 'IP-konfiguration', + ipConfigDescription: 'Vælg DHCP eller angiv en statisk adresse for dette kablede interface', + authTitle: '802.1X-godkendelse', + authDescription: 'Aktiver kun hvis dit kablede netværk kræver brugernavn, adgangskode eller certifikatgodkendelse', + driverRequired: 'Kablet 802.1X kræver en system-wpa_supplicant med understøttelse af wired-driver', + addressPlaceholder: 'Statisk IP-adresse (f.eks. 192.168.1.10)', + gatewayPlaceholder: 'Gateway (valgfrit)', + passwordUnchanged: 'Ingen ændring', + privateKeyPasswdUnchanged: 'Ingen ændring', + off: 'Deaktiveret', + enterprise: 'Erhverv / 802.1X', + active: '802.1X aktiveret', + inactive: '802.1X konfigureret, venter på godkendelse', + failed: 'Konfiguration af kablet netværk 802.1X mislykkedes. Prøv venligst igen.' + }, + tls: { description: 'Aktiver HTTPS-protokol', tip: 'Bemærk: Brug af HTTPS kan øge forsinkelsen, især med MJPEG-videotilstand.' @@ -411,6 +457,11 @@ const da = { ipAddress: 'IP-adresse', subnetMask: 'Undernetmaske', router: 'Router', + wired: 'Kablet', + wireless: 'Trådløst', + signalStrength: 'Signalstyrke', + rxRate: 'Modtagelseshastighed', + txRate: 'Sendehastighed', none: 'Ingen' } }, diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index 8cbaf2b2..135ec7e8 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -388,8 +388,54 @@ const de = { password: 'Passwort', joinBtn: 'Verbinden', confirmBtn: 'OK', - cancelBtn: 'Abbrechen' + cancelBtn: 'Abbrechen', + ipConfig: 'IP-Konfiguration', + ipConfigDescription: 'DHCP oder statische Adresse für diese Wi-Fi-Verbindung wählen', + security: 'Sicherheit', + personal: 'Persönlich / WPA-PSK', + enterprise: 'Unternehmen / 802.1X', + identity: 'Identität / Benutzername', + authentication: 'Authentifizierung', + innerAuthentication: 'Innere Authentifizierung', + anonymousIdentity: 'Anonyme Identität (optional)', + caCert: 'CA-Zertifikatpfad (optional)', + domainSuffixMatch: 'Domänensuffix-Abgleich (optional)', + clientCert: 'Client-Zertifikatpfad', + privateKey: 'Privater Schlüsselpfad', + privateKeyPasswd: 'Passwort für privaten Schlüssel (optional)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Zertifikat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Kabelgebundenes Netzwerk', + description: 'IP-Einstellungen und optionale 802.1X-Authentifizierung konfigurieren', + connect: 'Kabelgebundenes Netzwerk konfigurieren', + connectDesc: 'IP-Konfiguration für {{iface}} festlegen und bei Bedarf 802.1X aktivieren', + ipConfig: 'IP-Konfiguration', + ipConfigDescription: 'DHCP oder statische Adresse für diese kabelgebundene Schnittstelle wählen', + authTitle: '802.1X-Authentifizierung', + authDescription: 'Nur aktivieren, wenn Ihr kabelgebundenes Netzwerk Benutzername, Passwort oder Zertifikat-Authentifizierung erfordert', + driverRequired: 'Kabelgebundenes 802.1X erfordert ein wpa_supplicant-System mit Wired-Treiber-Unterstützung', + addressPlaceholder: 'Statische IP-Adresse (z.B. 192.168.1.10)', + gatewayPlaceholder: 'Gateway (optional)', + passwordUnchanged: 'Keine Änderung', + privateKeyPasswdUnchanged: 'Keine Änderung', + off: 'Deaktiviert', + enterprise: 'Unternehmen / 802.1X', + active: '802.1X aktiviert', + inactive: '802.1X konfiguriert, warte auf Authentifizierung', + failed: 'Konfiguration von Kabelgebundenes Netzwerk 802.1X fehlgeschlagen. Bitte erneut versuchen.' + }, + tls: { description: 'HTTPS-Protokoll aktivieren', tip: 'Hinweis: Die Verwendung von HTTPS kann die Latenz erhöhen, besonders im MJPEG-Videomodus.' @@ -416,6 +462,11 @@ const de = { ipAddress: 'IP-Adresse', subnetMask: 'Subnetzmaske', router: 'Router', + wired: 'Kabelgebunden', + wireless: 'Drahtlos', + signalStrength: 'Signalstärke', + rxRate: 'Empfangsrate', + txRate: 'Sendrate', none: 'Keine' } }, diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 7148c5a9..64f0efd3 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -375,14 +375,58 @@ const en = { connect: 'Join Wi-Fi', connectDesc1: 'Please enter the network ssid and password', connectDesc2: 'Please enter the password to join this network', + ipConfig: 'IP configuration', + ipConfigDescription: 'Choose DHCP or set a static address for this Wi-Fi connection', disconnect: 'Are you sure to disconnect the network?', failed: 'Connection failed, please try again.', ssid: 'Name', password: 'Password', + security: 'Security', + personal: 'Personal / WPA-PSK', + enterprise: 'Enterprise / 802.1X', + identity: 'Identity / Username', + authentication: 'Authentication', + innerAuthentication: 'Inner authentication', + anonymousIdentity: 'Anonymous identity (optional)', + caCert: 'CA certificate path (optional)', + domainSuffixMatch: 'Domain suffix match (optional)', + clientCert: 'Client certificate path', + privateKey: 'Private key path', + privateKeyPasswd: 'Private key password (optional)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certificate)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', joinBtn: 'Join', confirmBtn: 'Ok', cancelBtn: 'Cancel' }, + ethernet: { + title: 'Ethernet', + description: 'Configure IP settings and optional 802.1X authentication', + connect: 'Configure Ethernet', + connectDesc: 'Set IP configuration for {{iface}} and enable 802.1X if your network requires it', + ipConfig: 'IP configuration', + ipConfigDescription: 'Choose DHCP or set a static address for this wired interface', + authTitle: '802.1X authentication', + authDescription: 'Enable this only if your wired network requires username, password, or certificate authentication', + addressPlaceholder: 'Static IP address (e.g. 192.168.1.10)', + gatewayPlaceholder: 'Gateway (optional)', + passwordUnchanged: 'No change', + privateKeyPasswdUnchanged: 'No change', + off: 'Disabled', + enterprise: 'Enterprise / 802.1X', + active: '802.1X enabled', + inactive: '802.1X configured, waiting for authentication', + failed: 'Failed to configure Ethernet 802.1X, please try again.' + }, tls: { description: 'Enable HTTPS protocol', tip: 'Be aware: Using HTTPS can increase latency, especially with MJPEG video mode.' @@ -406,9 +450,14 @@ const en = { manualServersDescription: 'DNS servers can be edited manually', networkDetails: 'Network Details', interface: 'Interface', + wired: 'Wired', + wireless: 'Wireless', ipAddress: 'IP Address', subnetMask: 'Subnet Mask', router: 'Router', + signalStrength: 'Signal Strength', + rxRate: 'Receive Rate', + txRate: 'Transmit Rate', none: 'None' } }, diff --git a/web/src/i18n/locales/es.ts b/web/src/i18n/locales/es.ts index 8f789dd6..a71cc108 100644 --- a/web/src/i18n/locales/es.ts +++ b/web/src/i18n/locales/es.ts @@ -385,8 +385,54 @@ const es = { password: 'Contraseña', joinBtn: 'Unirse', confirmBtn: 'Aceptar', - cancelBtn: 'Cancelar' + cancelBtn: 'Cancelar', + ipConfig: 'Configuración IP', + ipConfigDescription: 'Elegir DHCP o establecer una dirección estática para esta conexión Wi-Fi', + security: 'Seguridad', + personal: 'Personal / WPA-PSK', + enterprise: 'Empresa / 802.1X', + identity: 'Identidad / Nombre de usuario', + authentication: 'Autenticación', + innerAuthentication: 'Autenticación interna', + anonymousIdentity: 'Identidad anónima (opcional)', + caCert: 'Ruta del certificado CA (opcional)', + domainSuffixMatch: 'Coincidencia de sufijo de dominio (opcional)', + clientCert: 'Ruta del certificado cliente', + privateKey: 'Ruta de la clave privada', + privateKeyPasswd: 'Contraseña de la clave privada (opcional)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certificado)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Red cableada', + description: 'Configurar IP y autenticación 802.1X opcional', + connect: 'Configurar red cableada', + connectDesc: 'Configurar IP para {{iface}} y habilitar 802.1X si es necesario', + ipConfig: 'Configuración IP', + ipConfigDescription: 'Elegir DHCP o establecer una dirección estática para esta interfaz cableada', + authTitle: 'Autenticación 802.1X', + authDescription: 'Active solo si su red cableada requiere autenticación por usuario, contraseña o certificado', + driverRequired: '802.1X cableado necesita un wpa_supplicant del sistema con soporte de controlador wired', + addressPlaceholder: 'Dirección IP estática (ej: 192.168.1.10)', + gatewayPlaceholder: 'Puerta de enlace (opcional)', + passwordUnchanged: 'Sin cambios', + privateKeyPasswdUnchanged: 'Sin cambios', + off: 'Deshabilitado', + enterprise: 'Empresa / 802.1X', + active: '802.1X habilitado', + inactive: '802.1X configurado, esperando autenticación', + failed: 'Error al configurar la red cableada 802.1X. Por favor, inténtelo de nuevo.' + }, + tls: { description: 'Habilitar protocolo HTTPS', tip: 'Aviso: Usar HTTPS puede aumentar la latencia, especialmente en modo de vídeo MJPEG.' @@ -413,6 +459,11 @@ const es = { ipAddress: 'Dirección IP', subnetMask: 'Máscara de subred', router: 'Router', + wired: 'Cableada', + wireless: 'Inalámbrica', + signalStrength: 'Intensidad de señal', + rxRate: 'Velocidad de recepción', + txRate: 'Velocidad de transmisión', none: 'Ninguno' } }, diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index d46989ea..72615dbb 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -15,7 +15,7 @@ const fr = { noEmptyUsername: "Le nom d'utilisateur ne peut pas être vide", noEmptyPassword: 'Le mot de passe ne peut pas être vide', noAccount: - "Impossible de récupérer les informations de l'utilisateur, veuillez rafraîchir la page ou réinitialiser le mot de passe", + "Impossible de récupérer les informations de l\'utilisateur, veuillez rafraîchir la page ou réinitialiser le mot de passe", invalidUser: "Nom d'utilisateur ou mot de passe invalide", locked: 'Trop de connexions, veuillez réessayer plus tard', globalLocked: 'Système sous protection, veuillez réessayer plus tard', @@ -387,8 +387,54 @@ const fr = { password: 'Mot de passe', joinBtn: 'Rejoindre', confirmBtn: 'OK', - cancelBtn: 'Annuler' + cancelBtn: 'Annuler', + ipConfig: 'Configuration IP', + ipConfigDescription: 'Choisir DHCP ou définir une adresse statique pour cette connexion Wi-Fi', + security: 'Sécurité', + personal: 'Personnel / WPA-PSK', + enterprise: 'Entreprise / 802.1X', + identity: 'Identifiant / Nom d\'utilisateur', + authentication: 'Authentification', + innerAuthentication: 'Authentification interne', + anonymousIdentity: 'Identité anonyme (optionnel)', + caCert: 'Chemin du certificat CA (optionnel)', + domainSuffixMatch: 'Correspondance du suffixe de domaine (optionnel)', + clientCert: 'Chemin du certificat client', + privateKey: 'Chemin de la clé privée', + privateKeyPasswd: 'Mot de passe de la clé privée (optionnel)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certificat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Réseau filaire', + description: 'Configurer les paramètres IP et l\'authentification 802.1X optionnelle', + connect: 'Configurer le réseau filaire', + connectDesc: 'Configurer les paramètres IP pour {{iface}} et activer 802.1X si nécessaire', + ipConfig: 'Configuration IP', + ipConfigDescription: 'Choisir DHCP ou définir une adresse statique pour cette interface filaire', + authTitle: 'Authentification 802.1X', + authDescription: 'Activez uniquement si votre réseau filaire nécessite une authentification par nom d\'utilisateur, mot de passe ou certificat', + driverRequired: 'Le réseau filaire 802.1X nécessite un système wpa_supplicant avec prise en charge du pilote wired', + addressPlaceholder: 'Adresse IP statique (ex: 192.168.1.10)', + gatewayPlaceholder: 'Passerelle (optionnel)', + passwordUnchanged: 'Aucun changement', + privateKeyPasswdUnchanged: 'Aucun changement', + off: 'Désactivé', + enterprise: 'Entreprise / 802.1X', + active: '802.1X activé', + inactive: '802.1X configuré, en attente d\'authentification', + failed: 'Échec de la configuration du réseau filaire 802.1X. Veuillez réessayer.' + }, + tls: { description: 'Activer le protocole HTTPS', tip: "Attention : l'utilisation de HTTPS peut augmenter la latence, surtout en mode vidéo MJPEG." @@ -415,6 +461,11 @@ const fr = { ipAddress: 'Adresse IP', subnetMask: 'Masque de sous-réseau', router: 'Routeur', + wired: 'Filaire', + wireless: 'Sans fil', + signalStrength: 'Force du signal', + rxRate: 'Débit réception', + txRate: 'Débit émission', none: 'Aucun' } }, diff --git a/web/src/i18n/locales/hu.ts b/web/src/i18n/locales/hu.ts index 2d20380a..85d1e9bb 100644 --- a/web/src/i18n/locales/hu.ts +++ b/web/src/i18n/locales/hu.ts @@ -386,8 +386,54 @@ const hu = { password: 'Jelszó', joinBtn: 'Csatlakozás', confirmBtn: 'OK', - cancelBtn: 'Mégse' + cancelBtn: 'Mégse', + ipConfig: 'IP konfiguráció', + ipConfigDescription: 'Válassza a DHCP-t vagy állítson be statikus címet ehhez a Wi-Fi kapcsolathoz', + security: 'Biztonság', + personal: 'Személyes / WPA-PSK', + enterprise: 'Vállalati / 802.1X', + identity: 'Azonosító / Felhasználónév', + authentication: 'Hitelesítés', + innerAuthentication: 'Belső hitelesítés', + anonymousIdentity: 'Névtelen azonosító (opcionális)', + caCert: 'CA tanúsítvány elérési útja (opcionális)', + domainSuffixMatch: 'Tartomány utótag egyezés (opcionális)', + clientCert: 'Kliens tanúsítvány elérési útja', + privateKey: 'Privát kulcs elérési útja', + privateKeyPasswd: 'Privát kulcs jelszava (opcionális)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Tanúsítvány)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Vezetékes hálózat', + description: 'IP beállítások és opcionális 802.1X hitelesítés konfigurálása', + connect: 'Vezetékes hálózat konfigurálása', + connectDesc: 'IP konfiguráció beállítása a(z) {{iface}} számára és 802.1X engedélyezése szükség esetén', + ipConfig: 'IP konfiguráció', + ipConfigDescription: 'Válassza a DHCP-t vagy állítson be statikus címet ehhez a vezetékes interfészhez', + authTitle: '802.1X hitelesítés', + authDescription: 'Csak akkor engedélyezze, ha a vezetékes hálózat felhasználónévvel, jelszóval vagy tanúsítvánnyal történő hitelesítést igényel', + driverRequired: 'A vezetékes 802.1X egy wired meghajtó támogatással rendelkező rendszer wpa_supplicant-t igényel', + addressPlaceholder: 'Statikus IP-cím (pl. 192.168.1.10)', + gatewayPlaceholder: 'Átjáró (opcionális)', + passwordUnchanged: 'Nincs változás', + privateKeyPasswdUnchanged: 'Nincs változás', + off: 'Letiltva', + enterprise: 'Vállalati / 802.1X', + active: '802.1X engedélyezve', + inactive: '802.1X konfigurálva, hitelesítésre vár', + failed: 'A vezetékes hálózat 802.1X konfigurálása sikertelen. Kérjük, próbálja újra.' + }, + tls: { description: 'HTTPS protokoll engedélyezése', tip: 'Figyelem: A HTTPS használata növelheti a késleltetést, különösen MJPEG videó módban.' @@ -414,6 +460,11 @@ const hu = { ipAddress: 'IP-cím', subnetMask: 'Alhálózati maszk', router: 'Router', + wired: 'Vezetékes', + wireless: 'Vezeték nélküli', + signalStrength: 'Jelerősség', + rxRate: 'Fogadási sebesség', + txRate: 'Küldési sebesség', none: 'Nincs' } }, diff --git a/web/src/i18n/locales/id.ts b/web/src/i18n/locales/id.ts index 7c37c4a1..105f0d3a 100644 --- a/web/src/i18n/locales/id.ts +++ b/web/src/i18n/locales/id.ts @@ -383,8 +383,54 @@ const id = { password: 'Kata sandi', joinBtn: 'Gabung', confirmBtn: 'OK', - cancelBtn: 'Batal' + cancelBtn: 'Batal', + ipConfig: 'Konfigurasi IP', + ipConfigDescription: 'Pilih DHCP atau atur alamat statis untuk koneksi Wi-Fi ini', + security: 'Keamanan', + personal: 'Personal / WPA-PSK', + enterprise: 'Enterprise / 802.1X', + identity: 'Identitas / Nama pengguna', + authentication: 'Autentikasi', + innerAuthentication: 'Autentikasi internal', + anonymousIdentity: 'Identitas anonim (opsional)', + caCert: 'Jalur sertifikat CA (opsional)', + domainSuffixMatch: 'Pencocokan akhiran domain (opsional)', + clientCert: 'Jalur sertifikat klien', + privateKey: 'Jalur kunci privat', + privateKeyPasswd: 'Kata sandi kunci privat (opsional)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Sertifikat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Jaringan berkabel', + description: 'Konfigurasi IP dan autentikasi 802.1X opsional', + connect: 'Konfigurasi jaringan berkabel', + connectDesc: 'Atur konfigurasi IP untuk {{iface}} dan aktifkan 802.1X jika diperlukan', + ipConfig: 'Konfigurasi IP', + ipConfigDescription: 'Pilih DHCP atau atur alamat statis untuk antarmuka berkabel ini', + authTitle: 'Autentikasi 802.1X', + authDescription: 'Aktifkan hanya jika jaringan berkabel Anda memerlukan autentikasi nama pengguna, kata sandi, atau sertifikat', + driverRequired: '802.1X berkabel memerlukan build wpa_supplicant sistem dengan dukungan driver wired', + addressPlaceholder: 'Alamat IP statis (mis: 192.168.1.10)', + gatewayPlaceholder: 'Gateway (opsional)', + passwordUnchanged: 'Tidak ada perubahan', + privateKeyPasswdUnchanged: 'Tidak ada perubahan', + off: 'Nonaktif', + enterprise: 'Enterprise / 802.1X', + active: '802.1X aktif', + inactive: '802.1X dikonfigurasi, menunggu autentikasi', + failed: 'Gagal mengkonfigurasi jaringan berkabel 802.1X. Silakan coba lagi.' + }, + tls: { description: 'Aktifkan protokol HTTPS', tip: 'Perhatian: Menggunakan HTTPS dapat meningkatkan latensi, terutama pada mode video MJPEG.' @@ -411,6 +457,11 @@ const id = { ipAddress: 'Alamat IP', subnetMask: 'Subnet mask', router: 'Router', + wired: 'Berkabel', + wireless: 'Nirkabel', + signalStrength: 'Kekuatan sinyal', + rxRate: 'Kecepatan terima', + txRate: 'Kecepatan kirim', none: 'Tidak ada' } }, diff --git a/web/src/i18n/locales/it.ts b/web/src/i18n/locales/it.ts index 8df8546f..8c066c0c 100644 --- a/web/src/i18n/locales/it.ts +++ b/web/src/i18n/locales/it.ts @@ -387,8 +387,54 @@ const it = { password: 'Password', joinBtn: 'Connetti', confirmBtn: 'OK', - cancelBtn: 'Annulla' + cancelBtn: 'Annulla', + ipConfig: 'Configurazione IP', + ipConfigDescription: 'Scegli DHCP o imposta un indirizzo statico per questa connessione Wi-Fi', + security: 'Sicurezza', + personal: 'Personale / WPA-PSK', + enterprise: 'Aziendale / 802.1X', + identity: 'Identità / Nome utente', + authentication: 'Autenticazione', + innerAuthentication: 'Autenticazione interna', + anonymousIdentity: 'Identità anonima (opzionale)', + caCert: 'Percorso certificato CA (opzionale)', + domainSuffixMatch: 'Corrispondenza suffisso dominio (opzionale)', + clientCert: 'Percorso certificato client', + privateKey: 'Percorso chiave privata', + privateKeyPasswd: 'Password chiave privata (opzionale)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certificato)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Rete cablata', + description: 'Configura IP e autenticazione 802.1X opzionale', + connect: 'Configura rete cablata', + connectDesc: 'Configura IP per {{iface}} e abilita 802.1X se necessario', + ipConfig: 'Configurazione IP', + ipConfigDescription: 'Scegli DHCP o imposta un indirizzo statico per questa interfaccia cablata', + authTitle: 'Autenticazione 802.1X', + authDescription: 'Abilita solo se la rete cablata richiede autenticazione tramite nome utente, password o certificato', + driverRequired: '802.1X cablato richiede un wpa_supplicant di sistema con supporto driver wired', + addressPlaceholder: 'Indirizzo IP statico (es: 192.168.1.10)', + gatewayPlaceholder: 'Gateway (opzionale)', + passwordUnchanged: 'Nessuna modifica', + privateKeyPasswdUnchanged: 'Nessuna modifica', + off: 'Disabilitato', + enterprise: 'Aziendale / 802.1X', + active: '802.1X abilitato', + inactive: '802.1X configurato, in attesa di autenticazione', + failed: 'Configurazione della rete cablata 802.1X non riuscita. Riprova.' + }, + tls: { description: 'Abilita protocollo HTTPS', tip: "Attenzione: l'uso di HTTPS può aumentare la latenza, soprattutto in modalità video MJPEG." @@ -415,6 +461,11 @@ const it = { ipAddress: 'Indirizzo IP', subnetMask: 'Subnet mask', router: 'Router', + wired: 'Cablata', + wireless: 'Wireless', + signalStrength: 'Intensità segnale', + rxRate: 'Velocità ricezione', + txRate: 'Velocità trasmissione', none: 'Nessuno' } }, diff --git a/web/src/i18n/locales/ja.ts b/web/src/i18n/locales/ja.ts index 91b642aa..79354048 100644 --- a/web/src/i18n/locales/ja.ts +++ b/web/src/i18n/locales/ja.ts @@ -385,8 +385,54 @@ const ja = { password: 'パスワード', joinBtn: '接続', confirmBtn: 'OK', - cancelBtn: 'キャンセル' + cancelBtn: 'キャンセル', + ipConfig: 'IP 設定', + ipConfigDescription: 'この Wi-Fi 接続に DHCP または静的アドレスを設定', + security: 'セキュリティ', + personal: 'パーソナル / WPA-PSK', + enterprise: 'エンタープライズ / 802.1X', + identity: 'ID / ユーザー名', + authentication: '認証方式', + innerAuthentication: '内部認証', + anonymousIdentity: '匿名 ID(任意)', + caCert: 'CA 証明書パス(任意)', + domainSuffixMatch: 'ドメインサフィックスマッチ(任意)', + clientCert: 'クライアント証明書パス', + privateKey: '秘密鍵パス', + privateKeyPasswd: '秘密鍵パスワード(任意)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS(証明書)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: '有線ネットワーク', + description: 'IP 設定とオプションの 802.1X 認証を設定', + connect: '有線ネットワークを設定', + connectDesc: '{{iface}} の IP 設定を行い、必要に応じて 802.1X 認証を有効にする', + ipConfig: 'IP 設定', + ipConfigDescription: 'この有線インターフェースに DHCP または静的アドレスを設定', + authTitle: '802.1X 認証', + authDescription: '有線ネットワークでユーザー名、パスワード、または証明書認証が必要な場合のみ有効にしてください', + driverRequired: '有線 802.1X には、wired ドライバーをサポートする wpa_supplicant のシステムビルドが必要です', + addressPlaceholder: '静的 IP アドレス(例: 192.168.1.10)', + gatewayPlaceholder: 'ゲートウェイ(任意)', + passwordUnchanged: '変更なし', + privateKeyPasswdUnchanged: '変更なし', + off: '無効', + enterprise: 'エンタープライズ / 802.1X', + active: '802.1X 有効', + inactive: '802.1X 設定済み、認証待ち', + failed: '有線ネットワーク 802.1X の設定に失敗しました。もう一度お試しください。' + }, + tls: { description: 'HTTPS プロトコルを有効にする', tip: '注意:HTTPS を使用すると、特に MJPEG ビデオモードで遅延が増加する可能性があります。' @@ -413,6 +459,11 @@ const ja = { ipAddress: 'IP アドレス', subnetMask: 'サブネットマスク', router: 'ルーター', + wired: '有線', + wireless: '無線', + signalStrength: '信号強度', + rxRate: '受信速度', + txRate: '送信速度', none: 'なし' } }, diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index 035272c1..c21dd8b7 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -379,8 +379,54 @@ const ko = { password: '비밀번호', joinBtn: '연결', confirmBtn: '확인', - cancelBtn: '취소' + cancelBtn: '취소', + ipConfig: 'IP 설정', + ipConfigDescription: '이 Wi-Fi 연결에 DHCP 또는 고정 주소 설정', + security: '보안', + personal: '개인 / WPA-PSK', + enterprise: '기업 / 802.1X', + identity: 'ID / 사용자 이름', + authentication: '인증 방식', + innerAuthentication: '내부 인증', + anonymousIdentity: '익명 ID (선택)', + caCert: 'CA 인증서 경로 (선택)', + domainSuffixMatch: '도메인 접미사 일치 (선택)', + clientCert: '클라이언트 인증서 경로', + privateKey: '개인키 경로', + privateKeyPasswd: '개인키 비밀번호 (선택)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (인증서)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: '유선 네트워크', + description: 'IP 설정 및 선택적 802.1X 인증 구성', + connect: '유선 네트워크 설정', + connectDesc: '{{iface}}의 IP 설정을 구성하고 필요시 802.1X 인증 활성화', + ipConfig: 'IP 설정', + ipConfigDescription: '이 유선 인터페이스에 DHCP 또는 고정 주소 설정', + authTitle: '802.1X 인증', + authDescription: '유선 네트워크에서 사용자 이름, 비밀번호 또는 인증서 인증이 필요한 경우에만 활성화하세요', + driverRequired: '유선 802.1X에는 wired 드라이버를 지원하는 wpa_supplicant 시스템 빌드가 필요합니다', + addressPlaceholder: '고정 IP 주소 (예: 192.168.1.10)', + gatewayPlaceholder: '게이트웨이 (선택)', + passwordUnchanged: '변경 없음', + privateKeyPasswdUnchanged: '변경 없음', + off: '비활성화', + enterprise: '기업 / 802.1X', + active: '802.1X 활성화됨', + inactive: '802.1X 설정됨, 인증 대기 중', + failed: '유선 네트워크 802.1X 설정에 실패했습니다. 다시 시도해 주세요.' + }, + tls: { description: 'HTTPS 프로토콜 활성화', tip: '주의: HTTPS 사용 시 특히 MJPEG 비디오 모드에서 지연 시간이 증가할 수 있습니다.' @@ -407,6 +453,11 @@ const ko = { ipAddress: 'IP 주소', subnetMask: '서브넷 마스크', router: '라우터', + wired: '유선', + wireless: '무선', + signalStrength: '신호 강도', + rxRate: '수신 속도', + txRate: '송신 속도', none: '없음' } }, diff --git a/web/src/i18n/locales/nb.ts b/web/src/i18n/locales/nb.ts index 5178a7b7..8f6c88b4 100644 --- a/web/src/i18n/locales/nb.ts +++ b/web/src/i18n/locales/nb.ts @@ -383,8 +383,54 @@ const nb = { password: 'Passord', joinBtn: 'Koble til', confirmBtn: 'OK', - cancelBtn: 'Avbryt' + cancelBtn: 'Avbryt', + ipConfig: 'IP-konfigurasjon', + ipConfigDescription: 'Velg DHCP eller angi en statisk adresse for denne Wi-Fi-tilkoblingen', + security: 'Sikkerhet', + personal: 'Personlig / WPA-PSK', + enterprise: 'Bedrift / 802.1X', + identity: 'Identitet / Brukernavn', + authentication: 'Autentisering', + innerAuthentication: 'Indre autentisering', + anonymousIdentity: 'Anonym identitet (valgfritt)', + caCert: 'CA-sertifikatsti (valgfritt)', + domainSuffixMatch: 'Domene suffiks-samsvar (valgfritt)', + clientCert: 'Klientsertifikatsti', + privateKey: 'Privat nøkkelsti', + privateKeyPasswd: 'Passord for privat nøkkel (valgfritt)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Sertifikat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Kablet nettverk', + description: 'Konfigurer IP-innstillinger og valgfri 802.1X-autentisering', + connect: 'Konfigurer kablet nettverk', + connectDesc: 'Konfigurer IP for {{iface}} og aktiver 802.1X ved behov', + ipConfig: 'IP-konfigurasjon', + ipConfigDescription: 'Velg DHCP eller angi en statisk adresse for dette kablede grensesnittet', + authTitle: '802.1X-autentisering', + authDescription: 'Aktiver bare hvis ditt kablede nettverk krever brukernavn, passord eller sertifikat-autentisering', + driverRequired: 'Kablet 802.1X krever en system-wpa_supplicant med støtte for wired-driver', + addressPlaceholder: 'Statisk IP-adresse (f.eks. 192.168.1.10)', + gatewayPlaceholder: 'Gateway (valgfritt)', + passwordUnchanged: 'Ingen endring', + privateKeyPasswdUnchanged: 'Ingen endring', + off: 'Deaktivert', + enterprise: 'Bedrift / 802.1X', + active: '802.1X aktivert', + inactive: '802.1X konfigurert, venter på autentisering', + failed: 'Konfigurasjon av kablet nettverk 802.1X mislyktes. Vennligst prøv igjen.' + }, + tls: { description: 'Aktiver HTTPS-protokoll', tip: 'Merk: Bruk av HTTPS kan øke forsinkelsen, spesielt i MJPEG-videomodus.' @@ -411,6 +457,11 @@ const nb = { ipAddress: 'IP-adresse', subnetMask: 'Subnettmaske', router: 'Ruter', + wired: 'Kablet', + wireless: 'Trådløst', + signalStrength: 'Signalstyrke', + rxRate: 'Mottakshastighet', + txRate: 'Sendingshastighet', none: 'Ingen' } }, diff --git a/web/src/i18n/locales/nl.ts b/web/src/i18n/locales/nl.ts index b7d0e1f9..bba48c6f 100644 --- a/web/src/i18n/locales/nl.ts +++ b/web/src/i18n/locales/nl.ts @@ -387,8 +387,54 @@ const nl = { password: 'Wachtwoord', joinBtn: 'Verbinden', confirmBtn: 'OK', - cancelBtn: 'Annuleren' + cancelBtn: 'Annuleren', + ipConfig: 'IP-configuratie', + ipConfigDescription: 'Kies DHCP of stel een statisch adres in voor deze Wi-Fi verbinding', + security: 'Beveiliging', + personal: 'Persoonlijk / WPA-PSK', + enterprise: 'Zakelijk / 802.1X', + identity: 'Identiteit / Gebruikersnaam', + authentication: 'Authenticatie', + innerAuthentication: 'Interne authenticatie', + anonymousIdentity: 'Anonieme identiteit (optioneel)', + caCert: 'CA-certificaatpad (optioneel)', + domainSuffixMatch: 'Domeinsuffix-overeenkomst (optioneel)', + clientCert: 'Clientcertificaatpad', + privateKey: 'Privésleutelpad', + privateKeyPasswd: 'Privésleutelwachtwoord (optioneel)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certificaat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Bekabeld netwerk', + description: 'IP-instellingen en optionele 802.1X-authenticatie configureren', + connect: 'Bekabeld netwerk configureren', + connectDesc: 'IP-configuratie voor {{iface}} instellen en 802.1X inschakelen indien nodig', + ipConfig: 'IP-configuratie', + ipConfigDescription: 'Kies DHCP of stel een statisch adres in voor deze bekabelde interface', + authTitle: '802.1X-authenticatie', + authDescription: 'Alleen inschakelen als uw bekabelde netwerk gebruikersnaam, wachtwoord of certificaat-authenticatie vereist', + driverRequired: 'Bekabeld 802.1X vereist een systeem wpa_supplicant build met wired driver ondersteuning', + addressPlaceholder: 'Statisch IP-adres (bijv. 192.168.1.10)', + gatewayPlaceholder: 'Gateway (optioneel)', + passwordUnchanged: 'Geen wijziging', + privateKeyPasswdUnchanged: 'Geen wijziging', + off: 'Uitgeschakeld', + enterprise: 'Zakelijk / 802.1X', + active: '802.1X ingeschakeld', + inactive: '802.1X geconfigureerd, wacht op authenticatie', + failed: 'Configuratie van bekabeld netwerk 802.1X mislukt. Probeer het opnieuw.' + }, + tls: { description: 'HTTPS-protocol inschakelen', tip: 'Let op: HTTPS gebruiken kan de latentie verhogen, vooral in MJPEG-videomodus.' @@ -415,6 +461,11 @@ const nl = { ipAddress: 'IP-adres', subnetMask: 'Subnetmasker', router: 'Router', + wired: 'Bekabeld', + wireless: 'Draadloos', + signalStrength: 'Signaalsterkte', + rxRate: 'Ontvangstsnelheid', + txRate: 'Verzendsnelheid', none: 'Geen' } }, diff --git a/web/src/i18n/locales/pl.ts b/web/src/i18n/locales/pl.ts index be69332b..a8aca3cb 100644 --- a/web/src/i18n/locales/pl.ts +++ b/web/src/i18n/locales/pl.ts @@ -386,8 +386,54 @@ const pl = { password: 'Hasło', joinBtn: 'Połącz', confirmBtn: 'OK', - cancelBtn: 'Anuluj' + cancelBtn: 'Anuluj', + ipConfig: 'Konfiguracja IP', + ipConfigDescription: 'Wybierz DHCP lub ustaw statyczny adres dla tego połączenia Wi-Fi', + security: 'Bezpieczeństwo', + personal: 'Osobiste / WPA-PSK', + enterprise: 'Firmowe / 802.1X', + identity: 'Tożsamość / Nazwa użytkownika', + authentication: 'Uwierzytelnianie', + innerAuthentication: 'Uwierzytelnianie wewnętrzne', + anonymousIdentity: 'Anonimowa tożsamość (opcjonalnie)', + caCert: 'Ścieżka certyfikatu CA (opcjonalnie)', + domainSuffixMatch: 'Dopasowanie sufiksu domeny (opcjonalnie)', + clientCert: 'Ścieżka certyfikatu klienta', + privateKey: 'Ścieżka klucza prywatnego', + privateKeyPasswd: 'Hasło klucza prywatnego (opcjonalnie)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certyfikat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Sieć przewodowa', + description: 'Konfiguracja IP i opcjonalnego uwierzytelniania 802.1X', + connect: 'Skonfiguruj sieć przewodową', + connectDesc: 'Skonfiguruj IP dla {{iface}} i włącz 802.1X jeśli potrzeba', + ipConfig: 'Konfiguracja IP', + ipConfigDescription: 'Wybierz DHCP lub ustaw statyczny adres dla tego interfejsu przewodowego', + authTitle: 'Uwierzytelnianie 802.1X', + authDescription: 'Włącz tylko jeśli sieć przewodowa wymaga uwierzytelnienia nazwą użytkownika, hasłem lub certyfikatem', + driverRequired: 'Przewodowe 802.1X wymaga systemowego wpa_supplicant z obsługą sterownika wired', + addressPlaceholder: 'Statyczny adres IP (np. 192.168.1.10)', + gatewayPlaceholder: 'Brama (opcjonalnie)', + passwordUnchanged: 'Bez zmian', + privateKeyPasswdUnchanged: 'Bez zmian', + off: 'Wyłączone', + enterprise: 'Firmowe / 802.1X', + active: '802.1X włączone', + inactive: '802.1X skonfigurowane, oczekiwanie na uwierzytelnienie', + failed: 'Nie udało się skonfigurować sieci przewodowej 802.1X. Spróbuj ponownie.' + }, + tls: { description: 'Włącz protokół HTTPS', tip: 'Uwaga: użycie HTTPS może zwiększyć opóźnienie, szczególnie w trybie wideo MJPEG.' @@ -414,6 +460,11 @@ const pl = { ipAddress: 'Adres IP', subnetMask: 'Maska podsieci', router: 'Router', + wired: 'Przewodowa', + wireless: 'Bezprzewodowa', + signalStrength: 'Siła sygnału', + rxRate: 'Prędkość odbierania', + txRate: 'Prędkość wysyłania', none: 'Brak' } }, diff --git a/web/src/i18n/locales/pt_br.ts b/web/src/i18n/locales/pt_br.ts index e6fd78b3..16aeac0a 100644 --- a/web/src/i18n/locales/pt_br.ts +++ b/web/src/i18n/locales/pt_br.ts @@ -384,8 +384,54 @@ const pt_br = { password: 'Senha', joinBtn: 'Entrar', confirmBtn: 'OK', - cancelBtn: 'Cancelar' + cancelBtn: 'Cancelar', + ipConfig: 'Configuração de IP', + ipConfigDescription: 'Escolher DHCP ou definir um endereço estático para esta conexão Wi-Fi', + security: 'Segurança', + personal: 'Pessoal / WPA-PSK', + enterprise: 'Empresarial / 802.1X', + identity: 'Identidade / Nome de usuário', + authentication: 'Autenticação', + innerAuthentication: 'Autenticação interna', + anonymousIdentity: 'Identidade anônima (opcional)', + caCert: 'Caminho do certificado CA (opcional)', + domainSuffixMatch: 'Correspondência de sufixo de domínio (opcional)', + clientCert: 'Caminho do certificado cliente', + privateKey: 'Caminho da chave privada', + privateKeyPasswd: 'Senha da chave privada (opcional)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certificado)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Rede cabeada', + description: 'Configurar IP e autenticação 802.1X opcional', + connect: 'Configurar rede cabeada', + connectDesc: 'Configurar IP para {{iface}} e habilitar 802.1X se necessário', + ipConfig: 'Configuração de IP', + ipConfigDescription: 'Escolher DHCP ou definir um endereço estático para esta interface cabeada', + authTitle: 'Autenticação 802.1X', + authDescription: 'Ative apenas se sua rede cabeada exigir autenticação por usuário, senha ou certificado', + driverRequired: '802.1X cabeado requer um wpa_supplicant do sistema com suporte ao driver wired', + addressPlaceholder: 'Endereço IP estático (ex: 192.168.1.10)', + gatewayPlaceholder: 'Gateway (opcional)', + passwordUnchanged: 'Sem alteração', + privateKeyPasswdUnchanged: 'Sem alteração', + off: 'Desabilitado', + enterprise: 'Empresarial / 802.1X', + active: '802.1X habilitado', + inactive: '802.1X configurado, aguardando autenticação', + failed: 'Falha ao configurar a rede cabeada 802.1X. Por favor, tente novamente.' + }, + tls: { description: 'Habilitar protocolo HTTPS', tip: 'Atenção: O uso de HTTPS pode aumentar a latência, especialmente com o modo de vídeo MJPEG.' @@ -412,6 +458,11 @@ const pt_br = { ipAddress: 'Endereço IP', subnetMask: 'Máscara de sub-rede', router: 'Roteador', + wired: 'Cabeada', + wireless: 'Sem fio', + signalStrength: 'Intensidade do sinal', + rxRate: 'Taxa de recepção', + txRate: 'Taxa de transmissão', none: 'Nenhum' } }, diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index 9b79faa3..3cfa94fa 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -386,8 +386,54 @@ const ru = { password: 'Пароль', joinBtn: 'Подключить', confirmBtn: 'OK', - cancelBtn: 'Отмена' + cancelBtn: 'Отмена', + ipConfig: 'Настройка IP', + ipConfigDescription: 'Выберите DHCP или установите статический адрес для этого Wi-Fi подключения', + security: 'Безопасность', + personal: 'Личная / WPA-PSK', + enterprise: 'Корпоративная / 802.1X', + identity: 'Идентификатор / Имя пользователя', + authentication: 'Аутентификация', + innerAuthentication: 'Внутренняя аутентификация', + anonymousIdentity: 'Анонимный идентификатор (необязательно)', + caCert: 'Путь к сертификату CA (необязательно)', + domainSuffixMatch: 'Совпадение суффикса домена (необязательно)', + clientCert: 'Путь к клиентскому сертификату', + privateKey: 'Путь к закрытому ключу', + privateKeyPasswd: 'Пароль закрытого ключа (необязательно)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Сертификат)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Проводная сеть', + description: 'Настройка IP и опциональной аутентификации 802.1X', + connect: 'Настроить проводную сеть', + connectDesc: 'Настроить IP для {{iface}} и включить 802.1X при необходимости', + ipConfig: 'Настройка IP', + ipConfigDescription: 'Выберите DHCP или установите статический адрес для этого проводного интерфейса', + authTitle: 'Аутентификация 802.1X', + authDescription: 'Включите только если ваша проводная сеть требует аутентификацию по имени пользователя, паролю или сертификату', + driverRequired: 'Проводной 802.1X требует системный wpa_supplicant с поддержкой драйвера wired', + addressPlaceholder: 'Статический IP-адрес (например: 192.168.1.10)', + gatewayPlaceholder: 'Шлюз (необязательно)', + passwordUnchanged: 'Без изменений', + privateKeyPasswdUnchanged: 'Без изменений', + off: 'Отключено', + enterprise: 'Корпоративная / 802.1X', + active: '802.1X включён', + inactive: '802.1X настроен, ожидание аутентификации', + failed: 'Ошибка настройки проводной сети 802.1X. Пожалуйста, попробуйте снова.' + }, + tls: { description: 'Включить протокол HTTPS', tip: 'Имейте в виду: использование HTTPS может увеличить задержку, особенно в режиме видео MJPEG.' @@ -414,6 +460,11 @@ const ru = { ipAddress: 'IP-адрес', subnetMask: 'Маска подсети', router: 'Маршрутизатор', + wired: 'Проводная', + wireless: 'Беспроводная', + signalStrength: 'Уровень сигнала', + rxRate: 'Скорость приёма', + txRate: 'Скорость передачи', none: 'Нет' } }, diff --git a/web/src/i18n/locales/se.ts b/web/src/i18n/locales/se.ts index 7662d627..2fa3c8c0 100644 --- a/web/src/i18n/locales/se.ts +++ b/web/src/i18n/locales/se.ts @@ -380,8 +380,54 @@ const se = { password: 'Lösenord', joinBtn: 'Anslut', confirmBtn: 'OK', - cancelBtn: 'Avbryt' + cancelBtn: 'Avbryt', + ipConfig: 'IP-konfiguration', + ipConfigDescription: 'Välj DHCP eller ange en statisk adress för detta Wi-Fi-anslutning', + security: 'Säkerhet', + personal: 'Personlig / WPA-PSK', + enterprise: 'Företag / 802.1X', + identity: 'Identitet / Användarnamn', + authentication: 'Autentisering', + innerAuthentication: 'Inre autentisering', + anonymousIdentity: 'Anonym identitet (valfritt)', + caCert: 'CA-certifikatsökväg (valfritt)', + domainSuffixMatch: 'Domänuffix-matchning (valfritt)', + clientCert: 'Klientcertifikatsökväg', + privateKey: 'Privat nyckelsökväg', + privateKeyPasswd: 'Lösenord för privat nyckel (valfritt)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Certifikat)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Trådbundet nätverk', + description: 'Konfigurera IP-inställningar och valfri 802.1X-autentisering', + connect: 'Konfigurera trådbundet nätverk', + connectDesc: 'Konfigurera IP för {{iface}} och aktivera 802.1X vid behov', + ipConfig: 'IP-konfiguration', + ipConfigDescription: 'Välj DHCP eller ange en statisk adress för detta trådbundna gränssnitt', + authTitle: '802.1X-autentisering', + authDescription: 'Aktivera endast om ditt trådbundna nätverk kräver användarnamn, lösenord eller certifikat-autentisering', + driverRequired: 'Trådbundet 802.1X kräver en system-wpa_supplicant med stöd för wired-drivrutin', + addressPlaceholder: 'Statisk IP-adress (t.ex. 192.168.1.10)', + gatewayPlaceholder: 'Gateway (valfritt)', + passwordUnchanged: 'Ingen ändring', + privateKeyPasswdUnchanged: 'Ingen ändring', + off: 'Inaktiverad', + enterprise: 'Företag / 802.1X', + active: '802.1X aktiverad', + inactive: '802.1X konfigurerad, väntar på autentisering', + failed: 'Misslyckades med att konfigurera trådbundet nätverk 802.1X. Försök igen.' + }, + tls: { description: 'Aktivera HTTPS-protokoll', tip: 'Observera: Användning av HTTPS kan öka fördröjningen, särskilt med MJPEG-läge.' @@ -408,6 +454,11 @@ const se = { ipAddress: 'IP-adress', subnetMask: 'Subnätmask', router: 'Router', + wired: 'Trådbundet', + wireless: 'Trådlöst', + signalStrength: 'Signalstyrka', + rxRate: 'Mottagningshastighet', + txRate: 'Sändningshastighet', none: 'Ingen' } }, diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts index bf2d5a35..c26a8c2d 100644 --- a/web/src/i18n/locales/th.ts +++ b/web/src/i18n/locales/th.ts @@ -377,8 +377,54 @@ const th = { password: 'รหัสผ่าน', joinBtn: 'เข้าร่วม', confirmBtn: 'ตกลง', - cancelBtn: 'ยกเลิก' + cancelBtn: 'ยกเลิก', + ipConfig: 'กำหนดค่า IP', + ipConfigDescription: 'เลือก DHCP หรือตั้งค่าที่อยู่แบบคงที่สำหรับการเชื่อมต่อ Wi-Fi นี้', + security: 'ความปลอดภัย', + personal: 'ส่วนบุคคล / WPA-PSK', + enterprise: 'องค์กร / 802.1X', + identity: 'ID / ชื่อผู้ใช้', + authentication: 'การยืนยันตัวตน', + innerAuthentication: 'การยืนยันตัวตนภายใน', + anonymousIdentity: 'ID นิรนาม (ไม่บังคับ)', + caCert: 'เส้นทางใบรับรอง CA (ไม่บังคับ)', + domainSuffixMatch: 'การจับคู่นามสกุลโดเมน (ไม่บังคับ)', + clientCert: 'เส้นทางใบรับรองไคลเอนต์', + privateKey: 'เส้นทางกุญแจส่วนตัว', + privateKeyPasswd: 'รหัสผ่านกุญแจส่วนตัว (ไม่บังคับ)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (ใบรับรอง)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'เครือข่ายแบบมีสาย', + description: 'กำหนดค่า IP และการยืนยันตัวตน 802.1X แบบเลือกได้', + connect: 'กำหนดค่าเครือข่ายแบบมีสาย', + connectDesc: 'กำหนดค่า IP สำหรับ {{iface}} และเปิดใช้งาน 802.1X หากจำเป็น', + ipConfig: 'กำหนดค่า IP', + ipConfigDescription: 'เลือก DHCP หรือตั้งค่าที่อยู่แบบคงที่สำหรับอินเทอร์เฟซแบบมีสายนี้', + authTitle: 'การยืนยันตัวตน 802.1X', + authDescription: 'เปิดใช้งานเฉพาะเมื่อเครือข่ายแบบมีสายของคุณต้องการการยืนยันตัวตนด้วยชื่อผู้ใช้ รหัสผ่าน หรือใบรับรอง', + driverRequired: 'เครือข่ายแบบมีสาย 802.1X ต้องการ wpa_supplicant ที่รองรับไดรเวอร์ wired', + addressPlaceholder: 'ที่อยู่ IP แบบคงที่ (เช่น 192.168.1.10)', + gatewayPlaceholder: 'เกตเวย์ (ไม่บังคับ)', + passwordUnchanged: 'ไม่เปลี่ยนแปลง', + privateKeyPasswdUnchanged: 'ไม่เปลี่ยนแปลง', + off: 'ปิดใช้งาน', + enterprise: 'องค์กร / 802.1X', + active: '802.1X เปิดใช้งานแล้ว', + inactive: '802.1X กำหนดค่าแล้ว รอการยืนยันตัวตน', + failed: 'กำหนดค่าเครือข่ายแบบมีสาย 802.1X ล้มเหลว กรุณาลองใหม่' + }, + tls: { description: 'เปิดใช้งานโปรโตคอล HTTPS', tip: 'โปรดทราบ: การใช้ HTTPS อาจเพิ่มความหน่วง โดยเฉพาะในโหมดวิดีโอ MJPEG' @@ -405,6 +451,11 @@ const th = { ipAddress: 'ที่อยู่ IP', subnetMask: 'ซับเน็ตมาสก์', router: 'เราเตอร์', + wired: 'แบบมีสาย', + wireless: 'แบบไร้สาย', + signalStrength: 'ความแรงของสัญญาณ', + rxRate: 'อัตราการรับ', + txRate: 'อัตราการส่ง', none: 'ไม่มี' } }, diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index fcdf3ea3..2960f0c2 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -384,8 +384,54 @@ const tr = { password: 'Parola', joinBtn: 'Katıl', confirmBtn: 'Tamam', - cancelBtn: 'İptal' + cancelBtn: 'İptal', + ipConfig: 'IP yapılandırması', + ipConfigDescription: 'Bu Wi-Fi bağlantısı için DHCP seçin veya statik adres ayarlayın', + security: 'Güvenlik', + personal: 'Kişisel / WPA-PSK', + enterprise: 'Kurumsal / 802.1X', + identity: 'Kimlik / Kullanıcı adı', + authentication: 'Kimlik doğrulama', + innerAuthentication: 'Dahili kimlik doğrulama', + anonymousIdentity: 'Anonim kimlik (isteğe bağlı)', + caCert: 'CA sertifika yolu (isteğe bağlı)', + domainSuffixMatch: 'Alan adı son ek eşleşmesi (isteğe bağlı)', + clientCert: 'İstemci sertifika yolu', + privateKey: 'Özel anahtar yolu', + privateKeyPasswd: 'Özel anahtar şifresi (isteğe bağlı)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Sertifika)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Kablolu ağ', + description: 'IP ayarları ve isteğe bağlı 802.1X kimlik doğrulaması yapılandırması', + connect: 'Kablolu ağı yapılandır', + connectDesc: '{{iface}} için IP yapılandırmasını ayarlayın ve gerekirse 802.1X kimlik doğrulamasını etkinleştirin', + ipConfig: 'IP yapılandırması', + ipConfigDescription: 'Bu kablolu arayüz için DHCP seçin veya statik adres ayarlayın', + authTitle: '802.1X kimlik doğrulama', + authDescription: 'Yalnızca kablolu ağınız kullanıcı adı, şifre veya sertifika kimlik doğrulaması gerektiriyorsa etkinleştirin', + driverRequired: 'Kablolu 802.1X, wired sürücü desteğine sahip bir sistem wpa_supplicant derlemesi gerektirir', + addressPlaceholder: 'Statik IP adresi (örn: 192.168.1.10)', + gatewayPlaceholder: 'Ağ geçidi (isteğe bağlı)', + passwordUnchanged: 'Değişiklik yok', + privateKeyPasswdUnchanged: 'Değişiklik yok', + off: 'Devre dışı', + enterprise: 'Kurumsal / 802.1X', + active: '802.1X etkin', + inactive: '802.1X yapılandırıldı, kimlik doğrulama bekleniyor', + failed: 'Kablolu ağ 802.1X yapılandırması başarısız oldu. Lütfen tekrar deneyin.' + }, + tls: { description: 'HTTPS protokolünü etkinleştir', tip: 'HTTPS protokolü bağlantıda gecikmeye sebep olabilir, özellikle MJPEG görüntü modu ile.' @@ -412,6 +458,11 @@ const tr = { ipAddress: 'IP Adresi', subnetMask: 'Alt Ağ Maskesi', router: 'Yönlendirici', + wired: 'Kablolu', + wireless: 'Kablosuz', + signalStrength: 'Sinyal gücü', + rxRate: 'Alış hızı', + txRate: 'Gönderim hızı', none: 'Yok' } }, diff --git a/web/src/i18n/locales/uk.ts b/web/src/i18n/locales/uk.ts index 1187a4d4..260f4afe 100644 --- a/web/src/i18n/locales/uk.ts +++ b/web/src/i18n/locales/uk.ts @@ -9,14 +9,14 @@ const uk = { }, auth: { login: 'Вхід', - placeholderUsername: "Введіть ім'я користувача", + placeholderUsername: "Введіть ім\'я користувача", placeholderPassword: 'Введіть пароль', placeholderPassword2: 'Введіть пароль ще раз', noEmptyUsername: "Ім'я користувача не може бути порожнім", noEmptyPassword: 'Пароль не може бути порожнім', noAccount: 'Не вдалося отримати інформацію про користувача, оновіть веб-сторінку або скиньте пароль', - invalidUser: "Недійсне ім'я користувача або пароль", + invalidUser: "Недійсне ім\'я користувача або пароль", locked: 'Забагато входів, спробуйте пізніше', globalLocked: 'Система під захистом, спробуйте пізніше', error: 'Якась халепа! Непередбачена помилка :(', @@ -384,8 +384,54 @@ const uk = { password: 'Пароль', joinBtn: 'Підключити', confirmBtn: 'OK', - cancelBtn: 'Скасувати' + cancelBtn: 'Скасувати', + ipConfig: 'Налаштування IP', + ipConfigDescription: 'Оберіть DHCP або встановіть статичну адресу для цього Wi-Fi з\'єднання', + security: 'Безпека', + personal: 'Особиста / WPA-PSK', + enterprise: 'Корпоративна / 802.1X', + identity: 'Ідентифікатор / Ім\'я користувача', + authentication: 'Аутентифікація', + innerAuthentication: 'Внутрішня аутентифікація', + anonymousIdentity: 'Анонімний ідентифікатор (необов\'язково)', + caCert: 'Шлях до сертифіката CA (необов\'язково)', + domainSuffixMatch: 'Збіг суфіксу домену (необов\'язково)', + clientCert: 'Шлях до сертифіката клієнта', + privateKey: 'Шлях до приватного ключа', + privateKeyPasswd: 'Пароль приватного ключа (необов\'язково)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Сертифікат)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Дротова мережа', + description: 'Налаштування IP та опціональної аутентифікації 802.1X', + connect: 'Налаштувати дротову мережу', + connectDesc: 'Налаштувати IP для {{iface}} та увімкнути 802.1X за потреби', + ipConfig: 'Налаштування IP', + ipConfigDescription: 'Оберіть DHCP або встановіть статичну адресу для цього дротового інтерфейсу', + authTitle: 'Аутентифікація 802.1X', + authDescription: 'Увімкніть лише якщо ваша дротова мережа вимагає аутентифікацію за ім\'ям користувача, паролем або сертифікатом', + driverRequired: 'Дротовий 802.1X потребує системний wpa_supplicant з підтримкою драйвера wired', + addressPlaceholder: 'Статична IP-адреса (наприклад: 192.168.1.10)', + gatewayPlaceholder: 'Шлюз (необов\'язково)', + passwordUnchanged: 'Без змін', + privateKeyPasswdUnchanged: 'Без змін', + off: 'Вимкнено', + enterprise: 'Корпоративна / 802.1X', + active: '802.1X увімкнено', + inactive: '802.1X налаштовано, очікування аутентифікації', + failed: 'Помилка налаштування дротової мережі 802.1X. Спробуйте ще раз.' + }, + tls: { description: 'Увімкнути протокол HTTPS', tip: 'Будьте в курсі: Використання HTTPS може збільшити затримку, особливо в режимі відео MJPEG.' @@ -412,6 +458,11 @@ const uk = { ipAddress: 'IP-адреса', subnetMask: 'Маска підмережі', router: 'Маршрутизатор', + wired: 'Дротова', + wireless: 'Бездротова', + signalStrength: 'Рівень сигналу', + rxRate: 'Швидкість прийому', + txRate: 'Швидкість передачі', none: 'Немає' } }, diff --git a/web/src/i18n/locales/vi.ts b/web/src/i18n/locales/vi.ts index 873531d2..ef62cc75 100644 --- a/web/src/i18n/locales/vi.ts +++ b/web/src/i18n/locales/vi.ts @@ -381,8 +381,54 @@ const vi = { password: 'Mật khẩu', joinBtn: 'Tham gia', confirmBtn: 'OK', - cancelBtn: 'Hủy' + cancelBtn: 'Hủy', + ipConfig: 'Cấu hình IP', + ipConfigDescription: 'Chọn DHCP hoặc đặt địa chỉ tĩnh cho kết nối Wi-Fi này', + security: 'Bảo mật', + personal: 'Cá nhân / WPA-PSK', + enterprise: 'Doanh nghiệp / 802.1X', + identity: 'ID / Tên người dùng', + authentication: 'Xác thực', + innerAuthentication: 'Xác thực nội bộ', + anonymousIdentity: 'ID ẩn danh (tùy chọn)', + caCert: 'Đường dẫn chứng chỉ CA (tùy chọn)', + domainSuffixMatch: 'Khớp hậu tố tên miền (tùy chọn)', + clientCert: 'Đường dẫn chứng chỉ máy khách', + privateKey: 'Đường dẫn khóa riêng', + privateKeyPasswd: 'Mật khẩu khóa riêng (tùy chọn)', + eapPeap: 'Protected EAP (PEAP)', + eapTtls: 'Tunneled TLS (TTLS)', + eapTls: 'TLS (Chứng chỉ)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', }, + ethernet: { + title: 'Mạng có dây', + description: 'Cấu hình IP và xác thực 802.1X tùy chọn', + connect: 'Cấu hình mạng có dây', + connectDesc: 'Cấu hình IP cho {{iface}} và bật 802.1X nếu cần', + ipConfig: 'Cấu hình IP', + ipConfigDescription: 'Chọn DHCP hoặc đặt địa chỉ tĩnh cho giao diện có dây này', + authTitle: 'Xác thực 802.1X', + authDescription: 'Chỉ bật nếu mạng có dây của bạn yêu cầu xác thực bằng tên người dùng, mật khẩu hoặc chứng chỉ', + driverRequired: 'Mạng có dây 802.1X cần wpa_supplicant với hỗ trợ trình điều khiển wired', + addressPlaceholder: 'Địa chỉ IP tĩnh (vd: 192.168.1.10)', + gatewayPlaceholder: 'Cổng mạng (tùy chọn)', + passwordUnchanged: 'Không thay đổi', + privateKeyPasswdUnchanged: 'Không thay đổi', + off: 'Tắt', + enterprise: 'Doanh nghiệp / 802.1X', + active: '802.1X đã bật', + inactive: '802.1X đã cấu hình, đang chờ xác thực', + failed: 'Cấu hình mạng có dây 802.1X thất bại. Vui lòng thử lại.' + }, + tls: { description: 'Bật giao thức HTTPS', tip: 'Lưu ý: Sử dụng HTTPS có thể tăng độ trễ, đặc biệt trong chế độ video MJPEG.' @@ -409,6 +455,11 @@ const vi = { ipAddress: 'Địa chỉ IP', subnetMask: 'Mặt nạ mạng con', router: 'Bộ định tuyến', + wired: 'Có dây', + wireless: 'Không dây', + signalStrength: 'Cường độ tín hiệu', + rxRate: 'Tốc độ nhận', + txRate: 'Tốc độ gửi', none: 'Không có' } }, diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index f11277be..b6314d97 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -367,14 +367,58 @@ const zh = { connect: '连接 Wi-Fi', connectDesc1: '请输入网络名称和密码', connectDesc2: '请输入密码以连接此网络', + ipConfig: 'IP 配置', + ipConfigDescription: '为此 Wi-Fi 连接选择 DHCP 或设置静态地址', disconnect: '是否要断开该网络连接?', failed: '连接失败,请重试', ssid: '名称', password: '密码', + security: '安全性', + personal: '个人 / WPA-PSK', + enterprise: '企业 / 802.1X', + identity: '身份 / 用户名', + authentication: '认证方式', + innerAuthentication: '内部认证', + anonymousIdentity: '匿名身份(可选)', + caCert: 'CA 证书路径(可选)', + domainSuffixMatch: '域名后缀匹配(可选)', + clientCert: '客户端证书路径', + privateKey: '私钥路径', + privateKeyPasswd: '私钥密码(可选)', + eapPeap: '受保护的 EAP (PEAP)', + eapTtls: '隧道 TLS (TTLS)', + eapTls: 'TLS(证书)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', joinBtn: '加入', confirmBtn: '确定', cancelBtn: '取消' }, + ethernet: { + title: '有线网络', + description: '配置 IP 信息和可选的 802.1X 认证', + connect: '配置有线网络', + connectDesc: '为 {{iface}} 配置 IP 信息,并在需要时启用 802.1X 认证', + ipConfig: 'IP 配置', + ipConfigDescription: '为此有线接口选择 DHCP 或设置静态地址', + authTitle: '802.1X 认证', + authDescription: '仅当你的有线网络需要用户名、密码或证书认证时才启用', + addressPlaceholder: '静态 IP 地址(例如 192.168.1.10)', + gatewayPlaceholder: '网关(可选)', + passwordUnchanged: '不修改', + privateKeyPasswdUnchanged: '不修改', + off: '已禁用', + enterprise: '企业 / 802.1X', + active: '802.1X 已启用', + inactive: '802.1X 已配置,等待认证', + failed: '配置有线网络 802.1X 失败,请重试。' + }, tls: { description: '启用 HTTPS 协议', tip: '注意:使用 HTTPS 可能导致延迟增加,特别是在 MJPEG 视频模式下。' @@ -398,9 +442,14 @@ const zh = { manualServersDescription: 'DNS 服务器可以手动编辑', networkDetails: '网络详情', interface: '接口', + wired: '有线', + wireless: '无线', ipAddress: 'IP 地址', subnetMask: '子网掩码', - router: '路由器', + router: '路由', + signalStrength: '信号强度', + rxRate: '接收速率', + txRate: '发送速率', none: '无' } }, diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index e2aa1bf5..e0825410 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -367,14 +367,58 @@ const zh_tw = { connect: '連線 Wi-Fi', connectDesc1: '請輸入網路名稱和密碼', connectDesc2: '請輸入密碼以連線此網路', + ipConfig: 'IP 設定', + ipConfigDescription: '為此 Wi-Fi 連線選擇 DHCP 或設定靜態位址', disconnect: '是否要中斷該網路連線?', failed: '連線失敗,請重試', ssid: 'SSID 名稱', password: '密碼', + security: '安全性', + personal: '個人 / WPA-PSK', + enterprise: '企業 / 802.1X', + identity: '身分 / 使用者名稱', + authentication: '認證方式', + innerAuthentication: '內部認證', + anonymousIdentity: '匿名身分(選填)', + caCert: 'CA 憑證路徑(選填)', + domainSuffixMatch: '網域後綴比對(選填)', + clientCert: '用戶端憑證路徑', + privateKey: '私鑰路徑', + privateKeyPasswd: '私鑰密碼(選填)', + eapPeap: '受保護的 EAP (PEAP)', + eapTtls: '通道 TLS (TTLS)', + eapTls: 'TLS(憑證)', + eapPwd: 'PWD', + eapLeap: 'LEAP', + authMschapv2: 'MSCHAPv2', + authMschap: 'MSCHAP', + authChap: 'CHAP', + authPap: 'PAP', + authGtc: 'GTC', + authMd5: 'MD5', joinBtn: '加入', confirmBtn: '確定', cancelBtn: '取消' }, + ethernet: { + title: '有線網路', + description: '設定 IP 資訊與可選的 802.1X 驗證', + connect: '設定有線網路', + connectDesc: '為 {{iface}} 設定 IP 資訊,並在需要時啟用 802.1X 驗證', + ipConfig: 'IP 設定', + ipConfigDescription: '為此有線介面選擇 DHCP 或設定靜態位址', + authTitle: '802.1X 驗證', + authDescription: '僅在你的有線網路需要使用者名稱、密碼或憑證驗證時啟用', + addressPlaceholder: '靜態 IP 位址(例如 192.168.1.10)', + gatewayPlaceholder: '閘道(可選)', + passwordUnchanged: '不變更', + privateKeyPasswdUnchanged: '不變更', + off: '已停用', + enterprise: '企業 / 802.1X', + active: '802.1X 已啟用', + inactive: '802.1X 已設定,等待驗證', + failed: '設定有線網路 802.1X 失敗,請重試。' + }, tls: { description: '啟用 HTTPS 協議', tip: '啟用 HTTPS 可以提高安全性,但可能會增加傳輸延遲,特別是使用 MJPEG 格式傳輸時。' @@ -398,9 +442,14 @@ const zh_tw = { manualServersDescription: 'DNS 伺服器可以手動編輯', networkDetails: '網路詳細資訊', interface: '介面', + wired: '有線', + wireless: '無線', ipAddress: 'IP 位址', subnetMask: '子網路遮罩', - router: '路由器', + router: '路由', + signalStrength: '訊號強度', + rxRate: '接收速率', + txRate: '傳送速率', none: '無' } }, diff --git a/web/src/pages/desktop/menu/settings/network/dns.tsx b/web/src/pages/desktop/menu/settings/network/dns.tsx index 399c7321..5ec5c549 100644 --- a/web/src/pages/desktop/menu/settings/network/dns.tsx +++ b/web/src/pages/desktop/menu/settings/network/dns.tsx @@ -11,26 +11,10 @@ type DNSState = { mode: DNSMode; servers: string[]; dhcp: string[]; - info: DNSInfo; -}; - -type DNSInfo = { - interface?: string; - type?: string; - address?: string; - subnetMask?: string; - gateway?: string; }; const maxServers = 6; -function formatInterface(info: DNSInfo) { - if (!info.interface) return ''; - if (!info.type) return info.interface; - - return `${info.type} (${info.interface})`; -} - function normalizeServers(servers: string[]) { const seen = new Set(); const normalized: string[] = []; @@ -100,31 +84,6 @@ const Panel = ({ ); }; -const InfoRow = ({ - label, - value, - isLast = false -}: { - label: string; - value?: string; - isLast?: boolean; -}) => { - return ( -
-
- {label} - - {value || '-'} - -
-
- ); -}; - const ServerList = ({ servers }: { servers: string[] }) => { const { t } = useTranslation(); @@ -202,7 +161,6 @@ export const DNS = () => { const [servers, setServers] = useState([]); const [originalServers, setOriginalServers] = useState([]); const [dhcp, setDHCP] = useState([]); - const [info, setInfo] = useState({}); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); @@ -233,7 +191,6 @@ export const DNS = () => { setServers(fetchedServers); setOriginalServers(fetchedServers); setDHCP(data.dhcp || []); - setInfo(data.info || {}); } catch (err) { console.log(err); } finally { @@ -347,74 +304,63 @@ export const DNS = () => { /> -
- - - - - - - - - {mode === 'manual' ? ( -
- {servers.length === 0 ? ( -
- {t('settings.network.dns.none')} -
- ) : ( - servers.map((server, index) => ( - updateServer(index, val)} - onRemove={() => removeServer(index)} + + {mode === 'manual' ? ( +
+ {servers.length === 0 ? ( +
+ {t('settings.network.dns.none')} +
+ ) : ( + servers.map((server, index) => ( + updateServer(index, val)} + onRemove={() => removeServer(index)} + /> + )) + )} + + {canAdd && ( +
+
+ + -
- )} - - {/* Validation hints */} - {(hasInvalidServer || isExceedMax) && ( -
- {hasInvalidServer && ( -
{t('settings.network.dns.invalid')}
- )} - {isExceedMax && ( -
- {t('settings.network.dns.maxServers', { count: maxServers })} -
- )} -
- )} -
- ) : ( - - )} -
-
+
+ )} + + {(hasInvalidServer || isExceedMax) && ( +
+ {hasInvalidServer && ( +
{t('settings.network.dns.invalid')}
+ )} + {isExceedMax && ( +
+ {t('settings.network.dns.maxServers', { count: maxServers })} +
+ )} +
+ )} + + ) : ( + + )} + {/* Footer: status + save button */} {(hasChanges || statusText) && ( diff --git a/web/src/pages/desktop/menu/settings/network/ethernet.tsx b/web/src/pages/desktop/menu/settings/network/ethernet.tsx new file mode 100644 index 00000000..6ea36e72 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/network/ethernet.tsx @@ -0,0 +1,413 @@ +import { useEffect, useState } from 'react'; +import { ApiOutlined, LockOutlined, UserOutlined } from '@ant-design/icons'; +import { Button, Input, Modal, Segmented, Select, Switch } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import * as api from '@/api/network.ts'; +import type { EthernetIPMode, EthernetSecurityMode } from '@/api/network.ts'; + +type EAPMethod = 'PEAP' | 'TTLS' | 'TLS' | 'PWD' | 'LEAP'; + +const getDefaultPhase2 = (eap: EAPMethod) => { + if (eap === 'PEAP' || eap === 'TTLS') return 'auth=MSCHAPV2'; + return ''; +}; + +const getEapOptions = (t: (key: string) => string) => [ + { value: 'PEAP', label: t('settings.network.wifi.eapPeap') }, + { value: 'TTLS', label: t('settings.network.wifi.eapTtls') }, + { value: 'TLS', label: t('settings.network.wifi.eapTls') }, + { value: 'PWD', label: t('settings.network.wifi.eapPwd') }, + { value: 'LEAP', label: t('settings.network.wifi.eapLeap') } +]; + +const getInnerAuthOptions = (eap: EAPMethod, t: (key: string) => string) => { + const peapInnerAuthOptions = [ + { value: 'auth=MSCHAPV2', label: t('settings.network.wifi.authMschapv2') }, + { value: 'auth=GTC', label: t('settings.network.wifi.authGtc') }, + { value: 'auth=MD5', label: t('settings.network.wifi.authMd5') } + ]; + const ttlsInnerAuthOptions = [ + { value: 'auth=MSCHAPV2', label: t('settings.network.wifi.authMschapv2') }, + { value: 'auth=MSCHAP', label: t('settings.network.wifi.authMschap') }, + { value: 'auth=CHAP', label: t('settings.network.wifi.authChap') }, + { value: 'auth=PAP', label: t('settings.network.wifi.authPap') } + ]; + if (eap === 'PEAP') return peapInnerAuthOptions; + if (eap === 'TTLS') return ttlsInnerAuthOptions; + return []; +}; + +const usesPassword = (eap: EAPMethod) => eap !== 'TLS'; +const usesInnerAuth = (eap: EAPMethod) => eap === 'PEAP' || eap === 'TTLS'; + +function isValidIPv4(value: string) { + const parts = value.split('.'); + if (parts.length !== 4) return false; + + return parts.every((part) => { + if (!/^\d+$/.test(part)) return false; + if (part.length > 1 && part.startsWith('0')) return false; + + const number = Number(part); + return number >= 0 && number <= 255; + }); +} + +function isValidSubnetMask(value: string) { + const parts = value.split('.'); + if (parts.length !== 4) return false; + + const bytes = parts.map((part) => { + if (!/^\d+$/.test(part)) return NaN; + const number = Number(part); + if (number < 0 || number > 255) return NaN; + return number; + }); + if (bytes.some(Number.isNaN)) return false; + + const mask = bytes + .map((byte) => byte.toString(2).padStart(8, '0')) + .join(''); + if (!/^1*0*$/.test(mask)) return false; + + const ones = mask.indexOf('0') === -1 ? 32 : mask.indexOf('0'); + return ones >= 1 && ones <= 32; +} + +export const Ethernet = () => { + const { t } = useTranslation(); + + const [configured, setConfigured] = useState(false); + const [connected, setConnected] = useState(false); + const [iface, setIface] = useState('eth0'); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [mode, setMode] = useState('off'); + const [ipMode, setIPMode] = useState('dhcp'); + const [address, setAddress] = useState(''); + const [subnetMask, setSubnetMask] = useState(''); + const [gateway, setGateway] = useState(''); + const [password, setPassword] = useState(''); + const [passwordSet, setPasswordSet] = useState(false); + const [identity, setIdentity] = useState(''); + const [eap, setEap] = useState('PEAP'); + const [phase2, setPhase2] = useState('auth=MSCHAPV2'); + const [anonymousIdentity, setAnonymousIdentity] = useState(''); + const [caCert, setCaCert] = useState(''); + const [clientCert, setClientCert] = useState(''); + const [privateKey, setPrivateKey] = useState(''); + const [privateKeyPasswd, setPrivateKeyPasswd] = useState(''); + const [privateKeyPasswdSet, setPrivateKeyPasswdSet] = useState(false); + const [domainSuffixMatch, setDomainSuffixMatch] = useState(''); + const [status, setStatus] = useState<'' | 'saving'>(''); + const [message, setMessage] = useState(''); + + useEffect(() => { + getEthernet(); + }, []); + + async function getEthernet() { + try { + const rsp = await api.getEthernet(); + if (rsp.code !== 0) return; + + setConfigured(!!rsp.data?.configured); + setConnected(!!rsp.data?.connected); + setIface(rsp.data?.interface || 'eth0'); + setMode(rsp.data?.mode === 'enterprise' ? 'enterprise' : 'off'); + setIPMode(rsp.data?.ipMode === 'manual' ? 'manual' : 'dhcp'); + setAddress(rsp.data?.address || ''); + setSubnetMask(rsp.data?.subnetMask || ''); + setGateway(rsp.data?.gateway || ''); + setPassword(''); + setPasswordSet(!!rsp.data?.passwordSet); + setIdentity(rsp.data?.identity || ''); + setEap((rsp.data?.eap as EAPMethod) || 'PEAP'); + setPhase2(rsp.data?.phase2 || 'auth=MSCHAPV2'); + setAnonymousIdentity(rsp.data?.anonymousIdentity || ''); + setCaCert(rsp.data?.caCert || ''); + setClientCert(rsp.data?.clientCert || ''); + setPrivateKey(rsp.data?.privateKey || ''); + setPrivateKeyPasswd(''); + setPrivateKeyPasswdSet(!!rsp.data?.privateKeyPasswdSet); + setDomainSuffixMatch(rsp.data?.domainSuffixMatch || ''); + } catch { + /* empty */ + } + } + + async function save() { + setMessage(''); + + if (mode === 'enterprise' && !identity) return; + if (mode === 'enterprise' && usesPassword(eap) && !password && !passwordSet) return; + if (mode === 'enterprise' && eap === 'TLS' && (!clientCert || !privateKey)) return; + if (ipMode === 'manual' && !isValidIPv4(address)) return; + if (ipMode === 'manual' && !subnetMask) return; + if (ipMode === 'manual' && !isValidSubnetMask(subnetMask)) return; + if (ipMode === 'manual' && gateway && !isValidIPv4(gateway)) return; + if (status !== '') return; + + setStatus('saving'); + + try { + const rsp = await api.setEthernet({ + mode, + ipMode, + address, + subnetMask, + gateway, + password, + identity, + eap, + phase2, + anonymousIdentity, + caCert, + clientCert, + privateKey, + privateKeyPasswd, + domainSuffixMatch + }); + if (rsp.code !== 0) { + setMessage(t('settings.network.ethernet.failed')); + await getEthernet(); + return; + } + + setConfigured(mode === 'enterprise'); + setIsModalOpen(false); + await getEthernet(); + } finally { + setStatus(''); + } + } + + function openModal() { + setPassword(''); + setPrivateKeyPasswd(''); + setMessage(''); + setIsModalOpen(true); + } + + function closeModal() { + if (status !== '') return; + setIsModalOpen(false); + } + + function changeEap(value: EAPMethod) { + setEap(value); + setPhase2(getDefaultPhase2(value)); + if (value === 'TLS') { + setPassword(''); + } + } + + const statusText = configured + ? connected + ? t('settings.network.ethernet.active') + : t('settings.network.ethernet.inactive') + : t('settings.network.ethernet.description'); + const enterpriseEnabled = mode === 'enterprise'; + + return ( + <> +
+
+ {t('settings.network.ethernet.title')} + {statusText} +
+ + +
+ + +
+
+ +
+ +
+ {t('settings.network.ethernet.connect')} + + {t('settings.network.ethernet.connectDesc', { iface })} + +
+
+ +
+
+
+ {t('settings.network.ethernet.ipConfig')} + + {t('settings.network.ethernet.ipConfigDescription')} + +
+ setIPMode(value as EthernetIPMode)} + /> + + {ipMode === 'manual' && ( +
+ setAddress(e.target.value)} + status={address !== '' && !isValidIPv4(address) ? 'error' : undefined} + /> + setSubnetMask(e.target.value)} + status={subnetMask !== '' && !isValidSubnetMask(subnetMask) ? 'error' : undefined} + /> + setGateway(e.target.value)} + status={gateway !== '' && !isValidIPv4(gateway) ? 'error' : undefined} + /> +
+ )} +
+ +
+ +
+
+ {t('settings.network.ethernet.authTitle')} + + {t('settings.network.ethernet.authDescription')} + +
+ { + if (!enabled) { + setMode('off'); + return; + } + setMode('enterprise'); + if (!identity) setIdentity(''); + if (!eap) setEap('PEAP'); + if (!phase2) setPhase2('auth=MSCHAPV2'); + }} + /> +
+ + {enterpriseEnabled && ( + <> + } + placeholder={t('settings.network.wifi.identity')} + onChange={(e) => setIdentity(e.target.value)} + /> + {usesPassword(eap) && ( + } + placeholder={ + passwordSet + ? t('settings.network.ethernet.passwordUnchanged') + : t('settings.network.wifi.password') + } + onChange={(e) => setPassword(e.target.value)} + /> + )} + + )} + setAnonymousIdentity(e.target.value)} + /> + {eap === 'TLS' && ( + <> + setClientCert(e.target.value)} + /> + setPrivateKey(e.target.value)} + /> + setPrivateKeyPasswd(e.target.value)} + /> + + )} + setCaCert(e.target.value)} + /> + setDomainSuffixMatch(e.target.value)} + /> + + )} + + {!!message && {message}} +
+ + + ); +}; diff --git a/web/src/pages/desktop/menu/settings/network/index.tsx b/web/src/pages/desktop/menu/settings/network/index.tsx index a85fd1cc..07d25f81 100644 --- a/web/src/pages/desktop/menu/settings/network/index.tsx +++ b/web/src/pages/desktop/menu/settings/network/index.tsx @@ -2,6 +2,8 @@ import { Divider } from 'antd'; import { useTranslation } from 'react-i18next'; import { DNS } from './dns.tsx'; +import { Ethernet } from './ethernet.tsx'; +import { NetworkDetails } from './network-details.tsx'; import { Tls } from './tls.tsx'; import { Wifi } from './wifi.tsx'; @@ -15,7 +17,9 @@ export const Network = () => {
+ +
diff --git a/web/src/pages/desktop/menu/settings/network/network-details.tsx b/web/src/pages/desktop/menu/settings/network/network-details.tsx new file mode 100644 index 00000000..44eec050 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/network/network-details.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import * as api from '@/api/network.ts'; + +type NetworkInfo = { + interface?: string; + type?: string; + address?: string; + subnetMask?: string; + gateway?: string; + signal?: string; + rxRate?: string; + txRate?: string; +}; + +type NetworkDetailsData = { + info?: NetworkInfo; + infos?: NetworkInfo[]; +}; + +function formatInterfaceType(type: string | undefined, t: (key: string) => string) { + const translate = t as unknown as (key: string, options?: { defaultValue?: string }) => string; + switch ((type || '').toLowerCase()) { + case 'wired': + return translate('settings.network.dns.wired', { defaultValue: 'Wired' }); + case 'wireless': + return translate('settings.network.dns.wireless', { defaultValue: 'Wireless' }); + default: + return type || ''; + } +} + +function isWireless(info: NetworkInfo) { + return (info.type || '').toLowerCase() === 'wireless'; +} + +function formatInterface(info: NetworkInfo, t: (key: string) => string) { + if (!info.interface) return ''; + const type = formatInterfaceType(info.type, t); + if (!type) return info.interface; + + return `${type} (${info.interface})`; +} + +const InfoRow = ({ + label, + value, + isLast = false +}: { + label: string; + value?: string; + isLast?: boolean; +}) => { + return ( +
+ {label} + + {value || '-'} + +
+ ); +}; + +const WirelessRow = ({ label, value }: { label: string; value?: string }) => { + if (!value) return null; + + return ; +}; + +const NetworkCard = ({ info, isLast = false }: { info: NetworkInfo; isLast?: boolean }) => { + const { t } = useTranslation(); + + return ( +
+
+
{formatInterface(info, t) || '-'}
+
+
+ + + + {isWireless(info) && ( + <> + + + + + )} +
+
+ ); +}; + +export const NetworkDetails = () => { + const { t } = useTranslation(); + const [details, setDetails] = useState([]); + + useEffect(() => { + getNetworkDetails(); + }, []); + + async function getNetworkDetails() { + try { + const rsp = await api.getDNS(); + if (rsp.code !== 0) return; + + const data = rsp.data as NetworkDetailsData | undefined; + const infos = data?.infos?.length ? data.infos : data?.info ? [data.info] : []; + setDetails(infos); + } catch { + /* empty */ + } + } + + return ( +
+
+
+ {t('settings.network.dns.networkDetails')} +
+
+
+ {details.length > 0 ? ( + details.map((info, index) => ( + + )) + ) : ( +
+ {t('settings.network.dns.none')} +
+ )} +
+
+ ); +}; diff --git a/web/src/pages/desktop/menu/settings/network/wifi.tsx b/web/src/pages/desktop/menu/settings/network/wifi.tsx index d6114a65..00740571 100644 --- a/web/src/pages/desktop/menu/settings/network/wifi.tsx +++ b/web/src/pages/desktop/menu/settings/network/wifi.tsx @@ -1,10 +1,78 @@ import { useEffect, useState } from 'react'; -import { LockOutlined, WifiOutlined } from '@ant-design/icons'; -import { Button, Input, Modal, Switch } from 'antd'; +import { LockOutlined, UserOutlined, WifiOutlined } from '@ant-design/icons'; +import { Button, Input, Modal, Select, Segmented, Switch } from 'antd'; import { WifiIcon, WifiPenIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import * as api from '@/api/network.ts'; +import type { WiFiIPMode, WiFiSecurityMode } from '@/api/network.ts'; + +type EAPMethod = 'PEAP' | 'TTLS' | 'TLS' | 'PWD' | 'LEAP'; + +const getDefaultPhase2 = (eap: EAPMethod) => { + if (eap === 'PEAP' || eap === 'TTLS') return 'auth=MSCHAPV2'; + return ''; +}; + +const getEapOptions = (t: (key: string) => string) => [ + { value: 'PEAP', label: t('settings.network.wifi.eapPeap') }, + { value: 'TTLS', label: t('settings.network.wifi.eapTtls') }, + { value: 'TLS', label: t('settings.network.wifi.eapTls') }, + { value: 'PWD', label: t('settings.network.wifi.eapPwd') }, + { value: 'LEAP', label: t('settings.network.wifi.eapLeap') } +]; + +const getInnerAuthOptions = (eap: EAPMethod, t: (key: string) => string) => { + const peapInnerAuthOptions = [ + { value: 'auth=MSCHAPV2', label: t('settings.network.wifi.authMschapv2') }, + { value: 'auth=GTC', label: t('settings.network.wifi.authGtc') }, + { value: 'auth=MD5', label: t('settings.network.wifi.authMd5') } + ]; + const ttlsInnerAuthOptions = [ + { value: 'auth=MSCHAPV2', label: t('settings.network.wifi.authMschapv2') }, + { value: 'auth=MSCHAP', label: t('settings.network.wifi.authMschap') }, + { value: 'auth=CHAP', label: t('settings.network.wifi.authChap') }, + { value: 'auth=PAP', label: t('settings.network.wifi.authPap') } + ]; + if (eap === 'PEAP') return peapInnerAuthOptions; + if (eap === 'TTLS') return ttlsInnerAuthOptions; + return []; +}; + +const usesPassword = (eap: EAPMethod) => eap !== 'TLS'; +const usesInnerAuth = (eap: EAPMethod) => eap === 'PEAP' || eap === 'TTLS'; + +function isValidIPv4(value: string) { + const parts = value.split('.'); + if (parts.length !== 4) return false; + + return parts.every((part) => { + if (!/^\d+$/.test(part)) return false; + if (part.length > 1 && part.startsWith('0')) return false; + + const number = Number(part); + return number >= 0 && number <= 255; + }); +} + +function isValidSubnetMask(value: string) { + const parts = value.split('.'); + if (parts.length !== 4) return false; + + const bytes = parts.map((part) => { + if (!/^\d+$/.test(part)) return NaN; + const number = Number(part); + if (number < 0 || number > 255) return NaN; + return number; + }); + if (bytes.some(Number.isNaN)) return false; + + const mask = bytes.map((byte) => byte.toString(2).padStart(8, '0')).join(''); + if (!/^1*0*$/.test(mask)) return false; + + const ones = mask.indexOf('0') === -1 ? 32 : mask.indexOf('0'); + return ones >= 1 && ones <= 32; +} export const Wifi = () => { const { t } = useTranslation(); @@ -14,8 +82,24 @@ export const Wifi = () => { const [connectedWiFi, setConnectedWifi] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); + const [mode, setMode] = useState('psk'); + const [ipMode, setIPMode] = useState('dhcp'); + const [address, setAddress] = useState(''); + const [subnetMask, setSubnetMask] = useState(''); + const [gateway, setGateway] = useState(''); const [ssid, setSsid] = useState(''); const [password, setPassword] = useState(''); + const [passwordSet, setPasswordSet] = useState(false); + const [identity, setIdentity] = useState(''); + const [eap, setEap] = useState('PEAP'); + const [phase2, setPhase2] = useState('auth=MSCHAPV2'); + const [anonymousIdentity, setAnonymousIdentity] = useState(''); + const [caCert, setCaCert] = useState(''); + const [clientCert, setClientCert] = useState(''); + const [privateKey, setPrivateKey] = useState(''); + const [privateKeyPasswd, setPrivateKeyPasswd] = useState(''); + const [privateKeyPasswdSet, setPrivateKeyPasswdSet] = useState(false); + const [domainSuffixMatch, setDomainSuffixMatch] = useState(''); const [status, setStatus] = useState<'' | 'connecting' | 'disconnecting'>(''); const [message, setMessage] = useState(''); @@ -33,6 +117,24 @@ export const Wifi = () => { setIsSupported(!!rsp.data?.supported); setIsAPMode(!!rsp.data?.apMode); + setMode(rsp.data?.mode === 'enterprise' ? 'enterprise' : 'psk'); + setIPMode(rsp.data?.ipMode === 'manual' ? 'manual' : 'dhcp'); + setAddress(rsp.data?.address || ''); + setSubnetMask(rsp.data?.subnetMask || ''); + setGateway(rsp.data?.gateway || ''); + setSsid(rsp.data?.ssid || ''); + setPassword(''); + setPasswordSet(!!rsp.data?.passwordSet); + setIdentity(rsp.data?.identity || ''); + setEap((rsp.data?.eap as EAPMethod) || 'PEAP'); + setPhase2(rsp.data?.phase2 || 'auth=MSCHAPV2'); + setAnonymousIdentity(rsp.data?.anonymousIdentity || ''); + setCaCert(rsp.data?.caCert || ''); + setClientCert(rsp.data?.clientCert || ''); + setPrivateKey(rsp.data?.privateKey || ''); + setPrivateKeyPasswd(''); + setPrivateKeyPasswdSet(!!rsp.data?.privateKeyPasswdSet); + setDomainSuffixMatch(rsp.data?.domainSuffixMatch || ''); if (rsp.data?.connected && rsp.data?.ssid) { setConnectedWifi(rsp.data.ssid); @@ -47,13 +149,35 @@ export const Wifi = () => { async function connect() { setMessage(''); - if (!ssid || !password) return; + if (!ssid || (mode === 'psk' && !password && !passwordSet)) return; + if (mode === 'enterprise' && (!identity || (usesPassword(eap) && !password && !passwordSet))) + return; + if (mode === 'enterprise' && eap === 'TLS' && (!clientCert || !privateKey)) return; + if (ipMode === 'manual' && !isValidIPv4(address)) return; + if (ipMode === 'manual' && !subnetMask) return; + if (ipMode === 'manual' && !isValidSubnetMask(subnetMask)) return; + if (ipMode === 'manual' && gateway && !isValidIPv4(gateway)) return; if (status !== '') return; setStatus('connecting'); try { - const rsp = await api.connectWifi(ssid, password); + const rsp = await api.connectWifi(ssid, password, { + mode, + ipMode, + address, + subnetMask, + gateway, + identity, + eap, + phase2, + anonymousIdentity, + caCert, + clientCert, + privateKey, + privateKeyPasswd, + domainSuffixMatch + }); if (rsp.code !== 0) { console.log(rsp.msg); setMessage(t('settings.network.wifi.failed')); @@ -90,9 +214,10 @@ export const Wifi = () => { } } - function openModal() { - setSsid(''); + async function openModal() { + await getWiFi(); setPassword(''); + setPrivateKeyPasswd(''); setMessage(''); setIsModalOpen(true); } @@ -102,6 +227,14 @@ export const Wifi = () => { setIsModalOpen(false); } + function changeEap(value: EAPMethod) { + setEap(value); + setPhase2(getDefaultPhase2(value)); + if (value === 'TLS') { + setPassword(''); + } + } + if (!isSupported) { return <>; } @@ -134,12 +267,16 @@ export const Wifi = () => {
@@ -186,6 +323,64 @@ export const Wifi = () => { {/* form */}
+
+
+ {t('settings.network.wifi.ipConfig')} +
+ setIPMode(value as WiFiIPMode)} + /> + + {ipMode === 'manual' && ( +
+
+ {t('settings.network.dns.ipAddress')} + setAddress(e.target.value)} + status={address !== '' && !isValidIPv4(address) ? 'error' : undefined} + /> +
+
+ {t('settings.network.dns.subnetMask')} + setSubnetMask(e.target.value)} + status={subnetMask !== '' && !isValidSubnetMask(subnetMask) ? 'error' : undefined} + /> +
+
+ {t('settings.network.dns.router')} + setGateway(e.target.value)} + status={gateway !== '' && !isValidIPv4(gateway) ? 'error' : undefined} + /> +
+
+ )} +
+ +
+ + { placeholder={t('settings.network.wifi.ssid')} onChange={(e) => setSsid(e.target.value)} /> - } - placeholder={t('settings.network.wifi.password')} - onChange={(e) => setPassword(e.target.value)} - /> + {mode === 'psk' && ( + } + placeholder={ + passwordSet + ? t('settings.network.ethernet.passwordUnchanged') + : t('settings.network.wifi.password') + } + onChange={(e) => setPassword(e.target.value)} + /> + )} + {mode === 'enterprise' && ( + <> + } + placeholder={t('settings.network.wifi.identity')} + onChange={(e) => setIdentity(e.target.value)} + /> + {usesPassword(eap) && ( + } + placeholder={ + passwordSet + ? t('settings.network.ethernet.passwordUnchanged') + : t('settings.network.wifi.password') + } + onChange={(e) => setPassword(e.target.value)} + /> + )} + + )} + setAnonymousIdentity(e.target.value)} + /> + {eap === 'TLS' && ( + <> + setClientCert(e.target.value)} + /> + setPrivateKey(e.target.value)} + /> + setPrivateKeyPasswd(e.target.value)} + /> + + )} + setCaCert(e.target.value)} + /> + setDomainSuffixMatch(e.target.value)} + /> + + )} {!!message && {message}}
diff --git a/web/src/pages/wifi/index.tsx b/web/src/pages/wifi/index.tsx index f1319928..4cf5cab6 100644 --- a/web/src/pages/wifi/index.tsx +++ b/web/src/pages/wifi/index.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import { CheckOutlined, KeyOutlined, LockOutlined, WifiOutlined } from '@ant-design/icons'; -import { Button, Form, Input } from 'antd'; +import { CheckOutlined, KeyOutlined, LockOutlined, UserOutlined, WifiOutlined } from '@ant-design/icons'; +import { Button, Form, Input, Select } from 'antd'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; @@ -9,6 +9,40 @@ import { Head } from '@/components/head.tsx'; type State = '' | 'loading' | 'success' | 'failed' | 'denied'; type VerifyState = '' | 'failed' | 'denied'; +type EAPMethod = 'PEAP' | 'TTLS' | 'TLS' | 'PWD' | 'LEAP'; + +const getDefaultPhase2 = (eap: EAPMethod) => { + if (eap === 'PEAP' || eap === 'TTLS') return 'auth=MSCHAPV2'; + return ''; +}; + +const getEapOptions = (t: (key: string) => string) => [ + { value: 'PEAP', label: t('settings.network.wifi.eapPeap') }, + { value: 'TTLS', label: t('settings.network.wifi.eapTtls') }, + { value: 'TLS', label: t('settings.network.wifi.eapTls') }, + { value: 'PWD', label: t('settings.network.wifi.eapPwd') }, + { value: 'LEAP', label: t('settings.network.wifi.eapLeap') } +]; + +const getInnerAuthOptions = (eap: EAPMethod, t: (key: string) => string) => { + const peapInnerAuthOptions = [ + { value: 'auth=MSCHAPV2', label: t('settings.network.wifi.authMschapv2') }, + { value: 'auth=GTC', label: t('settings.network.wifi.authGtc') }, + { value: 'auth=MD5', label: t('settings.network.wifi.authMd5') } + ]; + const ttlsInnerAuthOptions = [ + { value: 'auth=MSCHAPV2', label: t('settings.network.wifi.authMschapv2') }, + { value: 'auth=MSCHAP', label: t('settings.network.wifi.authMschap') }, + { value: 'auth=CHAP', label: t('settings.network.wifi.authChap') }, + { value: 'auth=PAP', label: t('settings.network.wifi.authPap') } + ]; + if (eap === 'PEAP') return peapInnerAuthOptions; + if (eap === 'TTLS') return ttlsInnerAuthOptions; + return []; +}; + +const usesPassword = (eap: EAPMethod) => eap !== 'TLS'; +const usesInnerAuth = (eap: EAPMethod) => eap === 'PEAP' || eap === 'TTLS'; export const Wifi = () => { const { t } = useTranslation(); @@ -19,6 +53,9 @@ export const Wifi = () => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [verifying, setVerifying] = useState(false); const [verifyState, setVerifyState] = useState(''); + const [form] = Form.useForm(); + const mode = Form.useWatch('mode', form); + const eap = (Form.useWatch('eap', form) || 'PEAP') as EAPMethod; useEffect(() => { const pass = searchParams.get('p') || searchParams.get('P'); @@ -53,13 +90,34 @@ export const Wifi = () => { } async function connect(values: any) { - if (!values.ssid || !values.password) return; + if (!values.ssid) return; + if ((values.mode || 'psk') === 'psk' && !values.password) return; + if (values.mode === 'enterprise' && !values.identity) return; + if (values.mode === 'enterprise' && usesPassword(values.eap || 'PEAP') && !values.password) + return; + if ( + values.mode === 'enterprise' && + values.eap === 'TLS' && + (!values.clientCert || !values.privateKey) + ) + return; if (state === 'loading') return; setState('loading'); try { - const rsp = await api.connectWifiNoAuth(values.ssid, values.password, apPassword); + const rsp = await api.connectWifiNoAuth(values.ssid, values.password, apPassword, { + mode: values.mode || 'psk', + identity: values.identity, + eap: values.eap, + phase2: values.phase2, + anonymousIdentity: values.anonymousIdentity, + caCert: values.caCert, + clientCert: values.clientCert, + privateKey: values.privateKey, + privateKeyPasswd: values.privateKeyPasswd, + domainSuffixMatch: values.domainSuffixMatch + }); switch (rsp?.code) { case 0: @@ -129,8 +187,9 @@ export const Wifi = () => {
@@ -140,13 +199,94 @@ export const Wifi = () => { {t('wifi.description')}
+ + } placeholder="SSID" /> - - } placeholder="Password" /> - + {mode !== 'enterprise' && ( + + } + placeholder={t('settings.network.wifi.password')} + /> + + )} + + {mode === 'enterprise' && ( + <> + + } placeholder={t('settings.network.wifi.identity')} /> + + + {usesPassword(eap) && ( + + } + placeholder={t('settings.network.wifi.password')} + /> + + )} + + + + + )} + + + + + + + + + + {eap === 'TLS' && ( + <> + + + + + + + + + + + + + )} + + + + + + )} {state === 'success' ? (