You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
6.8 KiB
240 lines
6.8 KiB
package cfapi |
|
|
|
import ( |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"net" |
|
"net/http" |
|
"net/url" |
|
"path" |
|
"time" |
|
|
|
"github.com/google/uuid" |
|
"github.com/pkg/errors" |
|
) |
|
|
|
// Route is a mapping from customer's IP space to a tunnel. |
|
// Each route allows the customer to route eyeballs in their corporate network |
|
// to certain private IP ranges. Each Route represents an IP range in their |
|
// network, and says that eyeballs can reach that route using the corresponding |
|
// tunnel. |
|
type Route struct { |
|
Network CIDR `json:"network"` |
|
TunnelID uuid.UUID `json:"tunnel_id"` |
|
// Optional field. When unset, it means the Route belongs to the default virtual network. |
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"` |
|
Comment string `json:"comment"` |
|
CreatedAt time.Time `json:"created_at"` |
|
DeletedAt time.Time `json:"deleted_at"` |
|
} |
|
|
|
// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling. |
|
type CIDR net.IPNet |
|
|
|
func (c CIDR) String() string { |
|
n := net.IPNet(c) |
|
return n.String() |
|
} |
|
|
|
func (c CIDR) MarshalJSON() ([]byte, error) { |
|
str := c.String() |
|
json, err := json.Marshal(str) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "error serializing CIDR into JSON") |
|
} |
|
return json, nil |
|
} |
|
|
|
// UnmarshalJSON parses a JSON string into net.IPNet |
|
func (c *CIDR) UnmarshalJSON(data []byte) error { |
|
var s string |
|
if err := json.Unmarshal(data, &s); err != nil { |
|
return errors.Wrap(err, "error parsing cidr string") |
|
} |
|
_, network, err := net.ParseCIDR(s) |
|
if err != nil { |
|
return errors.Wrap(err, "error parsing invalid network from backend") |
|
} |
|
if network == nil { |
|
return fmt.Errorf("backend returned invalid network %s", s) |
|
} |
|
*c = CIDR(*network) |
|
return nil |
|
} |
|
|
|
// NewRoute has all the parameters necessary to add a new route to the table. |
|
type NewRoute struct { |
|
Network net.IPNet |
|
TunnelID uuid.UUID |
|
Comment string |
|
// Optional field. If unset, backend will assume the default vnet for the account. |
|
VNetID *uuid.UUID |
|
} |
|
|
|
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet). |
|
func (r NewRoute) MarshalJSON() ([]byte, error) { |
|
return json.Marshal(&struct { |
|
TunnelID uuid.UUID `json:"tunnel_id"` |
|
Comment string `json:"comment"` |
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"` |
|
}{ |
|
TunnelID: r.TunnelID, |
|
Comment: r.Comment, |
|
VNetID: r.VNetID, |
|
}) |
|
} |
|
|
|
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName. |
|
type DetailedRoute struct { |
|
Network CIDR `json:"network"` |
|
TunnelID uuid.UUID `json:"tunnel_id"` |
|
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network. |
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"` |
|
Comment string `json:"comment"` |
|
CreatedAt time.Time `json:"created_at"` |
|
DeletedAt time.Time `json:"deleted_at"` |
|
TunnelName string `json:"tunnel_name"` |
|
} |
|
|
|
// IsZero checks if DetailedRoute is the zero value. |
|
func (r *DetailedRoute) IsZero() bool { |
|
return r.TunnelID == uuid.Nil |
|
} |
|
|
|
// TableString outputs a table row summarizing the route, to be used |
|
// when showing the user their routing table. |
|
func (r DetailedRoute) TableString() string { |
|
deletedColumn := "-" |
|
if !r.DeletedAt.IsZero() { |
|
deletedColumn = r.DeletedAt.Format(time.RFC3339) |
|
} |
|
vnetColumn := "default" |
|
if r.VNetID != nil { |
|
vnetColumn = r.VNetID.String() |
|
} |
|
|
|
return fmt.Sprintf( |
|
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t", |
|
r.Network.String(), |
|
vnetColumn, |
|
r.Comment, |
|
r.TunnelID, |
|
r.TunnelName, |
|
r.CreatedAt.Format(time.RFC3339), |
|
deletedColumn, |
|
) |
|
} |
|
|
|
type DeleteRouteParams struct { |
|
Network net.IPNet |
|
// Optional field. If unset, backend will assume the default vnet for the account. |
|
VNetID *uuid.UUID |
|
} |
|
|
|
type GetRouteByIpParams struct { |
|
Ip net.IP |
|
// Optional field. If unset, backend will assume the default vnet for the account. |
|
VNetID *uuid.UUID |
|
} |
|
|
|
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account. |
|
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) { |
|
endpoint := r.baseEndpoints.accountRoutes |
|
endpoint.RawQuery = filter.Encode() |
|
resp, err := r.sendRequest("GET", endpoint, nil) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "REST request failed") |
|
} |
|
defer resp.Body.Close() |
|
|
|
if resp.StatusCode == http.StatusOK { |
|
return parseListDetailedRoutes(resp.Body) |
|
} |
|
|
|
return nil, r.statusCodeToError("list routes", resp) |
|
} |
|
|
|
// AddRoute calls the Tunnelstore POST endpoint for a given route. |
|
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) { |
|
endpoint := r.baseEndpoints.accountRoutes |
|
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String())) |
|
resp, err := r.sendRequest("POST", endpoint, newRoute) |
|
if err != nil { |
|
return Route{}, errors.Wrap(err, "REST request failed") |
|
} |
|
defer resp.Body.Close() |
|
|
|
if resp.StatusCode == http.StatusOK { |
|
return parseRoute(resp.Body) |
|
} |
|
|
|
return Route{}, r.statusCodeToError("add route", resp) |
|
} |
|
|
|
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route. |
|
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error { |
|
endpoint := r.baseEndpoints.accountRoutes |
|
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String())) |
|
setVnetParam(&endpoint, params.VNetID) |
|
|
|
resp, err := r.sendRequest("DELETE", endpoint, nil) |
|
if err != nil { |
|
return errors.Wrap(err, "REST request failed") |
|
} |
|
defer resp.Body.Close() |
|
|
|
if resp.StatusCode == http.StatusOK { |
|
_, err := parseRoute(resp.Body) |
|
return err |
|
} |
|
|
|
return r.statusCodeToError("delete route", resp) |
|
} |
|
|
|
// GetByIP checks which route will proxy a given IP. |
|
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) { |
|
endpoint := r.baseEndpoints.accountRoutes |
|
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String())) |
|
setVnetParam(&endpoint, params.VNetID) |
|
|
|
resp, err := r.sendRequest("GET", endpoint, nil) |
|
if err != nil { |
|
return DetailedRoute{}, errors.Wrap(err, "REST request failed") |
|
} |
|
defer resp.Body.Close() |
|
|
|
if resp.StatusCode == http.StatusOK { |
|
return parseDetailedRoute(resp.Body) |
|
} |
|
|
|
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp) |
|
} |
|
|
|
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) { |
|
var routes []*DetailedRoute |
|
err := parseResponse(body, &routes) |
|
return routes, err |
|
} |
|
|
|
func parseRoute(body io.ReadCloser) (Route, error) { |
|
var route Route |
|
err := parseResponse(body, &route) |
|
return route, err |
|
} |
|
|
|
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) { |
|
var route DetailedRoute |
|
err := parseResponse(body, &route) |
|
return route, err |
|
} |
|
|
|
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain |
|
// virtual network (if one is provided). |
|
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) { |
|
queryParams := url.Values{} |
|
if vnetID != nil { |
|
queryParams.Set("virtual_network_id", vnetID.String()) |
|
} |
|
endpoint.RawQuery = queryParams.Encode() |
|
}
|
|
|