Files
soft_usite/pkg/services/hostinger_service.go
T

448 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const hostingerBaseURL = "https://developers.hostinger.com/api"
// HostingerClient es el cliente HTTP para la API de Hostinger.
type HostingerClient struct {
token string
httpClient *http.Client
}
// NewHostingerClient crea un cliente con el token Bearer.
func NewHostingerClient(token string) *HostingerClient {
return &HostingerClient{
token: token,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// getRaw realiza una petición GET autenticada y devuelve el body crudo.
// El contexto permite cancelar la llamada si el caller abandona la petición.
func (c *HostingerClient) getRaw(ctx context.Context, path string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, hostingerBaseURL+path, nil)
if err != nil {
return nil, fmt.Errorf("hostinger: crear request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("hostinger: ejecutar request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("hostinger: leer body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("hostinger: status %d %s", resp.StatusCode, string(body))
}
return body, nil
}
// writeRaw envía una petición HTTP autenticada con método y body opcionales.
func (c *HostingerClient) writeRaw(ctx context.Context, method, path string, payload interface{}) ([]byte, error) {
var reqBody io.Reader
if payload != nil {
b, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("hostinger: serializar body: %w", err)
}
reqBody = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, hostingerBaseURL+path, reqBody)
if err != nil {
return nil, fmt.Errorf("hostinger: crear request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("hostinger: ejecutar request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("hostinger: leer body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("hostinger: status %d %s", resp.StatusCode, string(body))
}
return body, nil
}
// get realiza una petición GET autenticada y decodifica el cuerpo en dest.
func (c *HostingerClient) get(path string, dest interface{}) error {
body, err := c.getRaw(context.Background(), path)
if err != nil {
return err
}
if dest != nil {
if err := json.Unmarshal(body, dest); err != nil {
return fmt.Errorf("hostinger: decodificar respuesta: %w", err)
}
}
return nil
}
// ─── Tipos de respuesta ──────────────────────────────────────────────────────
// HostingerIPAddress representa una dirección IP de un VPS (IPv4 o IPv6).
type HostingerIPAddress struct {
ID int `json:"id"`
Address string `json:"address"`
PTR string `json:"ptr"`
}
// HostingerVPS representa una máquina virtual VPS.
type HostingerVPS struct {
ID int `json:"id"`
Hostname string `json:"hostname"`
State string `json:"state"`
Plan string `json:"plan"`
DataCenter string `json:"data_center"`
CPU int `json:"cpus"`
RAMBytes int64 `json:"memory"`
DiskBytes int64 `json:"disk"`
IPV4 []HostingerIPAddress `json:"ipv4"`
IPV6 []HostingerIPAddress `json:"ipv6"`
}
// HostingerDomain representa un dominio en el portafolio.
type HostingerDomain struct {
Domain string `json:"domain"`
Status string `json:"status"`
ExpiresAt string `json:"expires_at"`
RegisteredAt string `json:"registered_at"`
}
// HostingerDNSRecord representa un registro DNS aplanado para la vista.
type HostingerDNSRecord struct {
Type string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
TTL int `json:"ttl"`
}
// HostingerNameServers contiene los nameservers de un dominio.
type HostingerNameServers struct {
NS1 string `json:"ns1"`
NS2 string `json:"ns2"`
NS3 string `json:"ns3"`
NS4 string `json:"ns4"`
}
// HostingerDNSZoneInput es un grupo de registros para PUT /dns/v1/zones/{domain}.
type HostingerDNSZoneInput struct {
Name string `json:"name"`
Type string `json:"type"`
TTL int `json:"ttl,omitempty"`
Records []HostingerDNSZoneValue `json:"records"`
}
// HostingerDNSZoneValue es el valor de contenido de un registro DNS.
type HostingerDNSZoneValue struct {
Content string `json:"content"`
}
// HostingerSubscription representa una suscripción de facturación.
type HostingerSubscription struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
BillingPeriod int `json:"billing_period"`
BillingPeriodUnit string `json:"billing_period_unit"`
CurrencyCode string `json:"currency_code"`
TotalPrice int `json:"total_price"`
RenewalPrice int `json:"renewal_price"`
IsAutoRenewed bool `json:"is_auto_renewed"`
CreatedAt string `json:"created_at"`
ExpiresAt string `json:"expires_at"`
NextBillingAt string `json:"next_billing_at"`
}
// HostingerHosting representa una cuenta de hosting.
type HostingerHosting struct {
Domain string `json:"domain"`
VhostType string `json:"vhost_type"`
IsEnabled bool `json:"is_enabled"`
Username string `json:"username"`
ClientID int `json:"client_id"`
OrderID int `json:"order_id"`
CreatedAt string `json:"created_at"`
RootDirectory string `json:"root_directory"`
ParentDomain string `json:"parent_domain"`
}
// ─── Métodos de la API ───────────────────────────────────────────────────────
// GetVPSList obtiene la lista de VPS/VMs.
func (c *HostingerClient) GetVPSList() ([]HostingerVPS, error) {
body, err := c.getRaw(context.Background(), "/vps/v1/virtual-machines")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerVPS `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerVPS
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta VPS: %w", err)
}
return arr, nil
}
// GetDomains obtiene el portafolio de dominios.
func (c *HostingerClient) GetDomains() ([]HostingerDomain, error) {
body, err := c.getRaw(context.Background(), "/domains/v1/portfolio")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerDomain `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerDomain
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta dominios: %w", err)
}
return arr, nil
}
// hostingerDNSContent es el valor individual de un registro DNS.
type hostingerDNSContent struct {
Content string `json:"content"`
IsDisabled bool `json:"is_disabled"`
}
// hostingerDNSEntry es un grupo de registros del mismo tipo/nombre desde la API.
type hostingerDNSEntry struct {
Name string `json:"name"`
TTL int `json:"ttl"`
Type string `json:"type"`
Records []hostingerDNSContent `json:"records"`
}
// GetDNSRecords obtiene los registros DNS de un dominio y los aplana para la vista.
// Endpoint: GET /dns/v1/zones/{domain} — array de {name, ttl, type, records:[{content}]}
func (c *HostingerClient) GetDNSRecords(domain string) ([]HostingerDNSRecord, error) {
path := fmt.Sprintf("/dns/v1/zones/%s", domain)
body, err := c.getRaw(context.Background(), path)
if err != nil {
return nil, err
}
var entries []hostingerDNSEntry
if err := json.Unmarshal(body, &entries); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta DNS: %w", err)
}
var result []HostingerDNSRecord
for _, e := range entries {
for _, r := range e.Records {
result = append(result, HostingerDNSRecord{
Type: e.Type,
Name: e.Name,
Content: r.Content,
TTL: e.TTL,
})
}
}
if result == nil {
result = []HostingerDNSRecord{}
}
return result, nil
}
// GetDomainNameServers obtiene los nameservers del dominio desde el portafolio.
// Endpoint: GET /domains/v1/portfolio/{domain}
func (c *HostingerClient) GetDomainNameServers(domain string) (*HostingerNameServers, error) {
path := fmt.Sprintf("/domains/v1/portfolio/%s", domain)
body, err := c.getRaw(context.Background(), path)
if err != nil {
return nil, err
}
var resp struct {
NameServers HostingerNameServers `json:"name_servers"`
}
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar nameservers: %w", err)
}
return &resp.NameServers, nil
}
// GetOrders obtiene las suscripciones de facturación.
func (c *HostingerClient) GetOrders() ([]HostingerSubscription, error) {
body, err := c.getRaw(context.Background(), "/billing/v1/subscriptions")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerSubscription `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerSubscription
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta subscriptions: %w", err)
}
return arr, nil
}
// ─── DNS: escritura ─────────────────────────────────────────────────────────
// UpdateDNSZone agrega/actualiza registros en la zona DNS.
// overwrite=true reemplaza registros del mismo tipo/nombre; false los agrega.
func (c *HostingerClient) UpdateDNSZone(domain string, overwrite bool, zone []HostingerDNSZoneInput) error {
path := fmt.Sprintf("/dns/v1/zones/%s", domain)
_, err := c.writeRaw(context.Background(), http.MethodPut, path,
map[string]interface{}{"overwrite": overwrite, "zone": zone})
return err
}
// ResetDNS restaura la zona DNS de un dominio a sus valores por defecto.
func (c *HostingerClient) ResetDNS(domain string) error {
path := fmt.Sprintf("/dns/v1/zones/%s/reset", domain)
_, err := c.writeRaw(context.Background(), http.MethodPost, path, map[string]bool{"sync": true})
return err
}
// ─── Domains: escritura ──────────────────────────────────────────────────────
// UpdateNameservers actualiza los nameservers de un dominio.
func (c *HostingerClient) UpdateNameservers(domain, ns1, ns2, ns3, ns4 string) error {
path := fmt.Sprintf("/domains/v1/portfolio/%s/nameservers", domain)
_, err := c.writeRaw(context.Background(), http.MethodPut, path,
map[string]string{"ns1": ns1, "ns2": ns2, "ns3": ns3, "ns4": ns4})
return err
}
// SetDomainLock activa (PUT) o desactiva (DELETE) el bloqueo del dominio.
func (c *HostingerClient) SetDomainLock(domain string, enable bool) error {
path := fmt.Sprintf("/domains/v1/portfolio/%s/domain-lock", domain)
method := http.MethodPut
if !enable {
method = http.MethodDelete
}
_, err := c.writeRaw(context.Background(), method, path, nil)
return err
}
// SetPrivacyProtection activa (PUT) o desactiva (DELETE) la protección WHOIS.
func (c *HostingerClient) SetPrivacyProtection(domain string, enable bool) error {
path := fmt.Sprintf("/domains/v1/portfolio/%s/privacy-protection", domain)
method := http.MethodPut
if !enable {
method = http.MethodDelete
}
_, err := c.writeRaw(context.Background(), method, path, nil)
return err
}
// ─── VPS: acciones ───────────────────────────────────────────────────────────
// VPSAction ejecuta una acción en una VM: start | stop | restart.
func (c *HostingerClient) VPSAction(vmID int, action string) error {
path := fmt.Sprintf("/vps/v1/virtual-machines/%d/%s", vmID, action)
_, err := c.writeRaw(context.Background(), http.MethodPost, path, nil)
return err
}
// SetVPSRootPassword cambia la contraseña root de una VM.
func (c *HostingerClient) SetVPSRootPassword(vmID int, password string) error {
path := fmt.Sprintf("/vps/v1/virtual-machines/%d/root-password", vmID)
_, err := c.writeRaw(context.Background(), http.MethodPut, path, map[string]string{"password": password})
return err
}
// SetVPSHostname cambia el hostname de una VM.
func (c *HostingerClient) SetVPSHostname(vmID int, hostname string) error {
path := fmt.Sprintf("/vps/v1/virtual-machines/%d/hostname", vmID)
_, err := c.writeRaw(context.Background(), http.MethodPut, path, map[string]string{"hostname": hostname})
return err
}
// GetVPSMetrics obtiene las métricas de uso de una VM.
// dateFrom y dateTo deben estar en formato RFC3339, p. ej. "2025-01-01T00:00:00Z".
func (c *HostingerClient) GetVPSMetrics(vmID int, dateFrom, dateTo string) (json.RawMessage, error) {
path := fmt.Sprintf("/vps/v1/virtual-machines/%d/metrics?date_from=%s&date_to=%s",
vmID, dateFrom, dateTo)
body, err := c.getRaw(context.Background(), path)
if err != nil {
return nil, err
}
return json.RawMessage(body), nil
}
// GetVPSBackups obtiene los backups de una VM.
func (c *HostingerClient) GetVPSBackups(vmID int) (json.RawMessage, error) {
path := fmt.Sprintf("/vps/v1/virtual-machines/%d/backups", vmID)
body, err := c.getRaw(context.Background(), path)
if err != nil {
return nil, err
}
return json.RawMessage(body), nil
}
// ─── Billing: acciones ───────────────────────────────────────────────────────
// ToggleAutoRenewal activa (true) o desactiva la autorenovación de una suscripción.
func (c *HostingerClient) ToggleAutoRenewal(subscriptionID string, enable bool) (*HostingerSubscription, error) {
var path, method string
if enable {
path = fmt.Sprintf("/billing/v1/subscriptions/%s/auto-renewal/enable", subscriptionID)
method = http.MethodPatch
} else {
path = fmt.Sprintf("/billing/v1/subscriptions/%s/auto-renewal/disable", subscriptionID)
method = http.MethodDelete
}
body, err := c.writeRaw(context.Background(), method, path, nil)
if err != nil {
return nil, err
}
var sub HostingerSubscription
if err := json.Unmarshal(body, &sub); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta: %w", err)
}
return &sub, nil
}
// GetHostingAccounts obtiene las cuentas de hosting.
// Endpoint: GET /hosting/v1/websites
func (c *HostingerClient) GetHostingAccounts() ([]HostingerHosting, error) {
body, err := c.getRaw(context.Background(), "/hosting/v1/websites")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerHosting `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerHosting
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta hosting: %w", err)
}
return arr, nil
}