diff --git a/README.md b/README.md index 11775e1..153dc2e 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,83 @@ sudo ./bin/bittwister start -d eth0 -l 100 sudo ./bin/bittwister start -d eth0 -j 10 ``` +### Start the API server + +```bash +sudo ./bin/bittwister serve [flags] + +Flags: + -h, --help help for serve + --log-level string log level (e.g. debug, info, warn, error, dpanic, panic, fatal) (default "info") + --origin-allowed string origin allowed for CORS (default "*") + --production-mode production mode (e.g. disable debug logs) + --serve-addr string address to serve on (default "localhost:9007") +``` + +### API Endpoints + +Please note that all the endpoints have to be prefixed with `/api/v1`. + +#### Packet Loss + +- **Endpoint:** `/packetloss` + - `/start` + - **Method:** POST + - **Data**: `{"network_interface":"eth0","packet_loss_rate":30}` + - **Description:** Start packetloss service. + - `/status` + - **Method:** GET + - **Description:** Get packetloss status. + - `/stop` + - **Method:** POST + - **Description:** Stop packetloss service. + +**example:** + +```bash +curl -iX POST http://localhost:9007/api/v1/packetloss/start --data '{"network_interface":"eth0","packet_loss_rate":30}' +``` + +#### Bandwidth + +- **Endpoint:** `/bandwidth` + - `/start` + - **Method:** POST + - **Data**: `{"network_interface":"eth0","bandwidth":1048576}` + - **Description:** Start bandwidth service. + - `/status` + - **Method:** GET + - **Description:** Get bandwidth status. + - `/stop` + - **Method:** POST + - **Description:** Stop bandwidth service. + +#### Latency + +- **Endpoint:** `/latency` + - `/start` + - **Method:** POST + - **Data**: `{"network_interface":"eth0","latency":100,"jitter":10}` + - **Description:** Start latency service. + - `/status` + - **Method:** GET + - **Description:** Get latency status. + - `/stop` + - **Method:** POST + - **Description:** Stop latency service. + +#### Services + +- **Endpoint:** `/services` + - `/status` + - **Method:** GET + - **Description:** Get all network restriction services statuses and their configured parameters. + +### SDK for Go + +The BitTwister SDK for Go provides a convenient interface to interact with the BitTwister tool, which applies network restrictions on a network interface, including bandwidth limitation, packet loss, latency, and jitter. +More details about the SDK and how to use it can be found [here](./sdk/README.md). + ### Using Bittwister in Kubernetes To utilize Bittwister within a Kubernetes environment, specific configurations must be added to the container. @@ -75,6 +152,16 @@ The tests require docker to be installed. To run all the tests, execute the foll make test ``` +### Go unit tests + +The Go unit tests can be run by executing the following command: + +```bash +make test-go +``` + +**Note**: Root permission is required to run the unit tests. The tests are run on the loopback interface. + ### Test Packet Loss The packet loss function can be tested by running the following command: diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..5e65252 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,70 @@ +# BitTwister SDK for Go + +The BitTwister SDK for Go provides a convenient interface to interact with the BitTwister tool, which applies network restrictions on a network interface, including bandwidth limitation, packet loss, latency, and jitter. + +## Installation + +To use this SDK, import it into your Go project: + +```bash +go get -u github.com/celestiaorg/bittwister/sdk +``` + +## Usage + +### Initialization + +First, import the SDK into your Go project: + +```go +import "github.com/celestiaorg/bittwister/sdk" +``` + +Next, create a client by specifying the base URL of where BitTwister is running e.g. _a container running on the same machine_: + +```go +func main() { + baseURL := "http://localhost:9007/api/v1" + client := sdk.NewClient(baseURL) + // Use the client for API requests +} +``` + +### Examples + +Bandwidth Service + +Start the Bandwidth service with a specified network interface and bandwidth limit: + +```go +req := sdk.BandwidthStartRequest{ + NetworkInterfaceName: "eth0", + Limit: 100, +} + +err := client.BandwidthStart(req) +if err != nil { + // Handle error +} +``` + +Stop the Bandwidth service: + +```go +err := client.BandwidthStop() +if err != nil { + // Handle error +} +``` + +Retrieve the status of the Bandwidth service: + +```go +status, err := client.BandwidthStatus() +if err != nil { + // Handle error +} +// Use status for further processing +``` + +Similarly, you can use PacketlossStart, PacketlossStop, PacketlossStatus, LatencyStart, LatencyStop, LatencyStatus, and other functions provided by the SDK following similar usage patterns. diff --git a/sdk/apis.go b/sdk/apis.go new file mode 100644 index 0000000..577ffa3 --- /dev/null +++ b/sdk/apis.go @@ -0,0 +1,59 @@ +package sdk + +import ( + "encoding/json" + + "github.com/celestiaorg/bittwister/api/v1" +) + +func (c *Client) PacketlossStart(req api.PacketLossStartRequest) error { + _, err := c.postResource("/packetloss/start", req) + return err +} + +func (c *Client) PacketlossStop() error { + _, err := c.postResource("/packetloss/stop", nil) + return err +} + +func (c *Client) PacketlossStatus() (*api.MetaMessage, error) { + return c.getServiceStatus("/packetloss/status") +} + +func (c *Client) BandwidthStart(req api.BandwidthStartRequest) error { + _, err := c.postResource("/bandwidth/start", req) + return err +} + +func (c *Client) BandwidthStop() error { + _, err := c.postResource("/bandwidth/stop", nil) + return err +} + +func (c *Client) BandwidthStatus() (*api.MetaMessage, error) { + return c.getServiceStatus("/bandwidth/status") +} + +func (c *Client) LatencyStart(req api.LatencyStartRequest) error { + _, err := c.postResource("/latency/start", req) + return err +} + +func (c *Client) LatencyStop() error { + _, err := c.postResource("/latency/stop", nil) + return err +} + +func (c *Client) LatencyStatus() (*api.MetaMessage, error) { + return c.getServiceStatus("/latency/status") +} + +func (c *Client) AllServicesStatus() ([]api.ServiceStatus, error) { + resp, err := c.getResource("/services/status") + msgs := []api.ServiceStatus{} + + if err := json.Unmarshal(resp, &msgs); err != nil { + return nil, err + } + return msgs, err +} diff --git a/sdk/client.go b/sdk/client.go new file mode 100644 index 0000000..d3bf838 --- /dev/null +++ b/sdk/client.go @@ -0,0 +1,82 @@ +package sdk + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/celestiaorg/bittwister/api/v1" +) + +type Client struct { + baseURL string + httpClient *http.Client +} + +func NewClient(baseURL string) *Client { + return &Client{ + baseURL: baseURL, + httpClient: http.DefaultClient, + } +} + +func (c *Client) getResource(resPath string) ([]byte, error) { + resp, err := c.httpClient.Get(c.baseURL + resPath) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} + +func (c *Client) postResource(resPath string, requestBody interface{}) ([]byte, error) { + requestBodyJSON, err := json.Marshal(requestBody) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.baseURL+resPath, bytes.NewBuffer(requestBodyJSON)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} + +func (c *Client) getServiceStatus(resPath string) (*api.MetaMessage, error) { + resp, err := c.getResource(resPath) + msg := &api.MetaMessage{} + + if err := json.Unmarshal(resp, msg); err != nil { + return nil, err + } + return msg, err +} diff --git a/sdk/sdk_test.go b/sdk/sdk_test.go new file mode 100644 index 0000000..f67e513 --- /dev/null +++ b/sdk/sdk_test.go @@ -0,0 +1,367 @@ +package sdk + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/celestiaorg/bittwister/api/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_SDK_Client_GetResource_Success(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + respBody := []byte(`{"type": "info", "slug": "test", "title": "Test", "message": "Success"}`) + _, err := w.Write(respBody) + require.NoError(t, err) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + response, err := client.getResource("/test") + + require.NoError(t, err) + assert.NotNil(t, response) + + var message api.MetaMessage + err = json.Unmarshal(response, &message) + require.NoError(t, err) + assert.Equal(t, "info", message.Type) + assert.Equal(t, "test", message.Slug) + assert.Equal(t, "Test", message.Title) + assert.Equal(t, "Success", message.Message) +} + +func Test_SDK_Client_GetResource_HTTPError(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + res, err := client.getResource("/error") + assert.Nil(t, res) + assert.Error(t, err) +} + +func Test_SDK_Client_PostResource_Success(t *testing.T) { + testCases := []struct { + RequestBody interface{} + Expected api.MetaMessage + }{ + { + RequestBody: struct { + Type string `json:"type"` + Slug string `json:"slug"` + Title string `json:"title"` + Message string `json:"message"` + }{ + Type: "info", + Slug: "test", + Title: "Test", + Message: "Success", + }, + Expected: api.MetaMessage{ + Type: "info", + Slug: "test", + Title: "Test", + Message: "Success", + }, + }, + } + + for _, tc := range testCases { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + requestBodyJSON, err := json.Marshal(tc.Expected) + require.NoError(t, err) + _, err = w.Write(requestBodyJSON) + require.NoError(t, err) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + response, err := client.postResource("/test", tc.RequestBody) + + require.NoError(t, err) + assert.NotNil(t, response) + + var message api.MetaMessage + err = json.Unmarshal(response, &message) + require.NoError(t, err) + assert.Equal(t, tc.Expected, message) + } +} + +func Test_SDK_Client_PostResource_HTTPError(t *testing.T) { + testCases := []struct { + StatusCode int + }{ + {StatusCode: http.StatusNotFound}, + {StatusCode: http.StatusBadRequest}, + {StatusCode: http.StatusInternalServerError}, + } + + for _, tc := range testCases { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tc.StatusCode) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + res, err := client.postResource("/error", nil) + + assert.Nil(t, res) + assert.Error(t, err) + } +} + +func Test_SDK_Client_GetServiceStatus_Success(t *testing.T) { + expectedStatus := api.MetaMessage{ + Type: "info", + Slug: "service-ready", + Title: "Service Status", + Message: "Service is ready", + } + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + jsonBytes, err := json.Marshal(expectedStatus) + require.NoError(t, err) + + _, err = w.Write(jsonBytes) + require.NoError(t, err) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + status, err := client.getServiceStatus("/service/status") + + assert.NoError(t, err) + assert.NotNil(t, status) + assert.Equal(t, expectedStatus, *status) +} + +func Test_SDK_Client_GetServiceStatus_Error(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + status, err := client.getServiceStatus("/error/service/status") + + assert.Error(t, err) + assert.Nil(t, status) +} + +func Test_SDK_Client_PacketlossStart_Success(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + err := client.PacketlossStart(api.PacketLossStartRequest{}) + + assert.NoError(t, err) +} + +func Test_SDK_Client_PacketlossStop_Success(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + client := NewClient(mockServer.URL) + err := client.PacketlossStop() + assert.NoError(t, err) +} + +func Test_SDK_Client_PacketlossStatus_Success(t *testing.T) { + expectedStatus := api.MetaMessage{ + Type: "info", + Slug: "service-ready", + Title: "Packetloss Service", + Message: "Packetloss service is ready", + } + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + jsonBytes, err := json.Marshal(expectedStatus) + require.NoError(t, err) + + _, err = w.Write(jsonBytes) + require.NoError(t, err) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + status, err := client.PacketlossStatus() + + assert.NoError(t, err) + assert.Equal(t, expectedStatus, *status) +} + +func Test_SDK_Client_BandwidthStart_Success(t *testing.T) { + expectedRequest := api.BandwidthStartRequest{ + NetworkInterfaceName: "eth0", + Limit: 100, + } + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + err := client.BandwidthStart(expectedRequest) + + assert.NoError(t, err) +} + +func Test_SDK_Client_BandwidthStop_Success(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + err := client.BandwidthStop() + + assert.NoError(t, err) +} + +func Test_SDK_Client_BandwidthStatus_Success(t *testing.T) { + expectedStatus := api.MetaMessage{ + Type: "info", + Slug: "service-ready", + Title: "Bandwidth Service", + Message: "Bandwidth service is ready", + } + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + jsonBytes, err := json.Marshal(expectedStatus) + require.NoError(t, err) + + _, err = w.Write(jsonBytes) + require.NoError(t, err) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + status, err := client.BandwidthStatus() + + assert.NoError(t, err) + assert.Equal(t, expectedStatus, *status) +} + +func Test_SDK_Client_LatencyStart_Success(t *testing.T) { + expectedRequest := api.LatencyStartRequest{ + NetworkInterfaceName: "eth0", + Latency: 50, + Jitter: 10, + } + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + err := client.LatencyStart(expectedRequest) + + assert.NoError(t, err) +} + +func Test_SDK_Client_LatencyStop_Success(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + err := client.LatencyStop() + + assert.NoError(t, err) +} + +func Test_SDK_Client_LatencyStatus_Success(t *testing.T) { + expectedStatus := api.MetaMessage{ + Type: "info", + Slug: "service-ready", + Title: "Latency Service", + Message: "Latency service is ready", + } + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + jsonBytes, err := json.Marshal(expectedStatus) + require.NoError(t, err) + + _, err = w.Write(jsonBytes) + require.NoError(t, err) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + status, err := client.LatencyStatus() + + assert.NoError(t, err) + assert.Equal(t, expectedStatus, *status) +} + +func Test_SDK_Client_AllServicesStatus_Success(t *testing.T) { + expectedOutput := []api.ServiceStatus{{ + Name: "test-service", + Ready: true, + NetworkInterfaceName: "eth0", + Params: map[string]interface{}{"key": "value"}, + }} + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + jsonBytes, err := json.Marshal(expectedOutput) + require.NoError(t, err) + + _, err = w.Write(jsonBytes) + require.NoError(t, err) + })) + defer mockServer.Close() + + client := NewClient(mockServer.URL) + statuses, err := client.AllServicesStatus() + + assert.NoError(t, err) + assert.Equal(t, expectedOutput, statuses) +} + +func Test_SDK_Client_ServiceStatus_Unmarshal(t *testing.T) { + testCases := []struct { + Name string + Input string + Expected api.ServiceStatus + }{ + { + Name: "Valid service status", + Input: `{"name": "test-service", "ready": true, "network_interface_name": "eth0", "params": {"key": "value"}}`, + Expected: api.ServiceStatus{ + Name: "test-service", + Ready: true, + NetworkInterfaceName: "eth0", + Params: map[string]interface{}{"key": "value"}, + }, + }, + { + Name: "Empty service status", + Input: `{}`, + Expected: api.ServiceStatus{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + var result api.ServiceStatus + err := json.Unmarshal([]byte(tc.Input), &result) + + assert.NoError(t, err) + assert.Equal(t, tc.Expected, result) + }) + } +}