From 2c5123ec6cd960313c7aaf416c33398400af8072 Mon Sep 17 00:00:00 2001 From: winlin Date: Mon, 15 Jan 2024 17:34:12 +0800 Subject: [PATCH] LiveRoom: Support live room secret with stream URL. v5.13.11 --- DEVELOPER.md | 5 + platform/live-room.go | 193 +++++++++ platform/service.go | 4 + platform/srs-hooks.go | 38 +- platform/utils.go | 2 + test/go.mod | 1 + test/go.sum | 2 + test/liveroom_test.go | 369 ++++++++++++++++++ .../github.com/google/uuid/CHANGELOG.md | 28 ++ .../github.com/google/uuid/CONTRIBUTING.md | 26 ++ .../github.com/google/uuid/CONTRIBUTORS | 9 + test/vendor/github.com/google/uuid/LICENSE | 27 ++ test/vendor/github.com/google/uuid/README.md | 21 + test/vendor/github.com/google/uuid/dce.go | 80 ++++ test/vendor/github.com/google/uuid/doc.go | 12 + test/vendor/github.com/google/uuid/go.mod | 1 + test/vendor/github.com/google/uuid/hash.go | 53 +++ test/vendor/github.com/google/uuid/marshal.go | 38 ++ test/vendor/github.com/google/uuid/node.go | 90 +++++ test/vendor/github.com/google/uuid/node_js.go | 12 + .../vendor/github.com/google/uuid/node_net.go | 33 ++ test/vendor/github.com/google/uuid/null.go | 118 ++++++ test/vendor/github.com/google/uuid/sql.go | 59 +++ test/vendor/github.com/google/uuid/time.go | 134 +++++++ test/vendor/github.com/google/uuid/util.go | 43 ++ test/vendor/github.com/google/uuid/uuid.go | 365 +++++++++++++++++ .../vendor/github.com/google/uuid/version1.go | 44 +++ .../vendor/github.com/google/uuid/version4.go | 76 ++++ .../vendor/github.com/google/uuid/version6.go | 56 +++ .../vendor/github.com/google/uuid/version7.go | 75 ++++ test/vendor/modules.txt | 3 + ui/src/components/UrlGenerator.js | 8 +- ui/src/pages/Scenario.js | 12 +- ui/src/pages/ScenarioLive.js | 2 +- ui/src/pages/ScenarioLiveRoom.js | 273 +++++++++++++ ui/src/pages/Settings.js | 2 +- ui/src/resources/locale.json | 32 +- 37 files changed, 2330 insertions(+), 16 deletions(-) create mode 100644 platform/live-room.go create mode 100644 test/liveroom_test.go create mode 100644 test/vendor/github.com/google/uuid/CHANGELOG.md create mode 100644 test/vendor/github.com/google/uuid/CONTRIBUTING.md create mode 100644 test/vendor/github.com/google/uuid/CONTRIBUTORS create mode 100644 test/vendor/github.com/google/uuid/LICENSE create mode 100644 test/vendor/github.com/google/uuid/README.md create mode 100644 test/vendor/github.com/google/uuid/dce.go create mode 100644 test/vendor/github.com/google/uuid/doc.go create mode 100644 test/vendor/github.com/google/uuid/go.mod create mode 100644 test/vendor/github.com/google/uuid/hash.go create mode 100644 test/vendor/github.com/google/uuid/marshal.go create mode 100644 test/vendor/github.com/google/uuid/node.go create mode 100644 test/vendor/github.com/google/uuid/node_js.go create mode 100644 test/vendor/github.com/google/uuid/node_net.go create mode 100644 test/vendor/github.com/google/uuid/null.go create mode 100644 test/vendor/github.com/google/uuid/sql.go create mode 100644 test/vendor/github.com/google/uuid/time.go create mode 100644 test/vendor/github.com/google/uuid/util.go create mode 100644 test/vendor/github.com/google/uuid/uuid.go create mode 100644 test/vendor/github.com/google/uuid/version1.go create mode 100644 test/vendor/github.com/google/uuid/version4.go create mode 100644 test/vendor/github.com/google/uuid/version6.go create mode 100644 test/vendor/github.com/google/uuid/version7.go create mode 100644 ui/src/pages/ScenarioLiveRoom.js diff --git a/DEVELOPER.md b/DEVELOPER.md index d4c7bad3..0b391602 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -906,6 +906,10 @@ Platform, with token authentication: * `/terraform/v1/hooks/record/remove` Hooks: Remove the Record files. * `/terraform/v1/hooks/record/end` Record: As stream is unpublished, finish the record task quickly. * `/terraform/v1/hooks/record/files` Hooks: List the Record files. +* `/terraform/v1/live/room/create` Live: Create a new live room. +* `/terraform/v1/live/room/query` Live: Query a new live room. +* `/terraform/v1/live/room/remove`: Live: Remove a live room. +* `/terraform/v1/live/room/list` Live: List all available live rooms. * `/terraform/v1/ffmpeg/forward/secret` FFmpeg: Setup the forward secret to live streaming platforms. * `/terraform/v1/ffmpeg/forward/streams` FFmpeg: Query the forwarding streams. * `/terraform/v1/ffmpeg/vlive/secret` Setup the Virtual Live streaming secret. @@ -1068,6 +1072,7 @@ The following are the update records for the SRS Stack server. * RTSP: Rebuild the URL with escaped user info. v5.13.8 * VLive: Support SRT URL filter. [v5.13.9](https://github.com/ossrs/srs-stack/releases/tag/v5.13.9) * FFmpeg: Monitor and restart FFmpeg if stuck. v5.13.10 + * LiveRoom: Support live room secret with stream URL. v5.13.11 * v5.12 * Refine local variable name conf to config. v5.12.1 * Add forced exit on timeout for program termination. v5.12.1 diff --git a/platform/live-room.go b/platform/live-room.go new file mode 100644 index 00000000..9cbb17a9 --- /dev/null +++ b/platform/live-room.go @@ -0,0 +1,193 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-redis/redis/v8" + "github.com/google/uuid" + "github.com/ossrs/go-oryx-lib/errors" + ohttp "github.com/ossrs/go-oryx-lib/http" + "github.com/ossrs/go-oryx-lib/logger" + "net/http" + "os" + "strings" + "time" +) + +func handleLiveRoomService(ctx context.Context, handler *http.ServeMux) error { + ep := "/terraform/v1/live/room/create" + logger.Tf(ctx, "Handle %v", ep) + handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { + if err := func() error { + var token, title string + if err := ParseBody(ctx, r.Body, &struct { + Token *string `json:"token"` + Title *string `json:"title"` + }{ + Token: &token, Title: &title, + }); err != nil { + return errors.Wrapf(err, "parse body") + } + + apiSecret := os.Getenv("SRS_PLATFORM_SECRET") + if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil { + return errors.Wrapf(err, "authenticate") + } + + room := &SrsLiveRoom{ + UUID: uuid.NewString(), + // The title of live room. + Title: title, + // The secret of live room. + Secret: strings.ReplaceAll(uuid.NewString(), "-", ""), + // Create time. + CreatedAt: time.Now().Format(time.RFC3339), + } + if b, err := json.Marshal(room); err != nil { + return errors.Wrapf(err, "marshal room") + } else if err := rdb.HSet(ctx, SRS_LIVE_ROOM, room.UUID, string(b)).Err(); err != nil { + return errors.Wrapf(err, "hset %v %v %v", SRS_LIVE_ROOM, room.UUID, string(b)) + } + + ohttp.WriteData(ctx, w, r, &room) + logger.Tf(ctx, "srs live room create ok, title=%v, room=%v", title, room.String()) + return nil + }(); err != nil { + ohttp.WriteError(ctx, w, r, err) + } + }) + + ep = "/terraform/v1/live/room/query" + logger.Tf(ctx, "Handle %v", ep) + handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { + if err := func() error { + var token, uuid string + if err := ParseBody(ctx, r.Body, &struct { + Token *string `json:"token"` + UUID *string `json:"uuid"` + }{ + Token: &token, UUID: &uuid, + }); err != nil { + return errors.Wrapf(err, "parse body") + } + + apiSecret := os.Getenv("SRS_PLATFORM_SECRET") + if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil { + return errors.Wrapf(err, "authenticate") + } + + var room SrsLiveRoom + if r0, err := rdb.HGet(ctx, SRS_LIVE_ROOM, uuid).Result(); err != nil && err != redis.Nil { + return errors.Wrapf(err, "hget %v %v", SRS_LIVE_ROOM, uuid) + } else if r0 == "" { + return errors.Errorf("live room %v not exists", uuid) + } else if err = json.Unmarshal([]byte(r0), &room); err != nil { + return errors.Wrapf(err, "unmarshal %v %v", uuid, r0) + } + + ohttp.WriteData(ctx, w, r, &room) + logger.Tf(ctx, "srs live room query ok, uuid=%v, room=%v", uuid, room.String()) + return nil + }(); err != nil { + ohttp.WriteError(ctx, w, r, err) + } + }) + + ep = "/terraform/v1/live/room/list" + logger.Tf(ctx, "Handle %v", ep) + handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { + if err := func() error { + var token string + if err := ParseBody(ctx, r.Body, &struct { + Token *string `json:"token"` + }{ + Token: &token, + }); err != nil { + return errors.Wrapf(err, "parse body") + } + + apiSecret := os.Getenv("SRS_PLATFORM_SECRET") + if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil { + return errors.Wrapf(err, "authenticate") + } + + var rooms []*SrsLiveRoom + if configs, err := rdb.HGetAll(ctx, SRS_LIVE_ROOM).Result(); err != nil && err != redis.Nil { + return errors.Wrapf(err, "hgetall %v", SRS_LIVE_ROOM) + } else { + for k, v := range configs { + var obj SrsLiveRoom + if err = json.Unmarshal([]byte(v), &obj); err != nil { + return errors.Wrapf(err, "unmarshal %v %v", k, v) + } + rooms = append(rooms, &obj) + } + } + + ohttp.WriteData(ctx, w, r, &struct { + Rooms []*SrsLiveRoom `json:"rooms"` + }{ + Rooms: rooms, + }) + logger.Tf(ctx, "srs live room create ok, rooms=%v", len(rooms)) + return nil + }(); err != nil { + ohttp.WriteError(ctx, w, r, err) + } + }) + + ep = "/terraform/v1/live/room/remove" + logger.Tf(ctx, "Handle %v", ep) + handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { + if err := func() error { + var token, uuid string + if err := ParseBody(ctx, r.Body, &struct { + Token *string `json:"token"` + UUID *string `json:"uuid"` + }{ + Token: &token, UUID: &uuid, + }); err != nil { + return errors.Wrapf(err, "parse body") + } + + apiSecret := os.Getenv("SRS_PLATFORM_SECRET") + if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil { + return errors.Wrapf(err, "authenticate") + } + + if r0, err := rdb.HGet(ctx, SRS_LIVE_ROOM, uuid).Result(); err != nil && err != redis.Nil { + return errors.Wrapf(err, "hget %v %v", SRS_LIVE_ROOM, uuid) + } else if r0 == "" { + return errors.Errorf("live room %v not exists", uuid) + } + + if err := rdb.HDel(ctx, SRS_LIVE_ROOM, uuid).Err(); err != nil && err != redis.Nil { + return errors.Wrapf(err, "hdel %v %v", SRS_LIVE_ROOM, uuid) + } + + ohttp.WriteData(ctx, w, r, nil) + logger.Tf(ctx, "srs remove room ok, uuid=%v", uuid) + return nil + }(); err != nil { + ohttp.WriteError(ctx, w, r, err) + } + }) + + return nil +} + +type SrsLiveRoom struct { + // Live room UUID. + UUID string `json:"uuid"` + // Live room title. + Title string `json:"title"` + // Live room secret. + Secret string `json:"secret"` + // Create time. + CreatedAt string `json:"created_at"` +} + +func (v *SrsLiveRoom) String() string { + return fmt.Sprintf("uuid=%v, title=%v, secret=%v", v.UUID, v.Title, v.Secret) +} diff --git a/platform/service.go b/platform/service.go index 8bed034b..07e9e02a 100644 --- a/platform/service.go +++ b/platform/service.go @@ -222,6 +222,10 @@ func handleHTTPService(ctx context.Context, handler *http.ServeMux) error { return errors.Wrapf(err, "handle hooks") } + if err := handleLiveRoomService(ctx, handler); err != nil { + return errors.Wrapf(err, "handle live room") + } + var ep string handleHostVersions(ctx, handler) diff --git a/platform/srs-hooks.go b/platform/srs-hooks.go index 4633690d..b66eaed3 100644 --- a/platform/srs-hooks.go +++ b/platform/srs-hooks.go @@ -96,12 +96,8 @@ func handleHooksService(ctx context.Context, handler *http.ServeMux) error { return errors.Wrapf(err, "json unmarshal %v", string(b)) } + verifiedBy := "noVerify" if action == SrsActionOnPublish { - publish, err := rdb.HGet(ctx, SRS_AUTH_SECRET, "pubSecret").Result() - if err != nil && err != redis.Nil { - return errors.Wrapf(err, "hget %v pubSecret", SRS_AUTH_SECRET) - } - // Note that we allow pass secret by params or in stream name, for example, some encoder does not support params // with ?secret=xxx, so it will fail when url is: // rtmp://ip/live/livestream?secret=xxx @@ -110,8 +106,33 @@ func handleHooksService(ctx context.Context, handler *http.ServeMux) error { // or simply use secret as stream: // rtmp://ip/live/xxx // in this situation, the secret is part of stream name. - if publish != "" && !strings.Contains(streamObj.Param, publish) && !strings.Contains(streamObj.Stream, publish) { - return errors.Errorf("invalid stream=%v, param=%v, action=%v", streamObj.Stream, streamObj.Param, action) + isSecretOK := func(publish, stream, param string) bool { + return publish == "" || strings.Contains(param, publish) || strings.Contains(stream, publish) + } + + // Use live room secret to verify if stream name matches. + if r0, err := rdb.HGet(ctx, SRS_LIVE_ROOM, streamObj.Stream).Result(); (err == nil || err == redis.Nil) && r0 != "" { + var room SrsLiveRoom + if err = json.Unmarshal([]byte(r0), &room); err != nil { + return errors.Wrapf(err, "unmarshal %v %v", streamObj.Stream, r0) + } + + if !isSecretOK(room.Secret, streamObj.Stream, streamObj.Param) { + return errors.Errorf("invalid live room stream=%v, param=%v, action=%v", streamObj.Stream, streamObj.Param, action) + } + + verifiedBy = "LiveRoom" + } else { + // Use global publish secret to verify + publish, err := rdb.HGet(ctx, SRS_AUTH_SECRET, "pubSecret").Result() + if err != nil && err != redis.Nil { + return errors.Wrapf(err, "hget %v pubSecret", SRS_AUTH_SECRET) + } + if !isSecretOK(publish, streamObj.Stream, streamObj.Param) { + return errors.Errorf("invalid normal stream=%v, param=%v, action=%v", streamObj.Stream, streamObj.Param, action) + } + + verifiedBy = "NormalStream" } } @@ -176,7 +197,8 @@ func handleHooksService(ctx context.Context, handler *http.ServeMux) error { } ohttp.WriteData(ctx, w, r, nil) - logger.Tf(ctx, "srs hooks ok, action=%v, %v, %v", action, streamObj.String(), requestBody) + logger.Tf(ctx, "srs hooks ok, action=%v, verifiedBy=%v, %v, %v", + action, verifiedBy, streamObj.String(), requestBody) return nil }(); err != nil { ohttp.WriteError(ctx, w, r, err) diff --git a/platform/utils.go b/platform/utils.go index 855f5261..9bbceae6 100644 --- a/platform/utils.go +++ b/platform/utils.go @@ -292,6 +292,8 @@ const ( SRS_STAT_COUNTER = "SRS_STAT_COUNTER" // For container and images. SRS_CONTAINER_DISABLED = "SRS_CONTAINER_DISABLED" + // For live stream and rooms. + SRS_LIVE_ROOM = "SRS_LIVE_ROOM" // For system settings. SRS_LOCALE = "SRS_LOCALE" SRS_SECRET_PUBLISH = "SRS_SECRET_PUBLISH" diff --git a/test/go.mod b/test/go.mod index 2a502004..08b7796d 100644 --- a/test/go.mod +++ b/test/go.mod @@ -3,6 +3,7 @@ module test go 1.16 require ( + github.com/google/uuid v1.5.0 // indirect github.com/joho/godotenv v1.5.1 github.com/ossrs/go-oryx-lib v0.0.9 ) diff --git a/test/go.sum b/test/go.sum index 73c79a69..184d00da 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,3 +1,5 @@ +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/ossrs/go-oryx-lib v0.0.9 h1:piZkzit/1hqAcXP31/mvDEDpHVjCmBMmvzF3hN8hUuQ= diff --git a/test/liveroom_test.go b/test/liveroom_test.go new file mode 100644 index 00000000..7fea01b9 --- /dev/null +++ b/test/liveroom_test.go @@ -0,0 +1,369 @@ +package main + +import ( + "context" + "fmt" + "math/rand" + "os" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/ossrs/go-oryx-lib/errors" + "github.com/ossrs/go-oryx-lib/logger" +) + +func TestMedia_WithStream_LiveRoomCreateQueryRemove(t *testing.T) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + defer cancel() + + var r0 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done") + } + }(ctx) + + roomTitle := fmt.Sprintf("room-%v-%v", os.Getpid(), rand.Int()) + logger.Tf(ctx, "Test for room title %v", roomTitle) + + type LiveRoomCreateResult struct { + UUID string `json:"uuid"` + } + var roomCreated LiveRoomCreateResult + if err := NewApi().WithAuth(ctx, "/terraform/v1/live/room/create", &struct { + Title string `json:"title"` + }{ + Title: roomTitle, + }, &roomCreated); err != nil { + r0 = errors.Wrapf(err, "create room title=%v", roomTitle) + return + } + + defer func() { + // The ctx has already been cancelled by test case, which will cause the request failed. + ctx := context.Background() + NewApi().WithAuth(ctx, "/terraform/v1/live/room/remove", &roomCreated, nil) + }() + + type LiveRoomQueryResult struct { + // Live room UUID. + UUID string `json:"uuid"` + // Live room title. + Title string `json:"title"` + // Live room secret. + Secret string `json:"secret"` + // Create time. + CreatedAt string `json:"created_at"` + } + var roomQuery LiveRoomQueryResult + if err := NewApi().WithAuth(ctx, "/terraform/v1/live/room/query", &roomCreated, &roomQuery); err != nil { + r0 = errors.Wrapf(err, "query room uuid=%v, title=%v", roomCreated.UUID, roomTitle) + return + } + + var allRooms []LiveRoomQueryResult + if err := NewApi().WithAuth(ctx, "/terraform/v1/live/room/list", nil, &struct { + Rooms *[]LiveRoomQueryResult `json:"rooms"` + }{ + Rooms: &allRooms, + }); err != nil { + r0 = errors.Wrapf(err, "list rooms, uuid=%v, title=%v", roomCreated.UUID, roomTitle) + return + } + var found bool + for _, v := range allRooms { + if v.UUID == roomCreated.UUID { + found = true + break + } + } + if !found { + r0 = errors.Errorf("room not found in list, uuid=%v, title=%v", roomCreated.UUID, roomTitle) + return + } +} + +func TestMedia_WithStream_LiveRoomPublishStream(t *testing.T) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + defer cancel() + + if *noMediaTest { + return + } + + var r0, r1, r2, r3, r4, r5 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done") + } + }(ctx) + + roomTitle := fmt.Sprintf("room-%v-%v", os.Getpid(), rand.Int()) + logger.Tf(ctx, "Test for room title %v", roomTitle) + + type LiveRoomCreateResult struct { + // Live room UUID. + UUID string `json:"uuid"` + // Live room title. + Title string `json:"title"` + // Live room secret. + Secret string `json:"secret"` + // Create time. + CreatedAt string `json:"created_at"` + } + var liveRoom LiveRoomCreateResult + if err := NewApi().WithAuth(ctx, "/terraform/v1/live/room/create", &struct { + Title string `json:"title"` + }{ + Title: roomTitle, + }, &liveRoom); err != nil { + r0 = errors.Wrapf(err, "create room title=%v", roomTitle) + return + } + + defer func() { + // The ctx has already been cancelled by test case, which will cause the request failed. + ctx := context.Background() + NewApi().WithAuth(ctx, "/terraform/v1/live/room/remove", &liveRoom, nil) + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // Start FFmpeg to publish stream. + streamID := liveRoom.UUID + streamURL := fmt.Sprintf("%v/live/%v?secret=%v", *endpointRTMP, streamID, liveRoom.Secret) + ffmpeg := NewFFmpeg(func(v *ffmpegClient) { + v.args = []string{ + "-re", "-stream_loop", "-1", "-i", *srsInputFile, "-c", "copy", + "-f", "flv", streamURL, + } + }) + wg.Add(1) + go func() { + defer wg.Done() + r1 = ffmpeg.Run(ctx, cancel) + }() + + // Start FFprobe to detect and verify stream. + duration := time.Duration(*srsFFprobeDuration) * time.Millisecond + ffprobe := NewFFprobe(func(v *ffprobeClient) { + v.dvrFile = fmt.Sprintf("srs-ffprobe-%v.flv", streamID) + v.streamURL = fmt.Sprintf("%v/live/%v.flv", *endpointHTTP, streamID) + v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond + }) + wg.Add(1) + go func() { + defer wg.Done() + r2 = ffprobe.Run(ctx, cancel) + }() + + // Fast quit for probe done. + select { + case <-ctx.Done(): + case <-ffprobe.ProbeDoneCtx().Done(): + cancel() + } + + str, m := ffprobe.Result() + if len(m.Streams) != 2 { + r3 = errors.Errorf("invalid streams=%v, %v, %v", len(m.Streams), m.String(), str) + } + + if ts := 90; m.Format.ProbeScore < ts { + r4 = errors.Errorf("low score=%v < %v, %v, %v", m.Format.ProbeScore, ts, m.String(), str) + } + if dv := m.Duration(); dv < duration/3 { + r5 = errors.Errorf("short duration=%v < %v, %v, %v", dv, duration, m.String(), str) + } +} + +func TestMedia_WithStream_LiveRoomPublishInvalidStream(t *testing.T) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + defer cancel() + + if *noMediaTest { + return + } + + var r0, r1, r2, r3, r4, r5 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done") + } + }(ctx) + + roomTitle := fmt.Sprintf("room-%v-%v", os.Getpid(), rand.Int()) + logger.Tf(ctx, "Test for room title %v", roomTitle) + + type LiveRoomCreateResult struct { + // Live room UUID. + UUID string `json:"uuid"` + // Live room title. + Title string `json:"title"` + // Live room secret. + Secret string `json:"secret"` + // Create time. + CreatedAt string `json:"created_at"` + } + var liveRoom LiveRoomCreateResult + if err := NewApi().WithAuth(ctx, "/terraform/v1/live/room/create", &struct { + Title string `json:"title"` + }{ + Title: roomTitle, + }, &liveRoom); err != nil { + r0 = errors.Wrapf(err, "create room title=%v", roomTitle) + return + } + + defer func() { + // The ctx has already been cancelled by test case, which will cause the request failed. + ctx := context.Background() + NewApi().WithAuth(ctx, "/terraform/v1/live/room/remove", &liveRoom, nil) + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // Start FFmpeg to publish stream. + // Use a invalid random stream ID, which should be failed. + streamID := fmt.Sprintf("stream-%v-%v", os.Getpid(), rand.Int()) + streamURL := fmt.Sprintf("%v/live/%v?secret=%v", *endpointRTMP, streamID, liveRoom.Secret) + ffmpeg := NewFFmpeg(func(v *ffmpegClient) { + v.args = []string{ + "-re", "-stream_loop", "-1", "-i", *srsInputFile, "-c", "copy", + "-f", "flv", streamURL, + } + }) + wg.Add(1) + go func() { + defer wg.Done() + r1 = ffmpeg.Run(ctx, cancel) + }() + + // Start FFprobe to detect and verify stream. + duration := time.Duration(*srsFFprobeDuration) * time.Millisecond + ffprobe := NewFFprobe(func(v *ffprobeClient) { + v.dvrFile = fmt.Sprintf("srs-ffprobe-%v.flv", streamID) + v.streamURL = fmt.Sprintf("%v/live/%v.flv", *endpointHTTP, streamID) + v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond + }) + wg.Add(1) + go func() { + defer wg.Done() + r2 = ffprobe.Run(ctx, cancel) + }() + + // Fast quit for probe done. + select { + case <-ctx.Done(): + case <-ffprobe.ProbeDoneCtx().Done(): + cancel() + } + + // Should have no stream for callback failed. + str, m := ffprobe.Result() + if len(m.Streams) != 0 { + r3 = errors.Errorf("invalid streams=%v, %v, %v", len(m.Streams), m.String(), str) + } +} + +func TestMedia_WithStream_LiveRoomPublishInvalidSecret(t *testing.T) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + defer cancel() + + if *noMediaTest { + return + } + + var r0, r1, r2, r3, r4, r5 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done") + } + }(ctx) + + roomTitle := fmt.Sprintf("room-%v-%v", os.Getpid(), rand.Int()) + logger.Tf(ctx, "Test for room title %v", roomTitle) + + type LiveRoomCreateResult struct { + // Live room UUID. + UUID string `json:"uuid"` + // Live room title. + Title string `json:"title"` + // Live room secret. + Secret string `json:"secret"` + // Create time. + CreatedAt string `json:"created_at"` + } + var liveRoom LiveRoomCreateResult + if err := NewApi().WithAuth(ctx, "/terraform/v1/live/room/create", &struct { + Title string `json:"title"` + }{ + Title: roomTitle, + }, &liveRoom); err != nil { + r0 = errors.Wrapf(err, "create room title=%v", roomTitle) + return + } + + defer func() { + // The ctx has already been cancelled by test case, which will cause the request failed. + ctx := context.Background() + NewApi().WithAuth(ctx, "/terraform/v1/live/room/remove", &liveRoom, nil) + }() + + var wg sync.WaitGroup + defer wg.Wait() + + // Start FFmpeg to publish stream. + // Use a invalid random stream ID, which should be failed. + streamID := liveRoom.UUID + streamURL := fmt.Sprintf("%v/live/%v?secret=%v", *endpointRTMP, streamID, uuid.NewString()) + ffmpeg := NewFFmpeg(func(v *ffmpegClient) { + v.args = []string{ + "-re", "-stream_loop", "-1", "-i", *srsInputFile, "-c", "copy", + "-f", "flv", streamURL, + } + }) + wg.Add(1) + go func() { + defer wg.Done() + r1 = ffmpeg.Run(ctx, cancel) + }() + + // Start FFprobe to detect and verify stream. + duration := time.Duration(*srsFFprobeDuration) * time.Millisecond + ffprobe := NewFFprobe(func(v *ffprobeClient) { + v.dvrFile = fmt.Sprintf("srs-ffprobe-%v.flv", streamID) + v.streamURL = fmt.Sprintf("%v/live/%v.flv", *endpointHTTP, streamID) + v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond + }) + wg.Add(1) + go func() { + defer wg.Done() + r2 = ffprobe.Run(ctx, cancel) + }() + + // Fast quit for probe done. + select { + case <-ctx.Done(): + case <-ffprobe.ProbeDoneCtx().Done(): + cancel() + } + + // Should have no stream for callback failed. + str, m := ffprobe.Result() + if len(m.Streams) != 0 { + r3 = errors.Errorf("invalid streams=%v, %v, %v", len(m.Streams), m.String(), str) + } +} diff --git a/test/vendor/github.com/google/uuid/CHANGELOG.md b/test/vendor/github.com/google/uuid/CHANGELOG.md new file mode 100644 index 00000000..c9fb829d --- /dev/null +++ b/test/vendor/github.com/google/uuid/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12) + + +### Features + +* Validate UUID without creating new UUID ([#141](https://github.com/google/uuid/issues/141)) ([9ee7366](https://github.com/google/uuid/commit/9ee7366e66c9ad96bab89139418a713dc584ae29)) + +## [1.4.0](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) (2023-10-26) + + +### Features + +* UUIDs slice type with Strings() convenience method ([#133](https://github.com/google/uuid/issues/133)) ([cd5fbbd](https://github.com/google/uuid/commit/cd5fbbdd02f3e3467ac18940e07e062be1f864b4)) + +### Fixes + +* Clarify that Parse's job is to parse but not necessarily validate strings. (Documents current behavior) + +## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18) + + +### Bug Fixes + +* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0)) + +## Changelog diff --git a/test/vendor/github.com/google/uuid/CONTRIBUTING.md b/test/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 00000000..a502fdc5 --- /dev/null +++ b/test/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Tips + +Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org). + +Always try to include a test case! If it is not possible or not necessary, +please explain why in the pull request description. + +### Releasing + +Commits that would precipitate a SemVer change, as described in the Conventional +Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action) +to create a release candidate pull request. Once submitted, `release-please` +will create a release. + +For tips on how to work with `release-please`, see its documentation. + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/test/vendor/github.com/google/uuid/CONTRIBUTORS b/test/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 00000000..b4bb97f6 --- /dev/null +++ b/test/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/test/vendor/github.com/google/uuid/LICENSE b/test/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 00000000..5dc68268 --- /dev/null +++ b/test/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/vendor/github.com/google/uuid/README.md b/test/vendor/github.com/google/uuid/README.md new file mode 100644 index 00000000..3e9a6188 --- /dev/null +++ b/test/vendor/github.com/google/uuid/README.md @@ -0,0 +1,21 @@ +# uuid +The uuid package generates and inspects UUIDs based on +[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +```sh +go get github.com/google/uuid +``` + +###### Documentation +[![Go Reference](https://pkg.go.dev/badge/github.com/google/uuid.svg)](https://pkg.go.dev/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://pkg.go.dev/github.com/google/uuid diff --git a/test/vendor/github.com/google/uuid/dce.go b/test/vendor/github.com/google/uuid/dce.go new file mode 100644 index 00000000..fa820b9d --- /dev/null +++ b/test/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/test/vendor/github.com/google/uuid/doc.go b/test/vendor/github.com/google/uuid/doc.go new file mode 100644 index 00000000..5b8a4b9a --- /dev/null +++ b/test/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/test/vendor/github.com/google/uuid/go.mod b/test/vendor/github.com/google/uuid/go.mod new file mode 100644 index 00000000..fc84cd79 --- /dev/null +++ b/test/vendor/github.com/google/uuid/go.mod @@ -0,0 +1 @@ +module github.com/google/uuid diff --git a/test/vendor/github.com/google/uuid/hash.go b/test/vendor/github.com/google/uuid/hash.go new file mode 100644 index 00000000..b404f4be --- /dev/null +++ b/test/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) //nolint:errcheck + h.Write(data) //nolint:errcheck + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/test/vendor/github.com/google/uuid/marshal.go b/test/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 00000000..14bd3407 --- /dev/null +++ b/test/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + return err + } + *uuid = id + return nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/test/vendor/github.com/google/uuid/node.go b/test/vendor/github.com/google/uuid/node.go new file mode 100644 index 00000000..d651a2b0 --- /dev/null +++ b/test/vendor/github.com/google/uuid/node.go @@ -0,0 +1,90 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + ifname = "random" + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/test/vendor/github.com/google/uuid/node_js.go b/test/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 00000000..b2a0bc87 --- /dev/null +++ b/test/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This removes the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/test/vendor/github.com/google/uuid/node_net.go b/test/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 00000000..0cbbcddb --- /dev/null +++ b/test/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/test/vendor/github.com/google/uuid/null.go b/test/vendor/github.com/google/uuid/null.go new file mode 100644 index 00000000..d7fcbf28 --- /dev/null +++ b/test/vendor/github.com/google/uuid/null.go @@ -0,0 +1,118 @@ +// Copyright 2021 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "fmt" +) + +var jsonNull = []byte("null") + +// NullUUID represents a UUID that may be null. +// NullUUID implements the SQL driver.Scanner interface so +// it can be used as a scan destination: +// +// var u uuid.NullUUID +// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) +// ... +// if u.Valid { +// // use u.UUID +// } else { +// // NULL value +// } +// +type NullUUID struct { + UUID UUID + Valid bool // Valid is true if UUID is not NULL +} + +// Scan implements the SQL driver.Scanner interface. +func (nu *NullUUID) Scan(value interface{}) error { + if value == nil { + nu.UUID, nu.Valid = Nil, false + return nil + } + + err := nu.UUID.Scan(value) + if err != nil { + nu.Valid = false + return err + } + + nu.Valid = true + return nil +} + +// Value implements the driver Valuer interface. +func (nu NullUUID) Value() (driver.Value, error) { + if !nu.Valid { + return nil, nil + } + // Delegate to UUID Value function + return nu.UUID.Value() +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (nu NullUUID) MarshalBinary() ([]byte, error) { + if nu.Valid { + return nu.UUID[:], nil + } + + return []byte(nil), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (nu *NullUUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(nu.UUID[:], data) + nu.Valid = true + return nil +} + +// MarshalText implements encoding.TextMarshaler. +func (nu NullUUID) MarshalText() ([]byte, error) { + if nu.Valid { + return nu.UUID.MarshalText() + } + + return jsonNull, nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (nu *NullUUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + nu.Valid = false + return err + } + nu.UUID = id + nu.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler. +func (nu NullUUID) MarshalJSON() ([]byte, error) { + if nu.Valid { + return json.Marshal(nu.UUID) + } + + return jsonNull, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (nu *NullUUID) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, jsonNull) { + *nu = NullUUID{} + return nil // valid null UUID + } + err := json.Unmarshal(data, &nu.UUID) + nu.Valid = err == nil + return err +} diff --git a/test/vendor/github.com/google/uuid/sql.go b/test/vendor/github.com/google/uuid/sql.go new file mode 100644 index 00000000..2e02ec06 --- /dev/null +++ b/test/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently. +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/test/vendor/github.com/google/uuid/time.go b/test/vendor/github.com/google/uuid/time.go new file mode 100644 index 00000000..c3511292 --- /dev/null +++ b/test/vendor/github.com/google/uuid/time.go @@ -0,0 +1,134 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1, 2, 6 and 7 UUIDs. +func (uuid UUID) Time() Time { + var t Time + switch uuid.Version() { + case 6: + time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110 + t = Time(time) + case 7: + time := binary.BigEndian.Uint64(uuid[:8]) + t = Time((time>>16)*10000 + g1582ns100) + default: // forward compatible + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + t = Time(time) + } + return t +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/test/vendor/github.com/google/uuid/util.go b/test/vendor/github.com/google/uuid/util.go new file mode 100644 index 00000000..5ea6c737 --- /dev/null +++ b/test/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/test/vendor/github.com/google/uuid/uuid.go b/test/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 00000000..5232b486 --- /dev/null +++ b/test/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,365 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + "sync" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +const randPoolSize = 16 * 16 + +var ( + rander = rand.Reader // random function + poolEnabled = false + poolMu sync.Mutex + poolPos = randPoolSize // protected with poolMu + pool [randPoolSize]byte // protected with poolMu +) + +type invalidLengthError struct{ len int } + +func (err invalidLengthError) Error() string { + return fmt.Sprintf("invalid UUID length: %d", err.len) +} + +// IsInvalidLengthError is matcher function for custom error invalidLengthError +func IsInvalidLengthError(err error) bool { + _, ok := err.(invalidLengthError) + return ok +} + +// Parse decodes s into a UUID or returns an error if it cannot be parsed. Both +// the standard UUID forms defined in RFC 4122 +// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) are decoded. In addition, +// Parse accepts non-standard strings such as the raw hex encoding +// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and 38 byte "Microsoft style" encodings, +// e.g. {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Only the middle 36 bytes are +// examined in the latter case. Parse should not be used to validate strings as +// it parses non-standard encodings as indicated above. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if !strings.EqualFold(s[:9], "urn:uuid:") { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(s)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(b)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// Validate returns an error if s is not a properly formatted UUID in one of the following formats: +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +// It returns an error if the format is invalid, otherwise nil. +func Validate(s string) error { + switch len(s) { + // Standard UUID format + case 36: + + // UUID with "urn:uuid:" prefix + case 36 + 9: + if !strings.EqualFold(s[:9], "urn:uuid:") { + return fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // UUID enclosed in braces + case 36 + 2: + if s[0] != '{' || s[len(s)-1] != '}' { + return fmt.Errorf("invalid bracketed UUID format") + } + s = s[1 : len(s)-1] + + // UUID without hyphens + case 32: + for i := 0; i < len(s); i += 2 { + _, ok := xtob(s[i], s[i+1]) + if !ok { + return errors.New("invalid UUID format") + } + } + + default: + return invalidLengthError{len(s)} + } + + // Check for standard UUID format + if len(s) == 36 { + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return errors.New("invalid UUID format") + } + for _, x := range []int{0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} { + if _, ok := xtob(s[x], s[x+1]); !ok { + return errors.New("invalid UUID format") + } + } + } + + return nil +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} + +// EnableRandPool enables internal randomness pool used for Random +// (Version 4) UUID generation. The pool contains random bytes read from +// the random number generator on demand in batches. Enabling the pool +// may improve the UUID generation throughput significantly. +// +// Since the pool is stored on the Go heap, this feature may be a bad fit +// for security sensitive applications. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func EnableRandPool() { + poolEnabled = true +} + +// DisableRandPool disables the randomness pool if it was previously +// enabled with EnableRandPool. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func DisableRandPool() { + poolEnabled = false + defer poolMu.Unlock() + poolMu.Lock() + poolPos = randPoolSize +} + +// UUIDs is a slice of UUID types. +type UUIDs []UUID + +// Strings returns a string slice containing the string form of each UUID in uuids. +func (uuids UUIDs) Strings() []string { + var uuidStrs = make([]string, len(uuids)) + for i, uuid := range uuids { + uuidStrs[i] = uuid.String() + } + return uuidStrs +} diff --git a/test/vendor/github.com/google/uuid/version1.go b/test/vendor/github.com/google/uuid/version1.go new file mode 100644 index 00000000..46310962 --- /dev/null +++ b/test/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + copy(uuid[10:], nodeID[:]) + nodeMu.Unlock() + + return uuid, nil +} diff --git a/test/vendor/github.com/google/uuid/version4.go b/test/vendor/github.com/google/uuid/version4.go new file mode 100644 index 00000000..7697802e --- /dev/null +++ b/test/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,76 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewString creates a new random UUID and returns it as a string or panics. +// NewString is equivalent to the expression +// +// uuid.New().String() +func NewString() string { + return Must(NewRandom()).String() +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// Uses the randomness pool if it was enabled with EnableRandPool. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + if !poolEnabled { + return NewRandomFromReader(rander) + } + return newRandomFromPool() +} + +// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. +func NewRandomFromReader(r io.Reader) (UUID, error) { + var uuid UUID + _, err := io.ReadFull(r, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} + +func newRandomFromPool() (UUID, error) { + var uuid UUID + poolMu.Lock() + if poolPos == randPoolSize { + _, err := io.ReadFull(rander, pool[:]) + if err != nil { + poolMu.Unlock() + return Nil, err + } + poolPos = 0 + } + copy(uuid[:], pool[poolPos:(poolPos+16)]) + poolPos += 16 + poolMu.Unlock() + + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/test/vendor/github.com/google/uuid/version6.go b/test/vendor/github.com/google/uuid/version6.go new file mode 100644 index 00000000..339a959a --- /dev/null +++ b/test/vendor/github.com/google/uuid/version6.go @@ -0,0 +1,56 @@ +// Copyright 2023 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "encoding/binary" + +// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality. +// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs. +// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead. +// +// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6 +// +// NewV6 returns a Version 6 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewV6 returns Nil and an error. +func NewV6() (UUID, error) { + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | time_high | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | time_mid | time_low_and_version | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |clk_seq_hi_res | clk_seq_low | node (0-1) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | node (2-5) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + binary.BigEndian.PutUint64(uuid[0:], uint64(now)) + binary.BigEndian.PutUint16(uuid[8:], seq) + + uuid[6] = 0x60 | (uuid[6] & 0x0F) + uuid[8] = 0x80 | (uuid[8] & 0x3F) + + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + copy(uuid[10:], nodeID[:]) + nodeMu.Unlock() + + return uuid, nil +} diff --git a/test/vendor/github.com/google/uuid/version7.go b/test/vendor/github.com/google/uuid/version7.go new file mode 100644 index 00000000..ba9dd5eb --- /dev/null +++ b/test/vendor/github.com/google/uuid/version7.go @@ -0,0 +1,75 @@ +// Copyright 2023 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// UUID version 7 features a time-ordered value field derived from the widely +// implemented and well known Unix Epoch timestamp source, +// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. +// As well as improved entropy characteristics over versions 1 or 6. +// +// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7 +// +// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible. +// +// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch). +// Uses the randomness pool if it was enabled with EnableRandPool. +// On error, NewV7 returns Nil and an error +func NewV7() (UUID, error) { + uuid, err := NewRandom() + if err != nil { + return uuid, err + } + makeV7(uuid[:]) + return uuid, nil +} + +// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch). +// it use NewRandomFromReader fill random bits. +// On error, NewV7FromReader returns Nil and an error. +func NewV7FromReader(r io.Reader) (UUID, error) { + uuid, err := NewRandomFromReader(r) + if err != nil { + return uuid, err + } + + makeV7(uuid[:]) + return uuid, nil +} + +// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6]) +// uuid[8] already has the right version number (Variant is 10) +// see function NewV7 and NewV7FromReader +func makeV7(uuid []byte) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | ver | rand_a | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |var| rand_b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | rand_b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + _ = uuid[15] // bounds check + + t := timeNow().UnixMilli() + + uuid[0] = byte(t >> 40) + uuid[1] = byte(t >> 32) + uuid[2] = byte(t >> 24) + uuid[3] = byte(t >> 16) + uuid[4] = byte(t >> 8) + uuid[5] = byte(t) + + uuid[6] = 0x70 | (uuid[6] & 0x0F) + // uuid[8] has already has right version +} diff --git a/test/vendor/modules.txt b/test/vendor/modules.txt index 53e06884..48a99f64 100644 --- a/test/vendor/modules.txt +++ b/test/vendor/modules.txt @@ -1,3 +1,6 @@ +# github.com/google/uuid v1.5.0 +## explicit +github.com/google/uuid # github.com/joho/godotenv v1.5.1 ## explicit github.com/joho/godotenv diff --git a/ui/src/components/UrlGenerator.js b/ui/src/components/UrlGenerator.js index afb248ce..28817cd2 100644 --- a/ui/src/components/UrlGenerator.js +++ b/ui/src/components/UrlGenerator.js @@ -119,6 +119,9 @@ export default function useUrls() { const [transcodeStreamKey, setTranscodeStreamKey] = React.useState(); const [transcodeFlvPlayer, setTranscodeFlvPlayer] = React.useState(); + // Whether urls are ready. + const [ready, setReady] = React.useState(false); + const [loading, setLoading] = React.useState(true); const [secret, setSecret] = React.useState(); const {t} = useTranslation(); @@ -196,9 +199,12 @@ export default function useUrls() { setTranscodeStreamKey(urls.transcodeStreamKey); setTranscodeFlvPlayer(urls.transcodeFlvPlayer); } - }, [loading, secret, rtmpStreamName, env]) + + setReady(true); + }, [loading, secret, rtmpStreamName, env, setReady]) return { + ready, // For basic stream. rtmpServer, rtmpStreamName, diff --git a/ui/src/pages/Scenario.js b/ui/src/pages/Scenario.js index 74ea3af2..b3f00edb 100644 --- a/ui/src/pages/Scenario.js +++ b/ui/src/pages/Scenario.js @@ -6,7 +6,7 @@ import {useSearchParams} from "react-router-dom"; import {Container, Tabs, Tab} from "react-bootstrap"; import React from "react"; -import ScenarioLive from './ScenarioLive'; +import ScenarioLiveStreams from './ScenarioLive'; import useUrls from "../components/UrlGenerator"; import ScenarioForward from './ScenarioForward'; import {SrsErrorBoundary} from "../components/SrsErrorBoundary"; @@ -18,6 +18,7 @@ import ScenarioVLive from "./ScenarioVLive"; import {ScenarioVxOthers} from "./ScenarioOthers"; import ScenarioTranscode from "./ScenarioTranscode"; import ScenarioTranscript from "./ScenarioTranscript"; +import ScenarioLiveRoom from "./ScenarioLiveRoom"; export default function Scenario() { const [searchParams] = useSearchParams(); @@ -26,7 +27,7 @@ export default function Scenario() { React.useEffect(() => { const tab = searchParams.get('tab') || 'tutorials'; - console.log(`?tab=tutorials|live|record|vlive|transcode|transcript|others, current=${tab}, Select the tab to render`); + console.log(`?tab=tutorials|live|stream|record|vlive|transcode|transcript|others, current=${tab}, Select the tab to render`); setDefaultActiveTab(tab); }, [searchParams, language]); @@ -46,7 +47,7 @@ function ScenarioImpl({defaultActiveTab}) { const onSelectTab = React.useCallback((k) => { setSearchParams({'tab': k}); setActiveTab(k); - }, [setSearchParams]); + }, [setSearchParams, setActiveTab]); return ( <> @@ -57,7 +58,10 @@ function ScenarioImpl({defaultActiveTab}) { {activeTab === 'tutorials' && } - {activeTab === 'live' && } + {activeTab === 'live' && } + + + {activeTab === 'stream' && } {activeTab === 'forward' && } diff --git a/ui/src/pages/ScenarioLive.js b/ui/src/pages/ScenarioLive.js index 56a9b290..63798e93 100644 --- a/ui/src/pages/ScenarioLive.js +++ b/ui/src/pages/ScenarioLive.js @@ -14,7 +14,7 @@ import {useTranslation} from "react-i18next"; import {useSearchParams} from "react-router-dom"; import {SrsEnvContext} from "../components/SrsEnvContext"; -export default function ScenarioLive({urls}) { +export default function ScenarioLiveStreams({urls}) { const [searchParams] = useSearchParams(); const {t} = useTranslation(); const copyToClipboard = React.useCallback((e, text) => { diff --git a/ui/src/pages/ScenarioLiveRoom.js b/ui/src/pages/ScenarioLiveRoom.js new file mode 100644 index 00000000..ef5294aa --- /dev/null +++ b/ui/src/pages/ScenarioLiveRoom.js @@ -0,0 +1,273 @@ +// +// Copyright (c) 2022-2023 Winlin +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// +import React from "react"; +import {useSrsLanguage} from "../components/LanguageSwitch"; +import {Accordion, Button, Form, Table} from "react-bootstrap"; +import {useTranslation} from "react-i18next"; +import axios from "axios"; +import {Clipboard, Token} from "../utils"; +import {useErrorHandler} from "react-error-boundary"; +import {useSearchParams} from "react-router-dom"; +import {buildUrls} from "../components/UrlGenerator"; +import {SrsEnvContext} from "../components/SrsEnvContext"; +import * as Icon from "react-bootstrap-icons"; +import PopoverConfirm from "../components/PopoverConfirm"; + +export default function ScenarioLiveRoom() { + const [searchParams] = useSearchParams(); + // The room id, to maintain a specified room. + const [roomId, setRoomId] = React.useState(); + + React.useEffect(() => { + const id = searchParams.get('roomid') || null; + console.log(`?roomid=xxx, current=${id}, Set the roomid to manage.`); + setRoomId(id); + }, [searchParams, setRoomId]); + + if (roomId) return ; + return ; +} + +export function ScenarioLiveRoomImpl({setRoomId}) { + const language = useSrsLanguage(); + const {t} = useTranslation(); + const handleError = useErrorHandler(); + const [searchParams, setSearchParams] = useSearchParams(); + const [name, setName] = React.useState('My Live Room'); + const [rooms, setRooms] = React.useState([]); + const [refreshNow, setRefreshNow] = React.useState(); + + const createLiveRoom = React.useCallback((e) => { + e.preventDefault(); + + axios.post('/terraform/v1/live/room/create', { + title: name, + }, { + headers: Token.loadBearerHeader(), + }).then(res => { + const {uuid} = res.data.data; + searchParams.set('roomid', uuid); setSearchParams(searchParams); + setRoomId(uuid); + console.log(`Status: Create ok, name=${name}, data=${JSON.stringify(res.data.data)}`); + }).catch(handleError); + }, [handleError, name, setRoomId, searchParams, setSearchParams]); + + const removeRoom = React.useCallback((uuid) => { + axios.post('/terraform/v1/live/room/remove', { + uuid: uuid, + }, { + headers: Token.loadBearerHeader(), + }).then(res => { + setRefreshNow(!refreshNow); + console.log(`Status: Remove ok, uuid=${uuid}, data=${JSON.stringify(res.data.data)}`); + }).catch(handleError); + }, [handleError, refreshNow, setRefreshNow]); + + const manageRoom = React.useCallback((room) => { + const uuid = room.uuid; + searchParams.set('roomid', uuid); setSearchParams(searchParams); + setRoomId(room.uuid); + }, [searchParams, setSearchParams, setRoomId]); + + React.useEffect(() => { + const refreshLiveRoomsTask = () => { + axios.post('/terraform/v1/live/room/list', { + }, { + headers: Token.loadBearerHeader(), + }).then(res => { + const {rooms} = res.data.data; + setRooms(rooms || []); + console.log(`Status: List ok, data=${JSON.stringify(res.data.data)}`); + }).catch(handleError); + }; + + refreshLiveRoomsTask(); + const timer = setInterval(() => refreshLiveRoomsTask(), 3 * 1000); + return () => { + clearInterval(timer); + setRooms([]); + } + }, [handleError, setRooms, refreshNow]); + + return ( + + + {language === 'zh' ? + + 场景介绍 + +
直播间,提供了按每个流鉴权的能力,并支持直播间的业务功能。
+

+

可应用的具体场景包括:

+
    +
  • 自建直播间,私域直播,仅限私域会员能观看的直播。
  • +
  • 企业直播,企业内部的直播间,仅限企业内部人员观看。
  • +
  • 电商直播,仅限电商特定买家可观看的直播。
  • +
+
+
: + + Scenario Introduction + +
Live room, which provides the ability to authenticate each stream and supports business functions of live room.
+

+

The specific scenarios that can be applied include:

+
    +
  • Self-built live room, private domain live broadcast, live broadcast that can only be watched by private domain members.
  • +
  • Enterprise live broadcast, live room within the enterprise, only for internal personnel of the enterprise.
  • +
  • E-commerce live broadcast, live broadcast that can only be watched by specific buyers of e-commerce.
  • +
+
+
} +
+ + {t('lr.create.title')} + +
+ + {t('lr.create.name')} + * {t('lr.create.name2')} + setName(e.target.value)} /> + + +
+
+
+ + {t('lr.list.title')} + + {rooms?.length ? + + + + + + + + + + + {rooms?.map((room, index) => { + return + + + + + + ; + })} + +
#UUIDTitleCreated AtActions
{index}{room.uuid}{room.title}{room.created_at} + { + e.preventDefault(); + manageRoom(room); + }}>{t('helper.manage')}   + {t('helper.delete')} } onClick={() => removeRoom(room.uuid)}> +

+ {t('lr.list.delete')} +

+
+
: t('lr.list.empty')} +
+
+
+ ); +} + +function ScenarioLiveRoomEditor({roomId, setRoomId}) { + const handleError = useErrorHandler(); + const env = React.useContext(SrsEnvContext)[0]; + const {t} = useTranslation(); + const [room, setRoom] = React.useState({}); + const [urls, setUrls] = React.useState({}); + + const copyToClipboard = React.useCallback((e, text) => { + e.preventDefault(); + + Clipboard.copy(text).then(() => { + alert(t('helper.copyOk')); + }).catch((err) => { + alert(`${t('helper.copyFail')} ${err}`); + }); + }, [t]); + + React.useEffect(() => { + axios.post('/terraform/v1/live/room/query', { + uuid: roomId, + }, { + headers: Token.loadBearerHeader(), + }).then(res => { + setRoom(res.data.data); + console.log(`Status: Query ok, uuid=${roomId}, data=${JSON.stringify(res.data.data)}`); + }).catch(handleError); + }, [handleError, setRoom]); + + React.useEffect(() => { + if (!room?.secret) return; + const urls = buildUrls(`live/${roomId}`, {publish: room.secret}, env); + setUrls(urls); + }, [room, env, setUrls]); + + const { + flvPlayer, rtmpServer, flvUrl, rtmpStreamKey, hlsPlayer, m3u8Url, rtcUrl, rtcPlayer, cnConsole, enConsole, rtcPublisher, + srtPublishUrl, srtPlayUrl, rtcPublishUrl, updateStreamName, whipUrl, whepUrl, + } = urls; + + return <> + + + + {t('live.obs.title')} + +
+ {t('live.obs.server')} {rtmpServer}   +
+ copyToClipboard(e, rtmpServer)} /> +
+
+
+ {t('live.obs.key')} {rtmpStreamKey}   +
+ copyToClipboard(e, rtmpStreamKey)} /> +
+
+
+ {t('live.share.hls')}  + {t('live.share.simple')},  + {m3u8Url}   +
+ copyToClipboard(e, m3u8Url)} /> +
+
+
+
+ + {t('live.srt.title')} + +
+ {t('live.obs.server')} {srtPublishUrl}   +
+ copyToClipboard(e, srtPublishUrl)} /> +
+
+
+ {t('live.obs.key')} {t('live.obs.nokey')} +
+
+ {t('live.share.hls')}  + {t('live.share.simple')},  + {m3u8Url}   +
+ copyToClipboard(e, m3u8Url)} /> +
+
+
+
+
+ ; +} diff --git a/ui/src/pages/Settings.js b/ui/src/pages/Settings.js index f4ff78c7..24428176 100644 --- a/ui/src/pages/Settings.js +++ b/ui/src/pages/Settings.js @@ -421,7 +421,7 @@ function SettingLimits() { }, { headers: Token.loadBearerHeader(), }).then(res => { - setVLiveBitrate(res.data.data.vlive); + if (res.data.data?.vlive) setVLiveBitrate(res.data.data.vlive); console.log(`Limits: query ${JSON.stringify(res.data.data)}`); }).catch(handleError); }, [handleError, setVLiveBitrate]); diff --git a/ui/src/resources/locale.json b/ui/src/resources/locale.json index 6e37083d..4d193308 100644 --- a/ui/src/resources/locale.json +++ b/ui/src/resources/locale.json @@ -29,7 +29,8 @@ }, "scenario": { "tutorials": "教程", - "live": "推拉直播流", + "live": "推拉流", + "stream": "直播间", "srt": "超清实时直播", "forward": "多平台转播", "transcode": "直播转码", @@ -214,6 +215,7 @@ "nofile": "未选择任何文件" }, "helper": { + "manage": "管理", "delete": "删除", "end": "结束", "preview": "预览", @@ -231,6 +233,7 @@ "enable": "启用", "disable": "禁用", "submit": "提交", + "create": "创建", "setOk": "设置成功", "testOk": "测试通过", "upload": "上传文件", @@ -441,11 +444,35 @@ "test3": "开始测试", "globEmpty": "Glob规则不能为空", "urlEmpty": "目标流URL不能为空" + }, + "lr": { + "create": { + "title": "创建直播间", + "name": "直播主题", + "name2": "请输入直播间主题" + }, + "list": { + "title": "直播间列表", + "empty": "暂无直播间", + "delete": "删除后不可恢复,确认删除直播间吗?" + } } } }, "en": { "translation": { + "lr": { + "list": { + "delete": "Deleted rooms cannot be restored. Confirm to delete the room?", + "empty": "No Live Rooms", + "title": "Live Rooms" + }, + "create": { + "name2": "Please input live room title", + "name": "Title", + "title": "Create Live Room" + } + }, "record": { "urlEmpty": "Stream URL cannot be empty", "globEmpty": "Glob filters cannot be empty", @@ -674,6 +701,7 @@ "scenario": { "tutorials": "Tutorials", "live": "Streaming", + "stream": "LiveRoom", "srt": "Low Latency", "forward": "Forward", "transcode": "Transcoding", @@ -858,6 +886,7 @@ "nofile": "No file choosen" }, "helper": { + "manage": "Manage", "delete": "Delete", "end": "End", "preview": "Preview", @@ -875,6 +904,7 @@ "enable": "On", "disable": "Off", "submit": "Submit", + "create": "Create", "setOk": "Setup OK", "testOk": "Test OK", "upload": "Upload File",