mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-15 03:02:07 +03:00
* clients: multiple IP, CIDR, MAC addresses
+ /clients/find * clients: move code for config read/write * clients: move HTTP handlers
This commit is contained in:
parent
db703283ba
commit
71ce0c6da9
@ -21,6 +21,7 @@ Contents:
|
||||
* Add client
|
||||
* Update client
|
||||
* Delete client
|
||||
* API: Find clients by IP
|
||||
* Enable DHCP server
|
||||
* "Show DHCP status" command
|
||||
* "Check DHCP" command
|
||||
@ -618,8 +619,6 @@ Notes:
|
||||
|
||||
* `name`, `ip` and `mac` values are unique.
|
||||
|
||||
* `ip` & `mac` values can't be set both at the same time.
|
||||
|
||||
* If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table.
|
||||
|
||||
* If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings.
|
||||
@ -643,8 +642,7 @@ Response:
|
||||
clients: [
|
||||
{
|
||||
name: "client1"
|
||||
ip: "..."
|
||||
mac: "..."
|
||||
ids: ["...", ...] // IP, CIDR or MAC
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
@ -682,8 +680,7 @@ Request:
|
||||
|
||||
{
|
||||
name: "client1"
|
||||
ip: "..."
|
||||
mac: "..."
|
||||
ids: ["...", ...] // IP, CIDR or MAC
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
@ -712,8 +709,7 @@ Request:
|
||||
name: "client1"
|
||||
data: {
|
||||
name: "client1"
|
||||
ip: "..."
|
||||
mac: "..."
|
||||
ids: ["...", ...] // IP, CIDR or MAC
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
@ -752,6 +748,41 @@ Error response (Client not found):
|
||||
400
|
||||
|
||||
|
||||
### API: Find clients by IP
|
||||
|
||||
This method returns the list of clients (manual and auto-clients) matching the IP list.
|
||||
For auto-clients only `name`, `ids` and `whois_info` fields are set. Other fields are empty.
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/clients/find?ip0=...&ip1=...&ip2=...
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
[
|
||||
{
|
||||
"1.2.3.4": {
|
||||
name: "client1"
|
||||
ids: ["...", ...] // IP, CIDR or MAC
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
safesearch_enabled: false
|
||||
use_global_blocked_services: true
|
||||
blocked_services: [ "name1", ... ]
|
||||
whois_info: {
|
||||
key: "value"
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
## DNS access settings
|
||||
|
||||
There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.
|
||||
|
@ -684,6 +684,21 @@ func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
|
||||
func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
|
||||
now := time.Now().Unix()
|
||||
|
||||
s.leasesLock.RLock()
|
||||
defer s.leasesLock.RUnlock()
|
||||
|
||||
for _, l := range s.leases {
|
||||
if l.Expiry.Unix() > now && l.IP.Equal(ip) {
|
||||
return l.HWAddr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset internal state
|
||||
func (s *Server) reset() {
|
||||
s.leasesLock.Lock()
|
||||
|
447
home/clients.go
447
home/clients.go
@ -1,11 +1,10 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
@ -23,8 +22,7 @@ const (
|
||||
|
||||
// Client information
|
||||
type Client struct {
|
||||
IP string
|
||||
MAC string
|
||||
IDs []string
|
||||
Name string
|
||||
UseOwnSettings bool // false: use global settings
|
||||
FilteringEnabled bool
|
||||
@ -37,22 +35,6 @@ type Client struct {
|
||||
BlockedServices []string
|
||||
}
|
||||
|
||||
type clientJSON struct {
|
||||
IP string `json:"ip"`
|
||||
MAC string `json:"mac"`
|
||||
Name string `json:"name"`
|
||||
UseGlobalSettings bool `json:"use_global_settings"`
|
||||
FilteringEnabled bool `json:"filtering_enabled"`
|
||||
ParentalEnabled bool `json:"parental_enabled"`
|
||||
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
|
||||
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
|
||||
|
||||
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||
|
||||
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
||||
BlockedServices []string `json:"blocked_services"`
|
||||
}
|
||||
|
||||
type clientSource uint
|
||||
|
||||
// Client sources
|
||||
@ -74,24 +56,79 @@ type ClientHost struct {
|
||||
|
||||
type clientsContainer struct {
|
||||
list map[string]*Client // name -> client
|
||||
ipIndex map[string]*Client // IP -> client
|
||||
idIndex map[string]*Client // IP -> client
|
||||
ipHost map[string]*ClientHost // IP -> Hostname
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Init initializes clients container
|
||||
// Note: this function must be called only once
|
||||
func (clients *clientsContainer) Init() {
|
||||
func (clients *clientsContainer) Init(objects []clientObject) {
|
||||
if clients.list != nil {
|
||||
log.Fatal("clients.list != nil")
|
||||
}
|
||||
clients.list = make(map[string]*Client)
|
||||
clients.ipIndex = make(map[string]*Client)
|
||||
clients.idIndex = make(map[string]*Client)
|
||||
clients.ipHost = make(map[string]*ClientHost)
|
||||
clients.addFromConfig(objects)
|
||||
|
||||
go clients.periodicUpdate()
|
||||
}
|
||||
|
||||
type clientObject struct {
|
||||
Name string `yaml:"name"`
|
||||
IDs []string `yaml:"ids"`
|
||||
UseGlobalSettings bool `yaml:"use_global_settings"`
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"`
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
|
||||
|
||||
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
|
||||
BlockedServices []string `yaml:"blocked_services"`
|
||||
}
|
||||
|
||||
func (clients *clientsContainer) addFromConfig(objects []clientObject) {
|
||||
for _, cy := range objects {
|
||||
cli := Client{
|
||||
Name: cy.Name,
|
||||
IDs: cy.IDs,
|
||||
UseOwnSettings: !cy.UseGlobalSettings,
|
||||
FilteringEnabled: cy.FilteringEnabled,
|
||||
ParentalEnabled: cy.ParentalEnabled,
|
||||
SafeSearchEnabled: cy.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
|
||||
|
||||
UseOwnBlockedServices: !cy.UseGlobalBlockedServices,
|
||||
BlockedServices: cy.BlockedServices,
|
||||
}
|
||||
_, err := clients.Add(cli)
|
||||
if err != nil {
|
||||
log.Tracef("clientAdd: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteDiskConfig - write configuration
|
||||
func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) {
|
||||
clientsList := clients.GetList()
|
||||
for _, cli := range clientsList {
|
||||
cy := clientObject{
|
||||
Name: cli.Name,
|
||||
IDs: cli.IDs,
|
||||
UseGlobalSettings: !cli.UseOwnSettings,
|
||||
FilteringEnabled: cli.FilteringEnabled,
|
||||
ParentalEnabled: cli.ParentalEnabled,
|
||||
SafeSearchEnabled: cli.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
||||
|
||||
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
|
||||
BlockedServices: cli.BlockedServices,
|
||||
}
|
||||
*objects = append(*objects, cy)
|
||||
}
|
||||
}
|
||||
|
||||
func (clients *clientsContainer) periodicUpdate() {
|
||||
for {
|
||||
clients.addFromHostsFile()
|
||||
@ -111,7 +148,7 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
_, ok := clients.ipIndex[ip]
|
||||
_, ok := clients.idIndex[ip]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
@ -128,25 +165,42 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool {
|
||||
|
||||
// Find searches for a client by IP
|
||||
func (clients *clientsContainer) Find(ip string) (Client, bool) {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
if ipAddr == nil {
|
||||
return Client{}, false
|
||||
}
|
||||
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
c, ok := clients.ipIndex[ip]
|
||||
c, ok := clients.idIndex[ip]
|
||||
if ok {
|
||||
return *c, true
|
||||
}
|
||||
|
||||
for _, c = range clients.list {
|
||||
if len(c.MAC) != 0 {
|
||||
mac, err := net.ParseMAC(c.MAC)
|
||||
for _, id := range c.IDs {
|
||||
_, ipnet, err := net.ParseCIDR(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ipAddr := config.dhcpServer.FindIPbyMAC(mac)
|
||||
if ipAddr == nil {
|
||||
if ipnet.Contains(ipAddr) {
|
||||
return *c, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macFound := config.dhcpServer.FindMACbyIP(ipAddr)
|
||||
if macFound == nil {
|
||||
return Client{}, false
|
||||
}
|
||||
for _, c = range clients.list {
|
||||
for _, id := range c.IDs {
|
||||
hwAddr, err := net.ParseMAC(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if ip == ipAddr.String() {
|
||||
if bytes.Equal(hwAddr, macFound) {
|
||||
return *c, true
|
||||
}
|
||||
}
|
||||
@ -155,28 +209,51 @@ func (clients *clientsContainer) Find(ip string) (Client, bool) {
|
||||
return Client{}, false
|
||||
}
|
||||
|
||||
// FindAutoClient - search for an auto-client by IP
|
||||
func (clients *clientsContainer) FindAutoClient(ip string) (ClientHost, bool) {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
if ipAddr == nil {
|
||||
return ClientHost{}, false
|
||||
}
|
||||
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
ch, ok := clients.ipHost[ip]
|
||||
if ok {
|
||||
return *ch, true
|
||||
}
|
||||
return ClientHost{}, false
|
||||
}
|
||||
|
||||
// Check if Client object's fields are correct
|
||||
func (c *Client) check() error {
|
||||
if len(c.Name) == 0 {
|
||||
return fmt.Errorf("Invalid Name")
|
||||
}
|
||||
|
||||
if (len(c.IP) == 0 && len(c.MAC) == 0) ||
|
||||
(len(c.IP) != 0 && len(c.MAC) != 0) {
|
||||
return fmt.Errorf("IP or MAC required")
|
||||
if len(c.IDs) == 0 {
|
||||
return fmt.Errorf("ID required")
|
||||
}
|
||||
|
||||
if len(c.IP) != 0 {
|
||||
ip := net.ParseIP(c.IP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("Invalid IP")
|
||||
for i, id := range c.IDs {
|
||||
ip := net.ParseIP(id)
|
||||
if ip != nil {
|
||||
c.IDs[i] = ip.String() // normalize IP address
|
||||
continue
|
||||
}
|
||||
c.IP = ip.String()
|
||||
} else {
|
||||
_, err := net.ParseMAC(c.MAC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid MAC: %s", err)
|
||||
|
||||
_, _, err := net.ParseCIDR(id)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = net.ParseMAC(id)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("Invalid ID: %s", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -198,26 +275,34 @@ func (clients *clientsContainer) Add(c Client) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check IP index
|
||||
if len(c.IP) != 0 {
|
||||
c2, ok := clients.ipIndex[c.IP]
|
||||
// check ID index
|
||||
for _, id := range c.IDs {
|
||||
c2, ok := clients.idIndex[id]
|
||||
if ok {
|
||||
return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
||||
return false, fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name)
|
||||
}
|
||||
}
|
||||
|
||||
ch, ok := clients.ipHost[c.IP]
|
||||
// remove auto-clients with the same IP address, keeping WHOIS info if possible
|
||||
for _, id := range c.IDs {
|
||||
ch, ok := clients.ipHost[id]
|
||||
if ok {
|
||||
if len(c.WhoisInfo) == 0 {
|
||||
c.WhoisInfo = ch.WhoisInfo
|
||||
delete(clients.ipHost, c.IP)
|
||||
}
|
||||
delete(clients.ipHost, id)
|
||||
}
|
||||
}
|
||||
|
||||
// update Name index
|
||||
clients.list[c.Name] = &c
|
||||
if len(c.IP) != 0 {
|
||||
clients.ipIndex[c.IP] = &c
|
||||
|
||||
// update ID index
|
||||
for _, id := range c.IDs {
|
||||
clients.idIndex[id] = &c
|
||||
}
|
||||
|
||||
log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list))
|
||||
log.Tracef("'%s': ID:%v [%d]", c.Name, c.IDs, len(clients.list))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@ -231,8 +316,26 @@ func (clients *clientsContainer) Del(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// update Name index
|
||||
delete(clients.list, name)
|
||||
delete(clients.ipIndex, c.IP)
|
||||
|
||||
// update ID index
|
||||
for _, id := range c.IDs {
|
||||
delete(clients.idIndex, id)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Return TRUE if arrays are equal
|
||||
func arraysEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i != len(a); i++ {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -260,27 +363,30 @@ func (clients *clientsContainer) Update(name string, c Client) error {
|
||||
}
|
||||
|
||||
// check IP index
|
||||
if old.IP != c.IP && len(c.IP) != 0 {
|
||||
c2, ok := clients.ipIndex[c.IP]
|
||||
if ok {
|
||||
return fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
||||
if !arraysEqual(old.IDs, c.IDs) {
|
||||
for _, id := range c.IDs {
|
||||
c2, ok := clients.idIndex[id]
|
||||
if ok && c2 != old {
|
||||
return fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// update ID index
|
||||
for _, id := range old.IDs {
|
||||
delete(clients.idIndex, id)
|
||||
}
|
||||
for _, id := range c.IDs {
|
||||
clients.idIndex[id] = old
|
||||
}
|
||||
}
|
||||
|
||||
// update Name index
|
||||
if old.Name != c.Name {
|
||||
delete(clients.list, old.Name)
|
||||
}
|
||||
clients.list[c.Name] = &c
|
||||
|
||||
// update IP index
|
||||
if old.IP != c.IP {
|
||||
delete(clients.ipIndex, old.IP)
|
||||
}
|
||||
if len(c.IP) != 0 {
|
||||
clients.ipIndex[c.IP] = &c
|
||||
clients.list[c.Name] = old
|
||||
}
|
||||
|
||||
*old = c
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -289,7 +395,7 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
c, ok := clients.ipIndex[ip]
|
||||
c, ok := clients.idIndex[ip]
|
||||
if ok {
|
||||
c.WhoisInfo = info
|
||||
log.Debug("Clients: set WHOIS info for client %s: %v", c.Name, c.WhoisInfo)
|
||||
@ -319,7 +425,7 @@ func (clients *clientsContainer) AddHost(ip, host string, source clientSource) (
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
// check index
|
||||
_, ok := clients.ipIndex[ip]
|
||||
_, ok := clients.idIndex[ip]
|
||||
if ok {
|
||||
return false, nil
|
||||
}
|
||||
@ -440,210 +546,3 @@ func (clients *clientsContainer) addFromDHCP() {
|
||||
}
|
||||
log.Debug("Added %d client aliases from DHCP", n)
|
||||
}
|
||||
|
||||
type clientHostJSON struct {
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
|
||||
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||
}
|
||||
|
||||
type clientListJSON struct {
|
||||
Clients []clientJSON `json:"clients"`
|
||||
AutoClients []clientHostJSON `json:"auto_clients"`
|
||||
}
|
||||
|
||||
// respond with information about configured clients
|
||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
data := clientListJSON{}
|
||||
|
||||
config.clients.lock.Lock()
|
||||
for _, c := range config.clients.list {
|
||||
cj := clientJSON{
|
||||
IP: c.IP,
|
||||
MAC: c.MAC,
|
||||
Name: c.Name,
|
||||
UseGlobalSettings: !c.UseOwnSettings,
|
||||
FilteringEnabled: c.FilteringEnabled,
|
||||
ParentalEnabled: c.ParentalEnabled,
|
||||
SafeSearchEnabled: c.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
||||
|
||||
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
|
||||
BlockedServices: c.BlockedServices,
|
||||
}
|
||||
|
||||
if len(c.MAC) != 0 {
|
||||
hwAddr, _ := net.ParseMAC(c.MAC)
|
||||
ipAddr := config.dhcpServer.FindIPbyMAC(hwAddr)
|
||||
if ipAddr != nil {
|
||||
cj.IP = ipAddr.String()
|
||||
}
|
||||
}
|
||||
|
||||
cj.WhoisInfo = make(map[string]interface{})
|
||||
for _, wi := range c.WhoisInfo {
|
||||
cj.WhoisInfo[wi[0]] = wi[1]
|
||||
}
|
||||
|
||||
data.Clients = append(data.Clients, cj)
|
||||
}
|
||||
for ip, ch := range config.clients.ipHost {
|
||||
cj := clientHostJSON{
|
||||
IP: ip,
|
||||
Name: ch.Host,
|
||||
}
|
||||
|
||||
cj.Source = "etc/hosts"
|
||||
switch ch.Source {
|
||||
case ClientSourceDHCP:
|
||||
cj.Source = "DHCP"
|
||||
case ClientSourceRDNS:
|
||||
cj.Source = "rDNS"
|
||||
case ClientSourceARP:
|
||||
cj.Source = "ARP"
|
||||
case ClientSourceWHOIS:
|
||||
cj.Source = "WHOIS"
|
||||
}
|
||||
|
||||
cj.WhoisInfo = make(map[string]interface{})
|
||||
for _, wi := range ch.WhoisInfo {
|
||||
cj.WhoisInfo[wi[0]] = wi[1]
|
||||
}
|
||||
|
||||
data.AutoClients = append(data.AutoClients, cj)
|
||||
}
|
||||
config.clients.lock.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w).Encode(data)
|
||||
if e != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Convert JSON object to Client object
|
||||
func jsonToClient(cj clientJSON) (*Client, error) {
|
||||
c := Client{
|
||||
IP: cj.IP,
|
||||
MAC: cj.MAC,
|
||||
Name: cj.Name,
|
||||
UseOwnSettings: !cj.UseGlobalSettings,
|
||||
FilteringEnabled: cj.FilteringEnabled,
|
||||
ParentalEnabled: cj.ParentalEnabled,
|
||||
SafeSearchEnabled: cj.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||
|
||||
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
||||
BlockedServices: cj.BlockedServices,
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// Add a new client
|
||||
func handleAddClient(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cj := clientJSON{}
|
||||
err = json.Unmarshal(body, &cj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := jsonToClient(cj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
ok, err := config.clients.Add(*c)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
httpError(w, http.StatusBadRequest, "Client already exists")
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
// Remove client
|
||||
func handleDelClient(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cj := clientJSON{}
|
||||
err = json.Unmarshal(body, &cj)
|
||||
if err != nil || len(cj.Name) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.clients.Del(cj.Name) {
|
||||
httpError(w, http.StatusBadRequest, "Client not found")
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
type updateJSON struct {
|
||||
Name string `json:"name"`
|
||||
Data clientJSON `json:"data"`
|
||||
}
|
||||
|
||||
// Update client's properties
|
||||
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var dj updateJSON
|
||||
err = json.Unmarshal(body, &dj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
if len(dj.Name) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
c, err := jsonToClient(dj.Data)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = config.clients.Update(dj.Name, *c)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
// RegisterClientsHandlers registers HTTP handlers
|
||||
func RegisterClientsHandlers() {
|
||||
httpRegister(http.MethodGet, "/control/clients", handleGetClients)
|
||||
httpRegister(http.MethodPost, "/control/clients/add", handleAddClient)
|
||||
httpRegister(http.MethodPost, "/control/clients/delete", handleDelClient)
|
||||
httpRegister(http.MethodPost, "/control/clients/update", handleUpdateClient)
|
||||
}
|
||||
|
286
home/clients_http.go
Normal file
286
home/clients_http.go
Normal file
@ -0,0 +1,286 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type clientJSON struct {
|
||||
IDs []string `json:"ids"`
|
||||
Name string `json:"name"`
|
||||
UseGlobalSettings bool `json:"use_global_settings"`
|
||||
FilteringEnabled bool `json:"filtering_enabled"`
|
||||
ParentalEnabled bool `json:"parental_enabled"`
|
||||
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
|
||||
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
|
||||
|
||||
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||
|
||||
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
||||
BlockedServices []string `json:"blocked_services"`
|
||||
}
|
||||
|
||||
type clientHostJSON struct {
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
|
||||
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||
}
|
||||
|
||||
type clientListJSON struct {
|
||||
Clients []clientJSON `json:"clients"`
|
||||
AutoClients []clientHostJSON `json:"auto_clients"`
|
||||
}
|
||||
|
||||
// respond with information about configured clients
|
||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
data := clientListJSON{}
|
||||
|
||||
config.clients.lock.Lock()
|
||||
for _, c := range config.clients.list {
|
||||
cj := clientToJSON(c)
|
||||
data.Clients = append(data.Clients, cj)
|
||||
}
|
||||
for ip, ch := range config.clients.ipHost {
|
||||
cj := clientHostJSON{
|
||||
IP: ip,
|
||||
Name: ch.Host,
|
||||
}
|
||||
|
||||
cj.Source = "etc/hosts"
|
||||
switch ch.Source {
|
||||
case ClientSourceDHCP:
|
||||
cj.Source = "DHCP"
|
||||
case ClientSourceRDNS:
|
||||
cj.Source = "rDNS"
|
||||
case ClientSourceARP:
|
||||
cj.Source = "ARP"
|
||||
case ClientSourceWHOIS:
|
||||
cj.Source = "WHOIS"
|
||||
}
|
||||
|
||||
cj.WhoisInfo = make(map[string]interface{})
|
||||
for _, wi := range ch.WhoisInfo {
|
||||
cj.WhoisInfo[wi[0]] = wi[1]
|
||||
}
|
||||
|
||||
data.AutoClients = append(data.AutoClients, cj)
|
||||
}
|
||||
config.clients.lock.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w).Encode(data)
|
||||
if e != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Convert JSON object to Client object
|
||||
func jsonToClient(cj clientJSON) (*Client, error) {
|
||||
c := Client{
|
||||
Name: cj.Name,
|
||||
IDs: cj.IDs,
|
||||
UseOwnSettings: !cj.UseGlobalSettings,
|
||||
FilteringEnabled: cj.FilteringEnabled,
|
||||
ParentalEnabled: cj.ParentalEnabled,
|
||||
SafeSearchEnabled: cj.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||
|
||||
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
||||
BlockedServices: cj.BlockedServices,
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// Convert Client object to JSON
|
||||
func clientToJSON(c *Client) clientJSON {
|
||||
cj := clientJSON{
|
||||
Name: c.Name,
|
||||
IDs: c.IDs,
|
||||
UseGlobalSettings: !c.UseOwnSettings,
|
||||
FilteringEnabled: c.FilteringEnabled,
|
||||
ParentalEnabled: c.ParentalEnabled,
|
||||
SafeSearchEnabled: c.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
||||
|
||||
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
|
||||
BlockedServices: c.BlockedServices,
|
||||
}
|
||||
|
||||
cj.WhoisInfo = make(map[string]interface{})
|
||||
for _, wi := range c.WhoisInfo {
|
||||
cj.WhoisInfo[wi[0]] = wi[1]
|
||||
}
|
||||
return cj
|
||||
}
|
||||
|
||||
type clientHostJSONWithID struct {
|
||||
IDs []string `json:"ids"`
|
||||
Name string `json:"name"`
|
||||
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||
}
|
||||
|
||||
// Convert ClientHost object to JSON
|
||||
func clientHostToJSON(ip string, ch ClientHost) clientHostJSONWithID {
|
||||
cj := clientHostJSONWithID{
|
||||
Name: ch.Host,
|
||||
IDs: []string{ip},
|
||||
}
|
||||
|
||||
cj.WhoisInfo = make(map[string]interface{})
|
||||
for _, wi := range ch.WhoisInfo {
|
||||
cj.WhoisInfo[wi[0]] = wi[1]
|
||||
}
|
||||
return cj
|
||||
}
|
||||
|
||||
// Add a new client
|
||||
func handleAddClient(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cj := clientJSON{}
|
||||
err = json.Unmarshal(body, &cj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := jsonToClient(cj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
ok, err := config.clients.Add(*c)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
httpError(w, http.StatusBadRequest, "Client already exists")
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
// Remove client
|
||||
func handleDelClient(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cj := clientJSON{}
|
||||
err = json.Unmarshal(body, &cj)
|
||||
if err != nil || len(cj.Name) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.clients.Del(cj.Name) {
|
||||
httpError(w, http.StatusBadRequest, "Client not found")
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
type updateJSON struct {
|
||||
Name string `json:"name"`
|
||||
Data clientJSON `json:"data"`
|
||||
}
|
||||
|
||||
// Update client's properties
|
||||
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var dj updateJSON
|
||||
err = json.Unmarshal(body, &dj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
if len(dj.Name) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
c, err := jsonToClient(dj.Data)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = config.clients.Update(dj.Name, *c)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
// Get the list of clients by IP address list
|
||||
func handleFindClient(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
data := []map[string]interface{}{}
|
||||
for i := 0; ; i++ {
|
||||
ip := q.Get(fmt.Sprintf("ip%d", i))
|
||||
if len(ip) == 0 {
|
||||
break
|
||||
}
|
||||
el := map[string]interface{}{}
|
||||
c, ok := config.clients.Find(ip)
|
||||
if !ok {
|
||||
ch, ok := config.clients.FindAutoClient(ip)
|
||||
if !ok {
|
||||
continue // a client with this IP isn't found
|
||||
}
|
||||
cj := clientHostToJSON(ip, ch)
|
||||
el[ip] = cj
|
||||
|
||||
} else {
|
||||
cj := clientToJSON(&c)
|
||||
el[ip] = cj
|
||||
}
|
||||
|
||||
data = append(data, el)
|
||||
}
|
||||
|
||||
js, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(js)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't write response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterClientsHandlers registers HTTP handlers
|
||||
func RegisterClientsHandlers() {
|
||||
httpRegister("GET", "/control/clients", handleGetClients)
|
||||
httpRegister("POST", "/control/clients/add", handleAddClient)
|
||||
httpRegister("POST", "/control/clients/delete", handleDelClient)
|
||||
httpRegister("POST", "/control/clients/update", handleUpdateClient)
|
||||
httpRegister("GET", "/control/clients/find", handleFindClient)
|
||||
}
|
@ -12,11 +12,11 @@ func TestClients(t *testing.T) {
|
||||
var b bool
|
||||
clients := clientsContainer{}
|
||||
|
||||
clients.Init()
|
||||
clients.Init(nil)
|
||||
|
||||
// add
|
||||
c = Client{
|
||||
IP: "1.1.1.1",
|
||||
IDs: []string{"1.1.1.1", "aa:aa:aa:aa:aa:aa"},
|
||||
Name: "client1",
|
||||
}
|
||||
b, e = clients.Add(c)
|
||||
@ -26,7 +26,7 @@ func TestClients(t *testing.T) {
|
||||
|
||||
// add #2
|
||||
c = Client{
|
||||
IP: "2.2.2.2",
|
||||
IDs: []string{"2.2.2.2"},
|
||||
Name: "client2",
|
||||
}
|
||||
b, e = clients.Add(c)
|
||||
@ -46,7 +46,7 @@ func TestClients(t *testing.T) {
|
||||
|
||||
// failed add - name in use
|
||||
c = Client{
|
||||
IP: "1.2.3.5",
|
||||
IDs: []string{"1.2.3.5"},
|
||||
Name: "client1",
|
||||
}
|
||||
b, _ = clients.Add(c)
|
||||
@ -56,7 +56,7 @@ func TestClients(t *testing.T) {
|
||||
|
||||
// failed add - ip in use
|
||||
c = Client{
|
||||
IP: "2.2.2.2",
|
||||
IDs: []string{"2.2.2.2"},
|
||||
Name: "client3",
|
||||
}
|
||||
b, e = clients.Add(c)
|
||||
@ -70,35 +70,45 @@ func TestClients(t *testing.T) {
|
||||
assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile))
|
||||
|
||||
// failed update - no such name
|
||||
c.IP = "1.2.3.0"
|
||||
c.IDs = []string{"1.2.3.0"}
|
||||
c.Name = "client3"
|
||||
if clients.Update("client3", c) == nil {
|
||||
t.Fatalf("Update")
|
||||
}
|
||||
|
||||
// failed update - name in use
|
||||
c.IP = "1.2.3.0"
|
||||
c.IDs = []string{"1.2.3.0"}
|
||||
c.Name = "client2"
|
||||
if clients.Update("client1", c) == nil {
|
||||
t.Fatalf("Update - name in use")
|
||||
}
|
||||
|
||||
// failed update - ip in use
|
||||
c.IP = "2.2.2.2"
|
||||
c.IDs = []string{"2.2.2.2"}
|
||||
c.Name = "client1"
|
||||
if clients.Update("client1", c) == nil {
|
||||
t.Fatalf("Update - ip in use")
|
||||
}
|
||||
|
||||
// update
|
||||
c.IP = "1.1.1.2"
|
||||
c.IDs = []string{"1.1.1.2"}
|
||||
c.Name = "client1"
|
||||
if clients.Update("client1", c) != nil {
|
||||
t.Fatalf("Update")
|
||||
}
|
||||
|
||||
// get after update
|
||||
assert.True(t, !(clients.Exists("1.1.1.1", ClientSourceHostsFile) || !clients.Exists("1.1.1.2", ClientSourceHostsFile)))
|
||||
assert.True(t, !clients.Exists("1.1.1.1", ClientSourceHostsFile))
|
||||
assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile))
|
||||
|
||||
// update - rename
|
||||
c.IDs = []string{"1.1.1.2"}
|
||||
c.Name = "client1-renamed"
|
||||
c.UseOwnSettings = true
|
||||
assert.True(t, clients.Update("client1", c) == nil)
|
||||
c = Client{}
|
||||
c, b = clients.Find("1.1.1.2")
|
||||
assert.True(t, b && c.Name == "client1-renamed" && c.IDs[0] == "1.1.1.2" && c.UseOwnSettings)
|
||||
|
||||
// failed remove - no such name
|
||||
if clients.Del("client3") {
|
||||
@ -106,7 +116,7 @@ func TestClients(t *testing.T) {
|
||||
}
|
||||
|
||||
// remove
|
||||
assert.True(t, !(!clients.Del("client1") || clients.Exists("1.1.1.2", ClientSourceHostsFile)))
|
||||
assert.True(t, !(!clients.Del("client1-renamed") || clients.Exists("1.1.1.2", ClientSourceHostsFile)))
|
||||
|
||||
// add host client
|
||||
b, e = clients.AddHost("1.1.1.1", "host", ClientSourceARP)
|
||||
@ -139,7 +149,7 @@ func TestClients(t *testing.T) {
|
||||
func TestClientsWhois(t *testing.T) {
|
||||
var c Client
|
||||
clients := clientsContainer{}
|
||||
clients.Init()
|
||||
clients.Init(nil)
|
||||
|
||||
whois := [][]string{{"orgname", "orgname-val"}, {"country", "country-val"}}
|
||||
// set whois info on new client
|
||||
@ -153,11 +163,11 @@ func TestClientsWhois(t *testing.T) {
|
||||
|
||||
// set whois info on existing client
|
||||
c = Client{
|
||||
IP: "1.1.1.2",
|
||||
IDs: []string{"1.1.1.2"},
|
||||
Name: "client1",
|
||||
}
|
||||
_, _ = clients.Add(c)
|
||||
clients.SetWhoisInfo("1.1.1.2", whois)
|
||||
assert.True(t, clients.ipIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val")
|
||||
assert.True(t, clients.idIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val")
|
||||
_ = clients.Del("client1")
|
||||
}
|
||||
|
@ -30,20 +30,6 @@ type logSettings struct {
|
||||
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
|
||||
}
|
||||
|
||||
type clientObject struct {
|
||||
Name string `yaml:"name"`
|
||||
IP string `yaml:"ip"`
|
||||
MAC string `yaml:"mac"`
|
||||
UseGlobalSettings bool `yaml:"use_global_settings"`
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"`
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
|
||||
|
||||
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
|
||||
BlockedServices []string `yaml:"blocked_services"`
|
||||
}
|
||||
|
||||
type HTTPSServer struct {
|
||||
server *http.Server
|
||||
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
|
||||
@ -285,27 +271,6 @@ func parseConfig() error {
|
||||
config.DNS.FiltersUpdateIntervalHours = 24
|
||||
}
|
||||
|
||||
for _, cy := range config.Clients {
|
||||
cli := Client{
|
||||
Name: cy.Name,
|
||||
IP: cy.IP,
|
||||
MAC: cy.MAC,
|
||||
UseOwnSettings: !cy.UseGlobalSettings,
|
||||
FilteringEnabled: cy.FilteringEnabled,
|
||||
ParentalEnabled: cy.ParentalEnabled,
|
||||
SafeSearchEnabled: cy.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
|
||||
|
||||
UseOwnBlockedServices: !cy.UseGlobalBlockedServices,
|
||||
BlockedServices: cy.BlockedServices,
|
||||
}
|
||||
_, err = config.clients.Add(cli)
|
||||
if err != nil {
|
||||
log.Tracef("clientAdd: %s", err)
|
||||
}
|
||||
}
|
||||
config.Clients = nil
|
||||
|
||||
status := tlsConfigStatus{}
|
||||
if !tlsLoadConfig(&config.TLS, &status) {
|
||||
log.Error("%s", status.WarningValidation)
|
||||
@ -335,27 +300,7 @@ func (c *configuration) write() error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
clientsList := config.clients.GetList()
|
||||
for _, cli := range clientsList {
|
||||
ip := cli.IP
|
||||
if len(cli.MAC) != 0 {
|
||||
ip = ""
|
||||
}
|
||||
cy := clientObject{
|
||||
Name: cli.Name,
|
||||
IP: ip,
|
||||
MAC: cli.MAC,
|
||||
UseGlobalSettings: !cli.UseOwnSettings,
|
||||
FilteringEnabled: cli.FilteringEnabled,
|
||||
ParentalEnabled: cli.ParentalEnabled,
|
||||
SafeSearchEnabled: cli.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
||||
|
||||
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
|
||||
BlockedServices: cli.BlockedServices,
|
||||
}
|
||||
config.Clients = append(config.Clients, cy)
|
||||
}
|
||||
config.clients.WriteDiskConfig(&config.Clients)
|
||||
|
||||
if config.auth != nil {
|
||||
config.Users = config.auth.GetUsers()
|
||||
|
@ -98,7 +98,6 @@ func run(args options) {
|
||||
}()
|
||||
|
||||
initConfig()
|
||||
config.clients.Init()
|
||||
initServices()
|
||||
|
||||
if !config.firstRun {
|
||||
@ -119,6 +118,9 @@ func run(args options) {
|
||||
}
|
||||
}
|
||||
|
||||
config.clients.Init(config.Clients)
|
||||
config.Clients = nil
|
||||
|
||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||
config.RlimitNoFile != 0 {
|
||||
setRlimit(config.RlimitNoFile)
|
||||
|
Loading…
Reference in New Issue
Block a user