Skip to content

Commit

Permalink
Merge pull request #41 from elacy/main
Browse files Browse the repository at this point in the history
Firewall Rule includes more properties and create returns tracker id
  • Loading branch information
sjafferali authored Oct 22, 2024
2 parents f7a111c + ac07898 commit 7a3679f
Show file tree
Hide file tree
Showing 19 changed files with 1,210 additions and 162 deletions.
19 changes: 18 additions & 1 deletion pfsenseapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"encoding/json"
"errors"
"fmt"
"golang.org/x/exp/slices"
"io"
"net/http"
"sync"
"time"

"golang.org/x/exp/slices"
)

var (
Expand All @@ -33,10 +35,12 @@ var (
type Client struct {
client *http.Client
Cfg Config
lock sync.Mutex

System *SystemService
Token *TokenService
DHCP *DHCPService
Unbound *UnboundService
Status *StatusService
Interface *InterfaceService
Routing *RoutingService
Expand Down Expand Up @@ -94,6 +98,7 @@ func NewClient(config Config) *Client {
newClient.Routing = &RoutingService{client: newClient}
newClient.Firewall = &FirewallService{client: newClient}
newClient.User = &UserService{client: newClient}
newClient.Unbound = &UnboundService{client: newClient}
return newClient
}

Expand Down Expand Up @@ -286,6 +291,10 @@ func (c *Client) get(ctx context.Context, endpoint string, queryMap map[string]s
}

func (c *Client) post(ctx context.Context, endpoint string, queryMap map[string]string, body []byte) ([]byte, error) {
// PFSense API cannot handle concurrent write requests
c.lock.Lock()
defer c.lock.Unlock()

res, err := c.do(ctx, http.MethodPost, endpoint, queryMap, body)
if err != nil {
return nil, err
Expand All @@ -312,6 +321,10 @@ func (c *Client) post(ctx context.Context, endpoint string, queryMap map[string]
}

func (c *Client) put(ctx context.Context, endpoint string, queryMap map[string]string, body []byte) ([]byte, error) {
// PFSense API cannot handle concurrent write requests
c.lock.Lock()
defer c.lock.Unlock()

res, err := c.do(ctx, http.MethodPut, endpoint, queryMap, body)
if err != nil {
return nil, err
Expand All @@ -338,6 +351,10 @@ func (c *Client) put(ctx context.Context, endpoint string, queryMap map[string]s
}

func (c *Client) delete(ctx context.Context, endpoint string, queryMap map[string]string) ([]byte, error) {
// PFSense API cannot handle concurrent write requests
c.lock.Lock()
defer c.lock.Unlock()

res, err := c.do(ctx, http.MethodDelete, endpoint, queryMap, nil)
if err != nil {
return nil, err
Expand Down
191 changes: 155 additions & 36 deletions pfsenseapi/dhcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package pfsenseapi
import (
"context"
"encoding/json"
"fmt"
"strconv"
)

const (
leasesEndpoint = "api/v1/services/dhcpd/lease"
staticMappingEndpoint = "api/v1/services/dhcpd/static_mapping"
serverEndpoint = "api/v1/services/dhcpd"
)

// DHCPService provides DHCP API methods
Expand Down Expand Up @@ -41,33 +43,35 @@ type dhcpStaticMappingResponse struct {

// DHCPStaticMapping represents a single DHCP static reservation
type DHCPStaticMapping struct {
ID int `json:"id"`
Mac string `json:"mac"`
Cid string `json:"cid"`
IPaddr string `json:"ipaddr"`
Hostname string `json:"hostname"`
Descr string `json:"descr"`
Filename string `json:"filename"`
Rootpath string `json:"rootpath"`
DefaultLeaseTime string `json:"defaultleasetime"`
MaxLeaseTime string `json:"maxleasetime"`
Gateway string `json:"gateway"`
Domain string `json:"domain"`
DomainSearchList string `json:"domainsearchlist"`
DDNSDomain string `json:"ddnsdomain"`
DDNSDomainPrimary string `json:"ddnsdomainprimary"`
DDNSDomainSecondary string `json:"ddnsdomainsecondary"`
DDNSDomainkeyName string `json:"ddnsdomainkeyname"`
DDNSDomainkeyAlgorithm string `json:"ddnsdomainkeyalgorithm"`
DDNSDomainkey string `json:"ddnsdomainkey"`
TFTP string `json:"tftp"`
LDAP string `json:"ldap"`
NextServer string `json:"nextserver"`
Filename32 string `json:"filename32"`
Filename64 string `json:"filename64"`
Filename32Arm string `json:"filename32arm"`
Filename64Arm string `json:"filename64arm"`
NumberOptions string `json:"numberoptions"`
ID int `json:"id"`
Mac string `json:"mac"`
Cid string `json:"cid"`
IPaddr string `json:"ipaddr"`
Hostname string `json:"hostname"`
Descr string `json:"descr"`
Filename string `json:"filename"`
Rootpath string `json:"rootpath"`
DefaultLeaseTime string `json:"defaultleasetime"`
MaxLeaseTime string `json:"maxleasetime"`
Gateway string `json:"gateway"`
Domain string `json:"domain"`
DomainSearchList string `json:"domainsearchlist"`
DDNSDomain string `json:"ddnsdomain"`
DDNSDomainPrimary string `json:"ddnsdomainprimary"`
DDNSDomainSecondary string `json:"ddnsdomainsecondary"`
DDNSDomainkeyName string `json:"ddnsdomainkeyname"`
DDNSDomainkeyAlgorithm string `json:"ddnsdomainkeyalgorithm"`
DDNSDomainkey string `json:"ddnsdomainkey"`
DNSServers []string `json:"dnsserver"`
TFTP string `json:"tftp"`
LDAP string `json:"ldap"`
NextServer string `json:"nextserver"`
Filename32 string `json:"filename32"`
Filename64 string `json:"filename64"`
Filename32Arm string `json:"filename32arm"`
Filename64Arm string `json:"filename64arm"`
NumberOptions string `json:"numberoptions"`
ArpTableStaticEntry TrueIfPresent `json:"arp_table_static_entry"`
}

// DHCPStaticMappingRequest represents a single DHCP static reservation. This
Expand Down Expand Up @@ -151,15 +155,37 @@ type dhcpStaticMappingRequestUpdate struct {
Id int `json:"id"`
}

func (s DHCPService) getStaticMappingObjectId(ctx context.Context, mappingInterface string, macAddress string) (int, error) {
mappings, err := s.ListStaticMappings(ctx, mappingInterface)

if err != nil {
return 0, err
}

for i, mapping := range mappings {
if mapping.Mac == macAddress {
return i, nil
}
}

return 0, fmt.Errorf("Unable to find static mapping on interface %s with mac %s", mappingInterface, macAddress)
}

// UpdateStaticMapping modifies a DHCP static mapping.
func (s DHCPService) UpdateStaticMapping(
ctx context.Context,
idToUpdate int,
macAddress string,
mappingData DHCPStaticMappingRequest,
) (*DHCPStaticMapping, error) {
id, err := s.getStaticMappingObjectId(ctx, mappingData.Interface, macAddress)

if err != nil {
return nil, err
}

requestData := dhcpStaticMappingRequestUpdate{
DHCPStaticMappingRequest: mappingData,
Id: idToUpdate,
Id: id,
}

jsonData, err := json.Marshal(requestData)
Expand All @@ -179,21 +205,114 @@ func (s DHCPService) UpdateStaticMapping(
}

// DeleteStaticMapping deletes a DHCP static mapping.
func (s DHCPService) DeleteStaticMapping(
ctx context.Context,
mappingInterface string,
idToDelete int,
) error {
_, err := s.client.delete(
func (s DHCPService) DeleteStaticMapping(ctx context.Context, mappingInterface string, macAddress string) error {
id, err := s.getStaticMappingObjectId(ctx, mappingInterface, macAddress)

if err != nil {
return err
}

_, err = s.client.delete(
ctx,
staticMappingEndpoint,
map[string]string{
"interface": mappingInterface,
"id": strconv.Itoa(idToDelete),
"id": strconv.Itoa(id),
},
)
if err != nil {
return err
}
return nil
}

// DHCPServerConfigurationRequest updates the current DHCP Server (dhcpd) configuration for a specified interface
type DHCPServerConfigurationRequest struct {
DefaultLeaseTime *int `json:"defaultleasetime"`
DenyUnknown bool `json:"denyunknown"`
DNSServer []string `json:"dnsserver,omitempty"`
Domain string `json:"domain,omitempty"`
DomainSearchList []string `json:"domainsearchlist,omitempty"`
Enable bool `json:"enable"`
Gateway string `json:"gateway,omitempty"`
IgnoreBootP bool `json:"ignorebootp,omitempty"`
Interface string `json:"interface"`
MacAllow []string `json:"mac_allow,omitempty"`
MacDeny []string `json:"mac_deny,omitempty"`
MaxLeaseTime *int `json:"maxleasetime,omitempty"`
NumberOptions []interface{} `json:"numberoptions,omitempty"`
RangeFrom string `json:"range_from,omitempty"`
RangeTo string `json:"range_to,omitempty"`
StaticARP bool `json:"staticarp"`
}

type DHCPRange struct {
From string `json:"from"`
To string `json:"to"`
}

// DHCPServerConfiguration describes the current DHCP Server (dhcpd) configuration for a specified interface
type DHCPServerConfiguration struct {
DefaultLeaseTime OptionalJSONInt `json:"defaultleasetime"`
DenyUnknown TrueIfPresent `json:"denyunknown"`
DNSServer []string `json:"dnsserver"`
Domain string `json:"domain"`
DomainSearchList string `json:"domainsearchlist"`
Enable TrueIfPresent `json:"enable"`
Gateway string `json:"gateway"`
IgnoreBootP bool `json:"ignorebootp"`
Interface string `json:"interface"`
MacAllow string `json:"mac_allow"`
MacDeny string `json:"mac_deny"`
MaxLeaseTime OptionalJSONInt `json:"maxleasetime"`
NumberOptions string `json:"numberoptions"`
Range *DHCPRange `json:"range"`
StaticARP TrueIfPresent `json:"staticarp"`
}

type dhcpServerResponse struct {
apiResponse
Data []*DHCPServerConfiguration `json:"data"`
}

// ListServerConfigurations lists all DHCP server configurations
func (s DHCPService) ListServerConfigurations(ctx context.Context) ([]*DHCPServerConfiguration, error) {
response, err := s.client.get(ctx, serverEndpoint, nil)
if err != nil {
return nil, err
}

resp := new(dhcpServerResponse)
if err = json.Unmarshal(response, resp); err != nil {
return nil, err
}
return resp.Data, nil
}

type dhcpServerUpdateResponse struct {
apiResponse
Data *DHCPServerConfiguration `json:"data"`
}

// UpdateServerConfiguration modifies a DHCP server configuration.
func (s DHCPService) UpdateServerConfiguration(
ctx context.Context,
dhcpConfigData DHCPServerConfigurationRequest,
) (*DHCPServerConfiguration, error) {
jsonData, err := json.Marshal(dhcpConfigData)
if err != nil {
return nil, err
}
response, err := s.client.put(ctx, serverEndpoint, nil, jsonData)
if err != nil {
return nil, err
}

resp := new(dhcpServerUpdateResponse)
if err = json.Unmarshal(response, resp); err != nil {
return nil, err
}

resp.Data.Interface = dhcpConfigData.Interface
return resp.Data, nil
}
77 changes: 76 additions & 1 deletion pfsenseapi/dhcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,80 @@ func TestDHCPService_ListStaticMappings(t *testing.T) {
newClient := NewClientWithNoAuth(server.URL)
response, err := newClient.DHCP.ListStaticMappings(context.Background(), testInterface)
require.NoError(t, err)
require.Len(t, response, 1)
require.Len(t, response, 4)
}

func TestDHCPService_DeleteStaticMappings(t *testing.T) {
listResponse := mustReadFileString(t, "testdata/liststaticmappings.json")
deleteResponse := mustReadFileString(t, "testdata/deletestaticmapping.json")

testInterface := "IOT"
mappingId := "3"
mappingMac := "00:1d:93:aa:4c"

handler := func(w http.ResponseWriter, r *http.Request) {

query, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = fmt.Fprintf(w, "invalid request")
return
}

interfaceValue := query.Get("interface")

if interfaceValue != testInterface {
w.WriteHeader(http.StatusBadRequest)
_, _ = fmt.Fprintf(w, "invalid request")
return
}

if r.Method == http.MethodDelete {
id := query.Get("id")

if id != mappingId {
w.WriteHeader(http.StatusBadRequest)
_, _ = fmt.Fprintf(w, "invalid request")
} else {
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, deleteResponse)
}
} else {
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, listResponse)
}
}

server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()

newClient := NewClientWithNoAuth(server.URL)
err := newClient.DHCP.DeleteStaticMapping(context.Background(), testInterface, mappingMac)
require.NoError(t, err)
}

func TestDHCPService_UpdateDHCPConfiguration(t *testing.T) {
data := makeResultList(t, mustReadFileString(t, "testdata/dhcpconfiguration.json"))

handler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(data.popStatus())
_, _ = io.WriteString(w, data.popResult())
}

server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()

newClient := NewClientWithNoAuth(server.URL)
response, err := newClient.DHCP.UpdateServerConfiguration(context.Background(), DHCPServerConfigurationRequest{})
require.NotNil(t, response)
require.NoError(t, err)

response, err = newClient.DHCP.UpdateServerConfiguration(context.Background(), DHCPServerConfigurationRequest{})
require.Nil(t, response)
require.Error(t, err)

response, err = newClient.DHCP.UpdateServerConfiguration(context.Background(), DHCPServerConfigurationRequest{})
require.Nil(t, response)
require.Error(t, err)
}
Loading

0 comments on commit 7a3679f

Please sign in to comment.