Skip to content

Commit

Permalink
feat: big refactor, adding all missing endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
amalucelli committed Jun 20, 2022
1 parent 7c335b7 commit 8747c06
Show file tree
Hide file tree
Showing 17 changed files with 1,507 additions and 129 deletions.
117 changes: 117 additions & 0 deletions nextdns/allowlist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package nextdns

import (
"context"
"fmt"
"net/http"

"github.com/pkg/errors"
)

// allowlistAPIPath is the HTTP path for the allowlist API.
const allowlistAPIPath = "allowlist"

// Allowlist represents the allow list of a profile.
type Allowlist struct {
ID string `json:"id,omitempty"`
Active bool `json:"active"`
}

// CreateAllowlistRequest encapsulates the request for creating an allowlist.
type CreateAllowlistRequest struct {
Profile string
}

// GetAllowlistRequest encapsulates the request for getting an allowlist.
type GetAllowlistRequest struct {
Profile string
}

// UpdateAllowlistRequest encapsulates the request for updating an allowlist.
type UpdateAllowlistRequest struct {
Profile string
ID string
}

// AllowlistService is an interface for communicating with the NextDNS allowlist API endpoint.
type AllowlistService interface {
Create(context.Context, *CreateAllowlistRequest, []*Allowlist) error
Get(context.Context, *GetAllowlistRequest) ([]*Allowlist, error)
Update(context.Context, *UpdateAllowlistRequest, *Allowlist) error
}

// allowlistResponse represents the allowlist response.
type allowlistResponse struct {
Allowlist []*Allowlist `json:"data"`
}

// privacyService represents the NextDNS allowlist service.
type allowlistService struct {
client *Client
}

var _ AllowlistService = &allowlistService{}

// NewAllowlistService returns a new NextDNS allowlist service.
// nolint: revive
func NewAllowlistService(client *Client) *allowlistService {
return &allowlistService{
client: client,
}
}

// Create creates an allowlist for a profile.
func (s *allowlistService) Create(ctx context.Context, request *CreateAllowlistRequest, v []*Allowlist) error {
path := fmt.Sprintf("%s/%s", profileAPIPath(request.Profile), allowlistAPIPath)
req, err := s.client.newRequest(http.MethodPut, path, v)
if err != nil {
return errors.Wrap(err, "error creating request to create an allow list")
}

response := allowlistResponse{}
err = s.client.do(ctx, req, &response)
if err != nil {
return errors.Wrap(err, "error making a request to create an allow list")
}

return nil
}

// Get returns the allowlist of a profile.
func (s *allowlistService) Get(ctx context.Context, request *GetAllowlistRequest) ([]*Allowlist, error) {
path := fmt.Sprintf("%s/%s", profileAPIPath(request.Profile), allowlistAPIPath)
req, err := s.client.newRequest(http.MethodGet, path, nil)
if err != nil {
return nil, errors.Wrap(err, "error creating request to get the allow list")
}

response := allowlistResponse{}
err = s.client.do(ctx, req, &response)
if err != nil {
return nil, errors.Wrap(err, "error making a request to get the allow list")
}

return response.Allowlist, nil
}

// Update updates an allowlist of a profile.
func (s *allowlistService) Update(ctx context.Context, request *UpdateAllowlistRequest, v *Allowlist) error {
path := fmt.Sprintf("%s/%s", profileAPIPath(request.Profile), allowlistIDAPIPath(request.ID))
req, err := s.client.newRequest(http.MethodPatch, path, v)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error creating request to update the allow list id: %s", request.ID))
}

response := allowlistResponse{}
err = s.client.do(ctx, req, &response)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error making a request to update the allow list id: %s", request.ID))
}

return nil
}

// allowlistIDAPIPath returns the HTTP path for the allowlist API.
func allowlistIDAPIPath(id string) string {
return fmt.Sprintf("%s/%s", allowlistAPIPath, id)
}
78 changes: 75 additions & 3 deletions nextdns/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,54 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"

"github.com/hashicorp/go-cleanhttp"
)

const (
baseURL = "https://api.nextdns.io/"
contentType = "application/json"
userAgent = "nextdns-go"
)

// Client represents a NextDNS client.
type Client struct {
client *http.Client
baseURL *url.URL

// Service for the Profile.
Profiles ProfilesService

// Services for the Allowlist and Denylist.
Allowlist AllowlistService
Denylist DenylistService

// Services for the ParentalControl.
ParentalControlServices ParentalControlServicesService
ParentalControlCategories ParentalControlCategoriesService

// Services for the Privacy.
Privacy PrivacyService
PrivacyBlocklists PrivacyBlocklistsService
PrivacyNatives PrivacyNativesService

// Services for the Settings.
Settings SettingsService
SettingsLogs SettingsLogsService
SettingsBlockPage SettingsBlockPageService
SettingsPerformance SettingsPerformanceService

// Services for the Security.
Security SecurityService
SecurityTlds SecurityTldsService

// Debug mode for the HTTP requests.
Debug bool
}

// ClientOption is a function that can be used to customize the client.
Expand Down Expand Up @@ -58,6 +88,14 @@ func WithAPIKey(apiKey string) ClientOption {
}
}

// WithDebug enables debug mode.
func WithDebug() ClientOption {
return func(c *Client) error {
c.Debug = true
return nil
}
}

// WithHTTPClient sets a custom HTTP client that can be used for requests.
func WithHTTPClient(client *http.Client) ClientOption {
return func(c *Client) error {
Expand Down Expand Up @@ -89,14 +127,42 @@ func New(opts ...ClientOption) (*Client, error) {
}
}

c.Profiles = &profilesService{client: c}
// Initialize the services for the Profile.
c.Profiles = NewProfilesService(c)

// Initialize the services for the Allowlist and Denylist.
c.Allowlist = NewAllowlistService(c)
c.Denylist = NewDenylistService(c)

// Initialize the services for the ParentalControl.
c.ParentalControlServices = NewParentalControlServicesService(c)
c.ParentalControlCategories = NewParentalControlCategoriesService(c)

// Initialize the services for the Privacy.
c.Privacy = NewPrivacyService(c)
c.PrivacyBlocklists = NewPrivacyBlocklistsService(c)
c.PrivacyNatives = NewPrivacyNativesService(c)

// Initialize the services for the Settings.
c.Settings = NewSettingsService(c)
c.SettingsLogs = NewSettingsLogsService(c)
c.SettingsBlockPage = NewSettingsBlockPageService(c)
c.SettingsPerformance = NewSettingsPerformanceService(c)

// Initialize the services for the Security.
c.Security = NewSecurityService(c)
c.SecurityTlds = NewSecurityTldsService(c)

return c, nil
}

// do executes an HTTP request and decodes the response into v.
func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) error {
req = req.WithContext(ctx)

// Sets a custom user agent.
req.Header.Set("User-Agent", userAgent)

res, err := c.client.Do(req)
if err != nil {
return err
Expand Down Expand Up @@ -127,7 +193,8 @@ func (c *Client) handleResponse(ctx context.Context, res *http.Response, v inter
}

// If the response is not a 200, then we need to handle the error.
if res.StatusCode >= http.StatusBadRequest {
// TODO(amalucelli): Report the behavior to NextDNS, but there are errors that return HTTP 200 ("duplicate" case).
if res.StatusCode >= http.StatusBadRequest || strings.Contains(string(out), "\"errors\"") {
if res.StatusCode >= http.StatusInternalServerError {
return &Error{
Type: ErrorTypeServiceError,
Expand Down Expand Up @@ -210,6 +277,9 @@ func (c *Client) newRequest(method string, path string, body interface{}) (*http
var req *http.Request
switch method {
case http.MethodGet:
if c.Debug {
fmt.Printf("[DEBUG] REQUEST: Method:%s, URL:%s\n", method, u.String())
}
req, err = http.NewRequest(method, u.String(), nil)
if err != nil {
return nil, err
Expand All @@ -222,7 +292,9 @@ func (c *Client) newRequest(method string, path string, body interface{}) (*http
return nil, err
}
}

if c.Debug {
fmt.Printf("[DEBUG] REQUEST: Method:%s, URL:%s, Body:%v", method, u.String(), buf.String())
}
req, err = http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, err
Expand Down
117 changes: 117 additions & 0 deletions nextdns/denylist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package nextdns

import (
"context"
"fmt"
"net/http"

"github.com/pkg/errors"
)

// denylistAPIPath is the HTTP path for the denylist API.
const denylistAPIPath = "denylist"

// Denylist represents the denylist of a profile.
type Denylist struct {
ID string `json:"id,omitempty"`
Active bool `json:"active"`
}

// CreateDenylistRequest encapsulates the request for creating a denylist.
type CreateDenylistRequest struct {
Profile string
}

// GetDenylistRequest encapsulates the request for getting a denylist.
type GetDenylistRequest struct {
Profile string
}

// UpdateDenylistRequest encapsulates the request for updating a denylist.
type UpdateDenylistRequest struct {
Profile string
ID string
}

// DenylistService is an interface for communicating with the NextDNS denylist API endpoint.
type DenylistService interface {
Create(context.Context, *CreateDenylistRequest, []*Denylist) error
Get(context.Context, *GetDenylistRequest) ([]*Denylist, error)
Update(context.Context, *UpdateDenylistRequest, *Denylist) error
}

// denylistResponse represents the denylist response.
type denylistResponse struct {
Denylist []*Denylist `json:"data"`
}

// denylistService represents the NextDNS denylist service.
type denylistService struct {
client *Client
}

var _ DenylistService = &denylistService{}

// NewDenylistService returns a new NextDNS denylist service.
// nolint: revive
func NewDenylistService(client *Client) *denylistService {
return &denylistService{
client: client,
}
}

// Create creates a denylist for a profile.
func (s *denylistService) Create(ctx context.Context, request *CreateDenylistRequest, v []*Denylist) error {
path := fmt.Sprintf("%s/%s", profileAPIPath(request.Profile), denylistAPIPath)
req, err := s.client.newRequest(http.MethodPut, path, v)
if err != nil {
return errors.Wrap(err, "error creating request to create an deny list")
}

response := denylistResponse{}
err = s.client.do(ctx, req, &response)
if err != nil {
return errors.Wrap(err, "error making a request to create an deny list")
}

return nil
}

// Get returns the denylist of a profile.
func (s *denylistService) Get(ctx context.Context, request *GetDenylistRequest) ([]*Denylist, error) {
path := fmt.Sprintf("%s/%s", profileAPIPath(request.Profile), denylistAPIPath)
req, err := s.client.newRequest(http.MethodGet, path, nil)
if err != nil {
return nil, errors.Wrap(err, "error creating request to get the deny list")
}

response := denylistResponse{}
err = s.client.do(ctx, req, &response)
if err != nil {
return nil, errors.Wrap(err, "error making a request to get the deny list")
}

return response.Denylist, nil
}

// Update updates a denylist of a profile.
func (s *denylistService) Update(ctx context.Context, request *UpdateDenylistRequest, v *Denylist) error {
path := fmt.Sprintf("%s/%s", profileAPIPath(request.Profile), denylistIDAPIPath(request.ID))
req, err := s.client.newRequest(http.MethodPatch, path, v)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error creating request to update the deny list id: %s", request.ID))
}

response := denylistResponse{}
err = s.client.do(ctx, req, &response)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error making a request to update the deny list id: %s", request.ID))
}

return nil
}

// denylistIDAPIPath returns the HTTP path for the denylist API.
func denylistIDAPIPath(id string) string {
return fmt.Sprintf("%s/%s", denylistAPIPath, id)
}
1 change: 1 addition & 0 deletions nextdns/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Error struct {
}

// Error returns the string representation of the error.
// TODO(amalucelli): This is still not the best way to handle multiple errors, make this better at some point.
func (e *Error) Error() string {
var out strings.Builder

Expand Down
Loading

0 comments on commit 8747c06

Please sign in to comment.