Implement plesk rest api
This commit is contained in:
297
provider.go
297
provider.go
@ -1,93 +1,296 @@
|
|||||||
package plesk
|
package plesk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/libdns/libdns"
|
"github.com/libdns/libdns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider implements the libdns interfaces for Plesk.
|
// Provider implements the libdns interfaces for Plesk.
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
// BaseURL is the root of your Plesk API, e.g. "https://my-plesk-server:8443/api/v2"
|
// BaseURL is your Plesk REST API endpoint, like:
|
||||||
|
// "https://my-plesk-server:8443/api/v2"
|
||||||
BaseURL string `json:"base_url,omitempty"`
|
BaseURL string `json:"base_url,omitempty"`
|
||||||
|
|
||||||
// Authentication: could be a secret token or user/pass
|
// SecretToken is your Plesk API token (or pass user/pass if needed).
|
||||||
// For example, if using "API secret keys" in Plesk:
|
// Example: "abcdefghijkl123456789"
|
||||||
SecretToken string `json:"secret_token,omitempty"`
|
SecretToken string `json:"secret_token,omitempty"`
|
||||||
|
|
||||||
// If you need additional fields or custom config, include them here
|
// HTTPClient allows custom HTTP client overrides (timeouts, etc.)
|
||||||
|
// If nil, we’ll use http.DefaultClient.
|
||||||
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// These ensure Provider satisfies the libdns interfaces you need.
|
// Ensure Provider implements the libdns interfaces you need.
|
||||||
|
// (You can also implement RecordSetter if needed, but typically
|
||||||
|
// for ACME challenges you only need Getter/Appender/Deleter.)
|
||||||
var (
|
var (
|
||||||
_ libdns.RecordGetter = &Provider{}
|
_ libdns.RecordGetter = &Provider{}
|
||||||
_ libdns.RecordAppender = &Provider{}
|
_ libdns.RecordAppender = &Provider{}
|
||||||
_ libdns.RecordDeleter = &Provider{}
|
_ libdns.RecordDeleter = &Provider{}
|
||||||
// If you need updating, also do: _ libdns.RecordSetter = &Provider{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// internal helper: make a request to Plesk
|
// pleskZone represents a single DNS zone (partial struct).
|
||||||
func (p *Provider) doRequest(ctx context.Context, method, path string, body []byte) ([]byte, error) {
|
type pleskZone struct {
|
||||||
url := strings.TrimSuffix(p.BaseURL, "/") + "/" + strings.TrimPrefix(path, "/")
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
req, err := http.NewRequestWithContext(ctx, method, url, nil)
|
Type string `json:"type"`
|
||||||
if err != nil {
|
Status string `json:"status"`
|
||||||
return nil, err
|
Modified string `json:"modified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a body (for POST/PUT requests), attach it
|
// pleskRecord represents a DNS record object in Plesk (partial struct).
|
||||||
|
type pleskRecord struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"` // e.g. "TXT"
|
||||||
|
Host string `json:"host,omitempty"` // e.g. "_acme-challenge"
|
||||||
|
Value string `json:"value,omitempty"` // The content, e.g. the ACME keyAuth
|
||||||
|
// You can add more fields if needed, such as "ttl" or "opt"
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequest is a helper that handles HTTP requests to the Plesk REST API.
|
||||||
|
func (p *Provider) doRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
|
||||||
|
if p.HTTPClient == nil {
|
||||||
|
p.HTTPClient = http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
fullURL := strings.TrimSuffix(p.BaseURL, "/") + "/" + strings.TrimPrefix(path, "/")
|
||||||
|
|
||||||
|
var reqBody []byte
|
||||||
|
var err error
|
||||||
if body != nil {
|
if body != nil {
|
||||||
req.Body = http.NoBody // replace with actual body if needed
|
reqBody, err = json.Marshal(body)
|
||||||
// This is just a placeholder
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("plesk: failed to marshal request body: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization: Bearer <token>
|
req, err := http.NewRequestWithContext(ctx, method, fullURL, bytes.NewReader(reqBody))
|
||||||
req.Header.Set("Authorization", "Bearer "+p.SecretToken)
|
|
||||||
// Possibly set content-type to JSON or XML depending on your Plesk API
|
|
||||||
// req.Header.Set("Content-Type", "application/json") // or "application/xml"
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("plesk: failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example auth using Bearer token. Adapt if you need Basic Auth, etc.
|
||||||
|
if p.SecretToken != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+p.SecretToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if method == http.MethodPost || method == http.MethodPatch || method == http.MethodPut {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := p.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("plesk: HTTP request error: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// read resp.Body
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
// ...
|
if err != nil {
|
||||||
// if resp.StatusCode != 200, handle error
|
return nil, fmt.Errorf("plesk: failed to read response body: %w", err)
|
||||||
// parse JSON or XML accordingly
|
|
||||||
|
|
||||||
// placeholder
|
|
||||||
return nil, errors.New("doRequest not fully implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRecords lists DNS records for the given zone in Plesk
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||||
|
return nil, fmt.Errorf("plesk: API returned status %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findZoneID attempts to locate the zone by domain name in Plesk.
|
||||||
|
// Returns the integer zone ID if found, or an error otherwise.
|
||||||
|
func (p *Provider) findZoneID(ctx context.Context, zone string) (int, error) {
|
||||||
|
// Plesk might store domain "example.com" as "example.com." or vice versa.
|
||||||
|
// Typically, we might need to remove trailing dots, or handle them carefully.
|
||||||
|
domain := strings.TrimSuffix(zone, ".")
|
||||||
|
|
||||||
|
// Example endpoint: GET /dns-zones?search=<domain>
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("search", domain)
|
||||||
|
path := fmt.Sprintf("dns-zones?%s", query.Encode())
|
||||||
|
|
||||||
|
respBody, err := p.doRequest(ctx, http.MethodGet, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("plesk: error searching zone: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zones []pleskZone
|
||||||
|
if err := json.Unmarshal(respBody, &zones); err != nil {
|
||||||
|
return 0, fmt.Errorf("plesk: error unmarshaling zone list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, z := range zones {
|
||||||
|
// If the zone name matches exactly, or if it matches after trimming.
|
||||||
|
if strings.EqualFold(strings.TrimSuffix(z.Name, "."), domain) {
|
||||||
|
return z.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("plesk: zone for domain '%s' not found", zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecords lists DNS records from Plesk in the given zone.
|
||||||
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
|
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
|
||||||
// Typically you'd do something like:
|
zoneID, err := p.findZoneID(ctx, zone)
|
||||||
// GET /dns/<zone> or /zones/<zone>/records
|
if err != nil {
|
||||||
// parse JSON/XML
|
return nil, err
|
||||||
// transform into []libdns.Record
|
}
|
||||||
// ...
|
|
||||||
return []libdns.Record{}, nil
|
// GET /dns-zones/<zoneID>/records
|
||||||
|
path := fmt.Sprintf("dns-zones/%d/records", zoneID)
|
||||||
|
respBody, err := p.doRequest(ctx, http.MethodGet, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var presRecords []pleskRecord
|
||||||
|
if err := json.Unmarshal(respBody, &presRecords); err != nil {
|
||||||
|
return nil, fmt.Errorf("plesk: error unmarshaling DNS records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from pleskRecord to libdns.Record
|
||||||
|
var results []libdns.Record
|
||||||
|
for _, r := range presRecords {
|
||||||
|
rec := libdns.Record{
|
||||||
|
ID: fmt.Sprintf("%d", r.ID),
|
||||||
|
Type: r.Type,
|
||||||
|
Name: r.Host,
|
||||||
|
Value: r.Value,
|
||||||
|
// If Plesk returns TTL or similar, map it here.
|
||||||
|
// TTL: time.Duration(r.TTL) * time.Second,
|
||||||
|
}
|
||||||
|
results = append(results, rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendRecords creates the given records in the specified DNS zone.
|
// AppendRecords creates the given records in the specified DNS zone.
|
||||||
func (p *Provider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
|
func (p *Provider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
|
||||||
// For each record, make a request to Plesk to create the DNS entry
|
zoneID, err := p.findZoneID(ctx, zone)
|
||||||
// e.g. POST /dns/<zone> with JSON/XML specifying a record type, name, content
|
if err != nil {
|
||||||
// Then parse the result. Return the created records in libdns format.
|
return nil, err
|
||||||
return recs, nil
|
}
|
||||||
|
|
||||||
|
var created []libdns.Record
|
||||||
|
|
||||||
|
for _, r := range recs {
|
||||||
|
// Prepare JSON for creating a record in Plesk
|
||||||
|
// POST /dns-zones/<zoneID>/records
|
||||||
|
request := pleskRecord{
|
||||||
|
Type: strings.ToUpper(r.Type),
|
||||||
|
Host: r.Name,
|
||||||
|
Value: r.Value,
|
||||||
|
// Add TTL if desired, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("dns-zones/%d/records", zoneID)
|
||||||
|
respBody, err := p.doRequest(ctx, http.MethodPost, path, request)
|
||||||
|
if err != nil {
|
||||||
|
return created, fmt.Errorf("plesk: error creating record '%s': %w", r.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plesk typically returns the created record. Let’s parse it.
|
||||||
|
var createdRecord pleskRecord
|
||||||
|
if err := json.Unmarshal(respBody, &createdRecord); err != nil {
|
||||||
|
return created, fmt.Errorf("plesk: error parsing create-record response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to libdns.Record.
|
||||||
|
out := libdns.Record{
|
||||||
|
ID: fmt.Sprintf("%d", createdRecord.ID),
|
||||||
|
Type: createdRecord.Type,
|
||||||
|
Name: createdRecord.Host,
|
||||||
|
Value: createdRecord.Value,
|
||||||
|
}
|
||||||
|
created = append(created, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return created, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRecords removes the given records from the specified DNS zone.
|
// DeleteRecords removes the given records from the specified DNS zone.
|
||||||
func (p *Provider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
|
func (p *Provider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
|
||||||
// For each record, figure out the record ID or how Plesk identifies it
|
zoneID, err := p.findZoneID(ctx, zone)
|
||||||
// Then DELETE /dns/<zone>/<recordID> (or some Plesk-specific path)
|
if err != nil {
|
||||||
// Return records that were successfully removed
|
return nil, err
|
||||||
return recs, nil
|
}
|
||||||
|
|
||||||
|
var deleted []libdns.Record
|
||||||
|
|
||||||
|
for _, r := range recs {
|
||||||
|
// If r.ID isn’t set, we need to look it up by matching (Type, Name, Value).
|
||||||
|
// However, for a robust delete, we typically rely on ID to be sure.
|
||||||
|
recordID, err := p.getRecordIDIfNeeded(ctx, zoneID, r)
|
||||||
|
if err != nil {
|
||||||
|
return deleted, err
|
||||||
|
}
|
||||||
|
if recordID == 0 {
|
||||||
|
// Not found; skip
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /dns-zones/<zoneID>/records/<recordID>
|
||||||
|
path := fmt.Sprintf("dns-zones/%d/records/%d", zoneID, recordID)
|
||||||
|
_, err = p.doRequest(ctx, http.MethodDelete, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return deleted, fmt.Errorf("plesk: error deleting record ID %d: %w", recordID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as deleted
|
||||||
|
delRec := r
|
||||||
|
if r.ID == "" {
|
||||||
|
// If the original r.ID was empty, fill it with the recordID we used
|
||||||
|
delRec.ID = fmt.Sprintf("%d", recordID)
|
||||||
|
}
|
||||||
|
deleted = append(deleted, delRec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRecordIDIfNeeded returns the record ID from Plesk if the libdns.Record.ID
|
||||||
|
// is empty, by searching existing records for a match. Otherwise returns the
|
||||||
|
// numeric value of r.ID.
|
||||||
|
func (p *Provider) getRecordIDIfNeeded(ctx context.Context, zoneID int, r libdns.Record) (int, error) {
|
||||||
|
if r.ID != "" {
|
||||||
|
// Convert to int
|
||||||
|
var recordID int
|
||||||
|
_, err := fmt.Sscanf(r.ID, "%d", &recordID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("plesk: record ID '%s' not a valid integer: %w", r.ID, err)
|
||||||
|
}
|
||||||
|
return recordID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If r.ID is empty, we search.
|
||||||
|
path := fmt.Sprintf("dns-zones/%d/records", zoneID)
|
||||||
|
respBody, err := p.doRequest(context.Background(), http.MethodGet, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var presRecords []pleskRecord
|
||||||
|
if err := json.Unmarshal(respBody, &presRecords); err != nil {
|
||||||
|
return 0, fmt.Errorf("plesk: error unmarshaling DNS records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We match by Type, Host, Value. (Or do a more sophisticated match if needed.)
|
||||||
|
for _, rec := range presRecords {
|
||||||
|
if strings.EqualFold(rec.Type, r.Type) &&
|
||||||
|
strings.EqualFold(rec.Host, r.Name) &&
|
||||||
|
rec.Value == r.Value {
|
||||||
|
return rec.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, errors.New("plesk: record not found (no ID match)")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user