-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8bb01d9
commit 790253c
Showing
25 changed files
with
1,293 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,3 +22,4 @@ go.work | |
bin/ | ||
*.o | ||
run.sh | ||
.vscode/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package api | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/gorilla/handlers" | ||
"github.com/gorilla/mux" | ||
"go.uber.org/zap" | ||
) | ||
|
||
func path(endpoint string) string { | ||
return fmt.Sprintf("/api/v1%s", endpoint) | ||
} | ||
|
||
func NewRESTApiV1(productionMode bool, logger *zap.Logger) *RESTApiV1 { | ||
restAPI := &RESTApiV1{ | ||
router: mux.NewRouter(), | ||
logger: logger, | ||
loggerNoStack: logger.WithOptions(zap.AddStacktrace(zap.DPanicLevel)), | ||
productionMode: productionMode, | ||
} | ||
|
||
restAPI.router.HandleFunc("/", restAPI.IndexPage).Methods(http.MethodGet, http.MethodPost, http.MethodOptions, http.MethodPut, http.MethodHead) | ||
|
||
restAPI.router.HandleFunc(path("/packetloss/start"), restAPI.PacketlossStart).Methods(http.MethodPost) | ||
restAPI.router.HandleFunc(path("/packetloss/status"), restAPI.PacketlossStatus).Methods(http.MethodGet) | ||
restAPI.router.HandleFunc(path("/packetloss/stop"), restAPI.PacketlossStop).Methods(http.MethodPost) | ||
|
||
restAPI.router.HandleFunc(path("/bandwidth/start"), restAPI.BandwidthStart).Methods(http.MethodPost) | ||
restAPI.router.HandleFunc(path("/bandwidth/status"), restAPI.BandwidthStatus).Methods(http.MethodGet) | ||
restAPI.router.HandleFunc(path("/bandwidth/stop"), restAPI.BandwidthStop).Methods(http.MethodPost) | ||
|
||
restAPI.router.HandleFunc(path("/latency/start"), restAPI.LatencyStart).Methods(http.MethodPost) | ||
restAPI.router.HandleFunc(path("/latency/status"), restAPI.LatencyStatus).Methods(http.MethodGet) | ||
restAPI.router.HandleFunc(path("/latency/stop"), restAPI.LatencyStop).Methods(http.MethodPost) | ||
|
||
restAPI.router.HandleFunc(path("/services/status"), restAPI.NetServicesStatus).Methods(http.MethodGet) | ||
|
||
return restAPI | ||
} | ||
|
||
func (a *RESTApiV1) Serve(addr, originAllowed string) error { | ||
http.Handle("/", a.router) | ||
|
||
headersOk := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", "X-CSRF-Token"}) | ||
originsOk := handlers.AllowedOrigins([]string{originAllowed}) | ||
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"}) | ||
|
||
a.logger.Info(fmt.Sprintf("serving on %s", addr)) | ||
|
||
a.server = &http.Server{ | ||
Addr: addr, | ||
Handler: handlers.CORS(originsOk, headersOk, methodsOk)(a.router), | ||
} | ||
|
||
return a.server.ListenAndServe() | ||
} | ||
|
||
func (a *RESTApiV1) Shutdown() error { | ||
if a.server == nil { | ||
return errors.New("server is not running") | ||
} | ||
return a.server.Shutdown(context.Background()) | ||
} | ||
|
||
func (a *RESTApiV1) GetAllAPIs() []string { | ||
list := []string{} | ||
err := a.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { | ||
apiPath, err := route.GetPathTemplate() | ||
if err == nil { | ||
list = append(list, apiPath) | ||
} | ||
return err | ||
}) | ||
if err != nil { | ||
a.logger.Error("error while getting all APIs", zap.Error(err)) | ||
} | ||
|
||
return list | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package api | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
|
||
"github.com/celestiaorg/bittwister/xdp/bandwidth" | ||
"go.uber.org/zap" | ||
) | ||
|
||
// BandwidthStart implements POST /bandwidth/start | ||
func (a *RESTApiV1) BandwidthStart(resp http.ResponseWriter, req *http.Request) { | ||
var body BandwidthStartRequest | ||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil { | ||
sendJSONError(resp, | ||
MetaMessage{ | ||
Type: APIMetaMessageTypeError, | ||
Slug: SlugJSONDecodeFailed, | ||
Title: "JSON decode failed", | ||
Message: err.Error(), | ||
}, | ||
http.StatusBadRequest) | ||
return | ||
} | ||
|
||
if a.bw == nil { | ||
a.bw = &netRestrictService{ | ||
service: &bandwidth.Bandwidth{ | ||
Limit: body.Limit, | ||
}, | ||
logger: a.logger, | ||
} | ||
} else { | ||
bw, ok := a.bw.service.(*bandwidth.Bandwidth) | ||
if !ok { | ||
sendJSONError(resp, | ||
MetaMessage{ | ||
Type: APIMetaMessageTypeError, | ||
Slug: SlugTypeError, | ||
Title: "Type cast error", | ||
Message: "could not cast netRestrictService.service to *packetloss.PacketLoss", | ||
}, | ||
http.StatusInternalServerError) | ||
return | ||
} | ||
bw.Limit = body.Limit | ||
} | ||
|
||
err := netServiceStart(resp, a.bw, body.NetworkInterfaceName) | ||
if err != nil { | ||
a.loggerNoStack.Error("netServiceStart failed", zap.Error(err)) | ||
} | ||
} | ||
|
||
// BandwidthStop implements POST /bandwidth/stop | ||
func (a *RESTApiV1) BandwidthStop(resp http.ResponseWriter, req *http.Request) { | ||
if err := netServiceStop(resp, a.bw); err != nil { | ||
a.loggerNoStack.Error("netServiceStop failed", zap.Error(err)) | ||
} | ||
} | ||
|
||
// BandwidthStatus implements GET /bandwidth/status | ||
func (a *RESTApiV1) BandwidthStatus(resp http.ResponseWriter, _ *http.Request) { | ||
if err := netServiceStatus(resp, a.bw); err != nil { | ||
a.loggerNoStack.Error("netServiceStatus failed", zap.Error(err)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package api_test | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
|
||
api "github.com/celestiaorg/bittwister/api/v1" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func (s *APITestSuite) TestBandwidthStart() { | ||
t := s.T() | ||
|
||
reqBody := api.BandwidthStartRequest{ | ||
NetworkInterfaceName: s.ifaceName, | ||
Limit: 100, | ||
} | ||
jsonBody, err := json.Marshal(reqBody) | ||
require.NoError(t, err) | ||
|
||
req, err := http.NewRequest(http.MethodPost, "/api/v1/bandwidth/start", bytes.NewReader(jsonBody)) | ||
require.NoError(t, err) | ||
|
||
rr := httptest.NewRecorder() | ||
s.restAPI.BandwidthStart(rr, req) | ||
// need to stop it to release the network interface for other tests | ||
defer s.restAPI.BandwidthStop(rr, nil) | ||
|
||
assert.Equal(t, http.StatusOK, rr.Code) | ||
} | ||
|
||
func (s *APITestSuite) TestBandwidthStop() { | ||
t := s.T() | ||
|
||
reqBody := api.BandwidthStartRequest{ | ||
NetworkInterfaceName: s.ifaceName, | ||
Limit: 100, | ||
} | ||
jsonBody, err := json.Marshal(reqBody) | ||
require.NoError(t, err) | ||
|
||
req, err := http.NewRequest(http.MethodPost, "/api/v1/bandwidth/start", bytes.NewReader(jsonBody)) | ||
require.NoError(t, err) | ||
|
||
rr := httptest.NewRecorder() | ||
s.restAPI.BandwidthStart(rr, req) | ||
|
||
require.NoError(t, waitForService(s.restAPI.BandwidthStatus)) | ||
|
||
rr = httptest.NewRecorder() | ||
s.restAPI.BandwidthStop(rr, nil) | ||
require.Equal(t, http.StatusOK, rr.Code) | ||
|
||
slug, err := getServiceStatusSlug(s.restAPI.BandwidthStatus) | ||
require.NoError(t, err) | ||
assert.Equal(t, api.SlugServiceNotReady, slug) | ||
} | ||
|
||
func (s *APITestSuite) TestBandwidthStatus() { | ||
t := s.T() | ||
|
||
slug, err := getServiceStatusSlug(s.restAPI.BandwidthStatus) | ||
require.NoError(t, err) | ||
if slug != api.SlugServiceNotReady && slug != api.SlugServiceNotInitialized { | ||
t.Fatalf("unexpected service status: %s", slug) | ||
} | ||
|
||
reqBody := api.BandwidthStartRequest{ | ||
NetworkInterfaceName: s.ifaceName, | ||
Limit: 100, | ||
} | ||
jsonBody, err := json.Marshal(reqBody) | ||
require.NoError(t, err) | ||
|
||
req, err := http.NewRequest(http.MethodPost, "/api/v1/bandwidth/start", bytes.NewReader(jsonBody)) | ||
require.NoError(t, err) | ||
|
||
rr := httptest.NewRecorder() | ||
s.restAPI.BandwidthStart(rr, req) | ||
|
||
require.NoError(t, waitForService(s.restAPI.BandwidthStatus)) | ||
|
||
slug, err = getServiceStatusSlug(s.restAPI.BandwidthStatus) | ||
require.NoError(t, err) | ||
assert.Equal(t, api.SlugServiceReady, slug) | ||
|
||
s.restAPI.BandwidthStop(rr, nil) | ||
require.Equal(t, http.StatusOK, rr.Code) | ||
|
||
slug, err = getServiceStatusSlug(s.restAPI.BandwidthStatus) | ||
require.NoError(t, err) | ||
assert.Equal(t, api.SlugServiceNotReady, slug) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package api | ||
|
||
import ( | ||
"errors" | ||
) | ||
|
||
const ( | ||
APIMetaMessageTypeInfo = "info" | ||
APIMetaMessageTypeWarning = "warning" | ||
APIMetaMessageTypeError = "error" | ||
) | ||
|
||
const ( | ||
SlugServiceAlreadyStarted = "service-already-started" | ||
SlugServiceStartFailed = "service-start-failed" | ||
SlugServiceStopFailed = "service-stop-failed" | ||
SlugServiceNotStarted = "service-not-started" | ||
SlugServiceNotInitialized = "service-not-initialized" | ||
SlugServiceReady = "service-ready" | ||
SlugServiceNotReady = "service-not-ready" | ||
SlugJSONDecodeFailed = "json-decode-failed" | ||
SlugTypeError = "type-error" | ||
) | ||
|
||
type MetaMessage struct { | ||
Type string `json:"type"` // info, warning, error | ||
Slug string `json:"slug"` | ||
Title string `json:"title"` | ||
Message string `json:"message"` | ||
} | ||
|
||
var ( | ||
ErrServiceNotInitialized = errors.New(SlugServiceNotInitialized) | ||
ErrServiceAlreadyStarted = errors.New(SlugServiceAlreadyStarted) | ||
ErrServiceNotStarted = errors.New(SlugServiceNotStarted) | ||
ErrServiceStopFailed = errors.New(SlugServiceStopFailed) | ||
ErrServiceStartFailed = errors.New(SlugServiceStartFailed) | ||
) | ||
|
||
// convert a ApiMetaMessage to map[string]interface{} | ||
func (m MetaMessage) ToMap() map[string]interface{} { | ||
return map[string]interface{}{ | ||
"type": m.Type, | ||
"slug": m.Slug, | ||
"title": m.Title, | ||
"message": m.Message, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package api | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"runtime/debug" | ||
"strings" | ||
) | ||
|
||
// IndexPage implements GET / | ||
func (a *RESTApiV1) IndexPage(resp http.ResponseWriter, _ *http.Request) { | ||
if a.productionMode { | ||
return | ||
} | ||
|
||
modName := "unknown" | ||
buildInfo := "" | ||
if bi, ok := debug.ReadBuildInfo(); ok { | ||
modName = bi.Path | ||
|
||
buildInfo += "<br /><h3>Build Info:</h3><table>" | ||
for _, s := range bi.Settings { | ||
buildInfo += fmt.Sprintf("<tr><td>%s</td><td>%s</td></tr>", s.Key, s.Value) | ||
} | ||
buildInfo += "</table>" | ||
} | ||
|
||
html := `<!DOCTYPE html><html><head><style> | ||
table {border-collapse: collapse; width: 100%;} | ||
td, th {border: 1px solid #222;text-align: left; padding: 8px;} | ||
tr:nth-child(even) {background-color: #222;} | ||
a { | ||
text-decoration:none;border-bottom: 2px solid #10747f; | ||
color: #f1ff8f;transition: background 0.1s cubic-bezier(.33,.66,.66,1); | ||
} | ||
a:hover {background: #10747f;} | ||
body { | ||
color: #FFF; font-family: sans-serif; | ||
justify-content: center;align-items: center; | ||
line-height:1.8;margin:0;padding:0 40px; | ||
background-image: linear-gradient(135deg, rgba(0, 0, 0, 0.85) 0%,rgba(0, 0, 0,1) 100%); | ||
} | ||
</style></head><body>` | ||
|
||
html += fmt.Sprintf("Ciao, this is `%v` \n\n<p>", modName) | ||
allAPIs := a.GetAllAPIs() | ||
html += "<h3>List of endpoints:</h3>" | ||
for _, a := range allAPIs { | ||
|
||
href := strings.TrimPrefix(a, "/") // it fixes the links if the service is running under a path | ||
html += fmt.Sprintf(`<a href="%s">%s</a><br />`, href, a) | ||
} | ||
|
||
html += fmt.Sprintf("<br />Production Mode: %v", os.Getenv("PRODUCTION_MODE")) | ||
html += buildInfo | ||
|
||
resp.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
_, err := resp.Write([]byte(html)) | ||
if err != nil { | ||
a.logger.Error(fmt.Sprintf("api `IndexPage`: %v", err)) | ||
} | ||
} |
Oops, something went wrong.