Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions pkg/providers/hetzner/hetzner.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/projectdiscovery/cloudlist/pkg/schema"
)

var Services = []string{"instance"}
var Services = []string{"instance", "loadbalancer", "floatingip", "primaryip"}

const (
authToken = "auth_token"
Expand Down Expand Up @@ -53,9 +53,21 @@ func (p *Provider) Services() []string {
// Resources returns the provider for an resource
func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) {
finalResources := schema.NewResources()
if p.services.Has("instance") {
provider := &instanceProvider{client: p.client, id: p.id}
if resources, err := provider.GetResource(ctx); err == nil {
providers := map[string]interface {
GetResource(ctx context.Context) (*schema.Resources, error)
}{
"instance": &instanceProvider{client: p.client, id: p.id},
"loadbalancer": &loadBalancerProvider{client: p.client, id: p.id},
"floatingip": &floatingIPProvider{client: p.client, id: p.id},
"primaryip": &primaryIPProvider{client: p.client, id: p.id},
}
// Address services are merged before instances. The first writer owns a
// shared address, so an assigned primary IP stays a primaryip resource.
for _, service := range []string{"loadbalancer", "floatingip", "primaryip", "instance"} {
if !p.services.Has(service) {
continue
}
if resources, err := providers[service].GetResource(ctx); err == nil {
finalResources.Merge(resources)
}
}
Expand Down
89 changes: 89 additions & 0 deletions pkg/providers/hetzner/hetzner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package hetzner

import (
"context"
"net/http"
"net/http/httptest"
"testing"

hetzner "github.com/hetznercloud/hcloud-go/hcloud"
"github.com/projectdiscovery/cloudlist/pkg/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var fakeResponses = map[string]string{
"/servers": `{"servers": [
{"id": 42, "name": "web", "public_net": {
"ipv4": {"ip": "95.217.30.1"},
"ipv6": {"ip": "2a01:4f9:c010:9::/64", "dns_ptr": []}
}}
]}`,
"/load_balancers": `{"load_balancers": [
{"id": 1, "name": "public-lb", "public_net": {"enabled": true,
"ipv4": {"ip": "95.217.10.1"}, "ipv6": {"ip": "2a01:4f9:c010:1::1"}}},
{"id": 2, "name": "private-lb", "public_net": {"enabled": false,
"ipv4": {"ip": "95.217.10.2"}, "ipv6": {"ip": ""}}}
]}`,
"/floating_ips": `{"floating_ips": [
{"id": 1, "type": "ipv4", "ip": "95.217.20.1", "server": 42},
{"id": 2, "type": "ipv6", "ip": "2a01:4f9:c010:2::/64", "server": null}
]}`,
"/primary_ips": `{"primary_ips": [
{"id": 1, "type": "ipv4", "ip": "95.217.30.1", "assignee_id": 42, "assignee_type": "server"},
{"id": 2, "type": "ipv4", "ip": "95.217.30.2", "assignee_id": null, "assignee_type": "server"}
]}`,
}

func newTestProvider(t *testing.T, services ...string) *Provider {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, ok := fakeResponses[r.URL.Path]
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(body))
}))
t.Cleanup(server.Close)

client := hetzner.NewClient(hetzner.WithToken("test"), hetzner.WithEndpoint(server.URL))
block := schema.OptionBlock{}
if len(services) > 0 {
block["services"] = services[0]
}
return &Provider{id: "test", client: client, services: block.ResolveServices(Services)}
}

func resourcesByService(resources *schema.Resources) map[string][]string {
byService := make(map[string][]string)
for _, r := range resources.Items {
for _, v := range []string{r.PublicIPv4, r.PublicIPv6} {
if v != "" {
byService[r.Service] = append(byService[r.Service], v)
}
}
}
return byService
}

func TestResources(t *testing.T) {
resources, err := newTestProvider(t).Resources(context.Background())
require.NoError(t, err)

byService := resourcesByService(resources)
assert.ElementsMatch(t, []string{"95.217.10.1", "2a01:4f9:c010:1::1"}, byService["loadbalancer"], "load balancers with public net disabled must be skipped")
assert.ElementsMatch(t, []string{"95.217.20.1", "2a01:4f9:c010:2::"}, byService["floatingip"])
assert.ElementsMatch(t, []string{"95.217.30.1", "95.217.30.2"}, byService["primaryip"], "unassigned primary IPs must be included")
assert.ElementsMatch(t, []string{"2a01:4f9:c010:9::"}, byService["instance"], "an assigned primary IPv4 keeps the primaryip service")
}

func TestResourcesServiceFilter(t *testing.T) {
resources, err := newTestProvider(t, "primaryip").Resources(context.Background())
require.NoError(t, err)

byService := resourcesByService(resources)
assert.Len(t, byService, 1)
assert.Len(t, byService["primaryip"], 2)
}
83 changes: 83 additions & 0 deletions pkg/providers/hetzner/ips.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package hetzner

import (
"context"
"net"

hetzner "github.com/hetznercloud/hcloud-go/hcloud"
"github.com/projectdiscovery/cloudlist/pkg/schema"
)

// floatingIPProvider is a floating IP provider for Hetzner Cloud API
type floatingIPProvider struct {
id string
client *hetzner.Client
}

func (p *floatingIPProvider) name() string {
return "floatingip"
}

// GetResource returns all the floating IPs, assigned or not.
func (p *floatingIPProvider) GetResource(ctx context.Context) (*schema.Resources, error) {
list := schema.NewResources()

floatingIPs, err := p.client.FloatingIP.All(ctx)
if err != nil {
return nil, err
}

for _, ip := range floatingIPs {
list.Append(publicIPResource(p.id, p.name(), ip.IP))
}
return list, nil
}

// primaryIPProvider is a primary IP provider for Hetzner Cloud API
type primaryIPProvider struct {
id string
client *hetzner.Client
}

func (p *primaryIPProvider) name() string {
return "primaryip"
}

// GetResource returns all the primary IPs, including unassigned ones that
// the server listing cannot surface.
func (p *primaryIPProvider) GetResource(ctx context.Context) (*schema.Resources, error) {
list := schema.NewResources()

primaryIPs, err := p.client.PrimaryIP.All(ctx)
if err != nil {
return nil, err
}

for _, ip := range primaryIPs {
list.Append(publicIPResource(p.id, p.name(), ip.IP))
}
return list, nil
}

func publicIPResource(id, service string, ip net.IP) *schema.Resource {
resource := &schema.Resource{
Provider: providerName,
ID: id,
Public: true,
Service: service,
}
if ip.To4() != nil {
resource.PublicIPv4 = ip.String()
} else {
resource.PublicIPv6 = ipString(ip)
}
return resource
}

// ipString avoids net.IP's "<nil>" rendering for unset addresses.
func ipString(ip net.IP) string {
if ip == nil {
return ""
}
return ip.String()
}
43 changes: 43 additions & 0 deletions pkg/providers/hetzner/loadbalancers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package hetzner

import (
"context"

hetzner "github.com/hetznercloud/hcloud-go/hcloud"
"github.com/projectdiscovery/cloudlist/pkg/schema"
)

// loadBalancerProvider is a load balancer provider for Hetzner Cloud API
type loadBalancerProvider struct {
id string
client *hetzner.Client
}

func (p *loadBalancerProvider) name() string {
return "loadbalancer"
}

// GetResource returns all the load balancers in the store for a provider.
func (p *loadBalancerProvider) GetResource(ctx context.Context) (*schema.Resources, error) {
list := schema.NewResources()

loadBalancers, err := p.client.LoadBalancer.All(ctx)
if err != nil {
return nil, err
}

for _, lb := range loadBalancers {
if !lb.PublicNet.Enabled {
continue
}
list.Append(&schema.Resource{
Provider: providerName,
ID: p.id,
PublicIPv4: ipString(lb.PublicNet.IPv4.IP),
PublicIPv6: ipString(lb.PublicNet.IPv6.IP),
Public: true,
Service: p.name(),
})
}
return list, nil
}
Loading