waveterm/pkg/wcloud/wcloud.go

200 lines
5.5 KiB
Go

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wcloud
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/daystr"
"github.com/wavetermdev/waveterm/pkg/wavebase"
)
const WCloudEndpoint = "https://api.waveterm.dev/central"
const WCloudEndpointVarName = "WCLOUD_ENDPOINT"
const WCloudWSEndpoint = "wss://wsapi.waveterm.dev/"
const WCloudWSEndpointVarName = "WCLOUD_WS_ENDPOINT"
var WCloudWSEndpoint_VarCache string
var WCloudEndpoint_VarCache string
const APIVersion = 1
const MaxPtyUpdateSize = (128 * 1024)
const MaxUpdatesPerReq = 10
const MaxUpdatesToDeDup = 1000
const MaxUpdateWriterErrors = 3
const WCloudDefaultTimeout = 5 * time.Second
const WCloudWebShareUpdateTimeout = 15 * time.Second
// setting to 1M to be safe (max is 6M for API-GW + Lambda, but there is base64 encoding and upload time)
// we allow one extra update past this estimated size
const MaxUpdatePayloadSize = 1 * (1024 * 1024)
const TelemetryUrl = "/telemetry"
const NoTelemetryUrl = "/no-telemetry"
const WebShareUpdateUrl = "/auth/web-share-update"
func CacheAndRemoveEnvVars() error {
WCloudEndpoint_VarCache = os.Getenv(WCloudEndpointVarName)
err := checkEndpointVar(WCloudEndpoint_VarCache, "wcloud endpoint", WCloudEndpointVarName)
if err != nil {
return err
}
os.Unsetenv(WCloudEndpointVarName)
WCloudWSEndpoint_VarCache = os.Getenv(WCloudWSEndpointVarName)
err = checkWSEndpointVar(WCloudWSEndpoint_VarCache, "wcloud ws endpoint", WCloudWSEndpointVarName)
if err != nil {
return err
}
os.Unsetenv(WCloudWSEndpointVarName)
return nil
}
func checkEndpointVar(endpoint string, debugName string, varName string) error {
if !wavebase.IsDevMode() {
return nil
}
if endpoint == "" || !strings.HasPrefix(endpoint, "https://") {
return fmt.Errorf("invalid %s, %s not set or invalid", debugName, varName)
}
return nil
}
func checkWSEndpointVar(endpoint string, debugName string, varName string) error {
if !wavebase.IsDevMode() {
return nil
}
log.Printf("checking endpoint %q\n", endpoint)
if endpoint == "" || !strings.HasPrefix(endpoint, "wss://") {
return fmt.Errorf("invalid %s, %s not set or invalid", debugName, varName)
}
return nil
}
func GetEndpoint() string {
if !wavebase.IsDevMode() {
return WCloudEndpoint
}
endpoint := WCloudEndpoint_VarCache
return endpoint
}
func GetWSEndpoint() string {
if !wavebase.IsDevMode() {
return WCloudWSEndpoint
}
endpoint := WCloudWSEndpoint_VarCache
return endpoint
}
func makeAnonPostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) {
endpoint := GetEndpoint()
if endpoint == "" {
return nil, errors.New("wcloud endpoint not set")
}
var dataReader io.Reader
if data != nil {
byteArr, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("error marshaling json for %s request: %v", apiUrl, err)
}
dataReader = bytes.NewReader(byteArr)
}
fullUrl := GetEndpoint() + apiUrl
req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, dataReader)
if err != nil {
return nil, fmt.Errorf("error creating %s request: %v", apiUrl, err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-PromptAPIVersion", strconv.Itoa(APIVersion))
req.Header.Set("X-PromptAPIUrl", apiUrl)
req.Close = true
return req, nil
}
func doRequest(req *http.Request, outputObj interface{}) (*http.Response, error) {
apiUrl := req.Header.Get("X-PromptAPIUrl")
log.Printf("[wcloud] sending request %s %v\n", req.Method, req.URL)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error contacting wcloud %q service: %v", apiUrl, err)
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return resp, fmt.Errorf("error reading %q response body: %v", apiUrl, err)
}
if resp.StatusCode != http.StatusOK {
return resp, fmt.Errorf("error contacting wcloud %q service: %s", apiUrl, resp.Status)
}
if outputObj != nil && resp.Header.Get("Content-Type") == "application/json" {
err = json.Unmarshal(bodyBytes, outputObj)
if err != nil {
return resp, fmt.Errorf("error decoding json: %v", err)
}
}
return resp, nil
}
func SendTelemetry(ctx context.Context, clientId string) error {
if !telemetry.IsTelemetryEnabled() {
log.Printf("telemetry disabled, not sending\n")
return nil
}
activity, err := telemetry.GetNonUploadedActivity(ctx)
if err != nil {
return fmt.Errorf("cannot get activity: %v", err)
}
if len(activity) == 0 {
return nil
}
log.Printf("[wcloud] sending telemetry data\n")
dayStr := daystr.GetCurDayStr()
input := TelemetryInputType{
ClientId: clientId,
UserId: clientId,
AppType: "w2",
AutoUpdateEnabled: telemetry.IsAutoUpdateEnabled(),
AutoUpdateChannel: telemetry.AutoUpdateChannel(),
CurDay: dayStr,
Activity: activity,
}
req, err := makeAnonPostReq(ctx, TelemetryUrl, input)
if err != nil {
return err
}
_, err = doRequest(req, nil)
if err != nil {
return err
}
err = telemetry.MarkActivityAsUploaded(ctx, activity)
if err != nil {
return fmt.Errorf("error marking activity as uploaded: %v", err)
}
return nil
}
func SendNoTelemetryUpdate(ctx context.Context, clientId string, noTelemetryVal bool) error {
req, err := makeAnonPostReq(ctx, NoTelemetryUrl, NoTelemetryInputType{ClientId: clientId, Value: noTelemetryVal})
if err != nil {
return err
}
_, err = doRequest(req, nil)
if err != nil {
return err
}
return nil
}