1package hcloud
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"errors"
8	"fmt"
9	"net"
10	"net/url"
11	"strconv"
12	"time"
13
14	"github.com/hetznercloud/hcloud-go/hcloud/schema"
15)
16
17// Firewall represents a Firewall in the Hetzner Cloud.
18type Firewall struct {
19	ID        int
20	Name      string
21	Labels    map[string]string
22	Created   time.Time
23	Rules     []FirewallRule
24	AppliedTo []FirewallResource
25}
26
27// FirewallRule represents a Firewall's rules.
28type FirewallRule struct {
29	Direction      FirewallRuleDirection
30	SourceIPs      []net.IPNet
31	DestinationIPs []net.IPNet
32	Protocol       FirewallRuleProtocol
33	Port           *string
34}
35
36// FirewallRuleDirection specifies the direction of a Firewall rule.
37type FirewallRuleDirection string
38
39const (
40	// FirewallRuleDirectionIn specifies a rule for inbound traffic.
41	FirewallRuleDirectionIn FirewallRuleDirection = "in"
42
43	// FirewallRuleDirectionOut specifies a rule for outbound traffic.
44	FirewallRuleDirectionOut FirewallRuleDirection = "out"
45)
46
47// FirewallRuleProtocol specifies the protocol of a Firewall rule.
48type FirewallRuleProtocol string
49
50const (
51	// FirewallRuleProtocolTCP specifies a TCP rule.
52	FirewallRuleProtocolTCP FirewallRuleProtocol = "tcp"
53	// FirewallRuleProtocolUDP specifies a UDP rule.
54	FirewallRuleProtocolUDP FirewallRuleProtocol = "udp"
55	// FirewallRuleProtocolICMP specifies an ICMP rule.
56	FirewallRuleProtocolICMP FirewallRuleProtocol = "icmp"
57)
58
59// FirewallResourceType specifies the resource to apply a Firewall on.
60type FirewallResourceType string
61
62const (
63	// FirewallResourceTypeServer specifies a Server.
64	FirewallResourceTypeServer FirewallResourceType = "server"
65)
66
67// FirewallResource represents a resource to apply the new Firewall on.
68type FirewallResource struct {
69	Type   FirewallResourceType
70	Server *FirewallResourceServer
71}
72
73// FirewallResourceServer represents a Server to apply a Firewall on.
74type FirewallResourceServer struct {
75	ID int
76}
77
78// FirewallClient is a client for the Firewalls API.
79type FirewallClient struct {
80	client *Client
81}
82
83// GetByID retrieves a Firewall by its ID. If the Firewall does not exist, nil is returned.
84func (c *FirewallClient) GetByID(ctx context.Context, id int) (*Firewall, *Response, error) {
85	req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/firewalls/%d", id), nil)
86	if err != nil {
87		return nil, nil, err
88	}
89
90	var body schema.FirewallGetResponse
91	resp, err := c.client.Do(req, &body)
92	if err != nil {
93		if IsError(err, ErrorCodeNotFound) {
94			return nil, resp, nil
95		}
96		return nil, nil, err
97	}
98	return FirewallFromSchema(body.Firewall), resp, nil
99}
100
101// GetByName retrieves a Firewall by its name. If the Firewall does not exist, nil is returned.
102func (c *FirewallClient) GetByName(ctx context.Context, name string) (*Firewall, *Response, error) {
103	if name == "" {
104		return nil, nil, nil
105	}
106	firewalls, response, err := c.List(ctx, FirewallListOpts{Name: name})
107	if len(firewalls) == 0 {
108		return nil, response, err
109	}
110	return firewalls[0], response, err
111}
112
113// Get retrieves a Firewall by its ID if the input can be parsed as an integer, otherwise it
114// retrieves a Firewall by its name. If the Firewall does not exist, nil is returned.
115func (c *FirewallClient) Get(ctx context.Context, idOrName string) (*Firewall, *Response, error) {
116	if id, err := strconv.Atoi(idOrName); err == nil {
117		return c.GetByID(ctx, int(id))
118	}
119	return c.GetByName(ctx, idOrName)
120}
121
122// FirewallListOpts specifies options for listing Firewalls.
123type FirewallListOpts struct {
124	ListOpts
125	Name string
126}
127
128func (l FirewallListOpts) values() url.Values {
129	vals := l.ListOpts.values()
130	if l.Name != "" {
131		vals.Add("name", l.Name)
132	}
133	return vals
134}
135
136// List returns a list of Firewalls for a specific page.
137//
138// Please note that filters specified in opts are not taken into account
139// when their value corresponds to their zero value or when they are empty.
140func (c *FirewallClient) List(ctx context.Context, opts FirewallListOpts) ([]*Firewall, *Response, error) {
141	path := "/firewalls?" + opts.values().Encode()
142	req, err := c.client.NewRequest(ctx, "GET", path, nil)
143	if err != nil {
144		return nil, nil, err
145	}
146
147	var body schema.FirewallListResponse
148	resp, err := c.client.Do(req, &body)
149	if err != nil {
150		return nil, nil, err
151	}
152	firewalls := make([]*Firewall, 0, len(body.Firewalls))
153	for _, s := range body.Firewalls {
154		firewalls = append(firewalls, FirewallFromSchema(s))
155	}
156	return firewalls, resp, nil
157}
158
159// All returns all Firewalls.
160func (c *FirewallClient) All(ctx context.Context) ([]*Firewall, error) {
161	allFirewalls := []*Firewall{}
162
163	opts := FirewallListOpts{}
164	opts.PerPage = 50
165
166	err := c.client.all(func(page int) (*Response, error) {
167		opts.Page = page
168		firewalls, resp, err := c.List(ctx, opts)
169		if err != nil {
170			return resp, err
171		}
172		allFirewalls = append(allFirewalls, firewalls...)
173		return resp, nil
174	})
175	if err != nil {
176		return nil, err
177	}
178
179	return allFirewalls, nil
180}
181
182// AllWithOpts returns all Firewalls for the given options.
183func (c *FirewallClient) AllWithOpts(ctx context.Context, opts FirewallListOpts) ([]*Firewall, error) {
184	var allFirewalls []*Firewall
185
186	err := c.client.all(func(page int) (*Response, error) {
187		opts.Page = page
188		firewalls, resp, err := c.List(ctx, opts)
189		if err != nil {
190			return resp, err
191		}
192		allFirewalls = append(allFirewalls, firewalls...)
193		return resp, nil
194	})
195	if err != nil {
196		return nil, err
197	}
198
199	return allFirewalls, nil
200}
201
202// FirewallCreateOpts specifies options for creating a new Firewall.
203type FirewallCreateOpts struct {
204	Name    string
205	Labels  map[string]string
206	Rules   []FirewallRule
207	ApplyTo []FirewallResource
208}
209
210// Validate checks if options are valid.
211func (o FirewallCreateOpts) Validate() error {
212	if o.Name == "" {
213		return errors.New("missing name")
214	}
215	return nil
216}
217
218// FirewallCreateResult is the result of a create Firewall call.
219type FirewallCreateResult struct {
220	Firewall *Firewall
221	Actions  []*Action
222}
223
224// Create creates a new Firewall.
225func (c *FirewallClient) Create(ctx context.Context, opts FirewallCreateOpts) (FirewallCreateResult, *Response, error) {
226	if err := opts.Validate(); err != nil {
227		return FirewallCreateResult{}, nil, err
228	}
229	reqBody := firewallCreateOptsToSchema(opts)
230	reqBodyData, err := json.Marshal(reqBody)
231	if err != nil {
232		return FirewallCreateResult{}, nil, err
233	}
234	req, err := c.client.NewRequest(ctx, "POST", "/firewalls", bytes.NewReader(reqBodyData))
235	if err != nil {
236		return FirewallCreateResult{}, nil, err
237	}
238
239	respBody := schema.FirewallCreateResponse{}
240	resp, err := c.client.Do(req, &respBody)
241	if err != nil {
242		return FirewallCreateResult{}, resp, err
243	}
244	result := FirewallCreateResult{
245		Firewall: FirewallFromSchema(respBody.Firewall),
246		Actions:  ActionsFromSchema(respBody.Actions),
247	}
248	return result, resp, nil
249}
250
251// FirewallUpdateOpts specifies options for updating a Firewall.
252type FirewallUpdateOpts struct {
253	Name   string
254	Labels map[string]string
255}
256
257// Update updates a Firewall.
258func (c *FirewallClient) Update(ctx context.Context, firewall *Firewall, opts FirewallUpdateOpts) (*Firewall, *Response, error) {
259	reqBody := schema.FirewallUpdateRequest{}
260	if opts.Name != "" {
261		reqBody.Name = &opts.Name
262	}
263	if opts.Labels != nil {
264		reqBody.Labels = &opts.Labels
265	}
266	reqBodyData, err := json.Marshal(reqBody)
267	if err != nil {
268		return nil, nil, err
269	}
270
271	path := fmt.Sprintf("/firewalls/%d", firewall.ID)
272	req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
273	if err != nil {
274		return nil, nil, err
275	}
276
277	respBody := schema.FirewallUpdateResponse{}
278	resp, err := c.client.Do(req, &respBody)
279	if err != nil {
280		return nil, resp, err
281	}
282	return FirewallFromSchema(respBody.Firewall), resp, nil
283}
284
285// Delete deletes a Firewall.
286func (c *FirewallClient) Delete(ctx context.Context, firewall *Firewall) (*Response, error) {
287	req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/firewalls/%d", firewall.ID), nil)
288	if err != nil {
289		return nil, err
290	}
291	return c.client.Do(req, nil)
292}
293
294// FirewallSetRulesOpts specifies options for setting rules of a Firewall.
295type FirewallSetRulesOpts struct {
296	Rules []FirewallRule
297}
298
299// SetRules sets the rules of a Firewall.
300func (c *FirewallClient) SetRules(ctx context.Context, firewall *Firewall, opts FirewallSetRulesOpts) ([]*Action, *Response, error) {
301	reqBody := firewallSetRulesOptsToSchema(opts)
302	reqBodyData, err := json.Marshal(reqBody)
303	if err != nil {
304		return nil, nil, err
305	}
306
307	path := fmt.Sprintf("/firewalls/%d/actions/set_rules", firewall.ID)
308	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
309	if err != nil {
310		return nil, nil, err
311	}
312
313	var respBody schema.FirewallActionSetRulesResponse
314	resp, err := c.client.Do(req, &respBody)
315	if err != nil {
316		return nil, resp, err
317	}
318	return ActionsFromSchema(respBody.Actions), resp, nil
319}
320
321func (c *FirewallClient) ApplyResources(ctx context.Context, firewall *Firewall, resources []FirewallResource) ([]*Action, *Response, error) {
322	applyTo := make([]schema.FirewallResource, len(resources))
323	for i, r := range resources {
324		applyTo[i] = firewallResourceToSchema(r)
325	}
326
327	reqBody := schema.FirewallActionApplyToResourcesRequest{ApplyTo: applyTo}
328	reqBodyData, err := json.Marshal(reqBody)
329	if err != nil {
330		return nil, nil, err
331	}
332
333	path := fmt.Sprintf("/firewalls/%d/actions/apply_to_resources", firewall.ID)
334	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
335	if err != nil {
336		return nil, nil, err
337	}
338
339	var respBody schema.FirewallActionApplyToResourcesResponse
340	resp, err := c.client.Do(req, &respBody)
341	if err != nil {
342		return nil, resp, err
343	}
344	return ActionsFromSchema(respBody.Actions), resp, nil
345}
346
347func (c *FirewallClient) RemoveResources(ctx context.Context, firewall *Firewall, resources []FirewallResource) ([]*Action, *Response, error) {
348	removeFrom := make([]schema.FirewallResource, len(resources))
349	for i, r := range resources {
350		removeFrom[i] = firewallResourceToSchema(r)
351	}
352
353	reqBody := schema.FirewallActionRemoveFromResourcesRequest{RemoveFrom: removeFrom}
354	reqBodyData, err := json.Marshal(reqBody)
355	if err != nil {
356		return nil, nil, err
357	}
358
359	path := fmt.Sprintf("/firewalls/%d/actions/remove_from_resources", firewall.ID)
360	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
361	if err != nil {
362		return nil, nil, err
363	}
364
365	var respBody schema.FirewallActionRemoveFromResourcesResponse
366	resp, err := c.client.Do(req, &respBody)
367	if err != nil {
368		return nil, resp, err
369	}
370	return ActionsFromSchema(respBody.Actions), resp, nil
371}
372