From 0760f8d00d2f0ecbcd6c1c4533596f3e912d714f Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 16 May 2022 21:25:29 +0000 Subject: [PATCH 01/13] chore: Add watch workspace endpoint --- coderd/coderd.go | 1 + coderd/workspaces.go | 92 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/coderd/coderd.go b/coderd/coderd.go index 1f1a4ff18dfac..a639982248596 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -311,6 +311,7 @@ func New(options *Options) (http.Handler, func()) { r.Put("/", api.putWorkspaceAutostop) }) }) + r.HandleFunc("/watch", api.watchWorkspace) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 4ef3b61301fe6..7f431b7c37b79 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -7,12 +7,16 @@ import ( "errors" "fmt" "net/http" + "time" + "cdr.dev/slog" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/coderd/database" @@ -535,6 +539,94 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) { } } +func (api *api) watchWorkspace(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + + c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ + // Fix for Safari 15.1: + // There is a bug in latest Safari in which compressed web socket traffic + // isn't handled correctly. Turning off compression is a workaround: + // https://github.com/nhooyr/websocket/issues/218 + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + api.Logger.Warn(r.Context(), "accept websocket connection", slog.Error(err)) + return + } + defer c.Close(websocket.StatusInternalError, "internal error") + + ctx := c.CloseRead(r.Context()) + + // Send a heartbeat every 15 seconds to avoid the websocket being killed. + go func() { + ticker := time.NewTicker(time.Second * 15) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := c.Ping(ctx) + if err != nil { + return + } + } + } + }() + + t := time.NewTicker(time.Second * 1) + defer t.Stop() + for { + select { + case <-t.C: + workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID) + if err != nil { + _ = wsjson.Write(ctx, c, httpapi.Response{ + Message: fmt.Sprintf("get workspace: %s", err), + }) + return + } + build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + if err != nil { + _ = wsjson.Write(ctx, c, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err), + }) + return + } + var ( + group errgroup.Group + job database.ProvisionerJob + template database.Template + owner database.User + ) + group.Go(func() (err error) { + job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID) + return err + }) + group.Go(func() (err error) { + template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) + return err + }) + group.Go(func() (err error) { + owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID) + return err + }) + err = group.Wait() + if err != nil { + _ = wsjson.Write(ctx, c, httpapi.Response{ + Message: fmt.Sprintf("fetch resource: %s", err), + }) + return + } + + _ = wsjson.Write(ctx, c, convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) + case <-ctx.Done(): + return + } + } +} + func convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) { workspaceIDs := make([]uuid.UUID, 0, len(workspaces)) templateIDs := make([]uuid.UUID, 0, len(workspaces)) From 0551613f81e07ad7b693063c1b95838674017300 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 16 May 2022 21:29:10 +0000 Subject: [PATCH 02/13] Add comment --- coderd/workspaces.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 7f431b7c37b79..70379c4afe31c 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -555,6 +555,7 @@ func (api *api) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } defer c.Close(websocket.StatusInternalError, "internal error") + // Makes the websocket connection write-only ctx := c.CloseRead(r.Context()) // Send a heartbeat every 15 seconds to avoid the websocket being killed. From da9c744fafed381c45f499354b1458288ab4961a Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 16 May 2022 22:41:40 +0000 Subject: [PATCH 03/13] Make WorkspaceWatcher --- codersdk/client.go | 36 ++++++++++++++++++++++++++++++++++++ codersdk/workspaces.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/codersdk/client.go b/codersdk/client.go index 48571adff5d0b..1930c5c3b0b65 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/xerrors" + "nhooyr.io/websocket" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" @@ -80,6 +81,41 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return resp, err } +// request performs an HTTP request with the body provided. +// The caller is responsible for closing the response body. +func (c *Client) websocket(ctx context.Context, path string) (*websocket.Conn, error) { + serverURL, err := c.URL.Parse(path) + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + + apiURL, err := url.Parse(serverURL.String()) + apiURL.Scheme = "ws" + if serverURL.Scheme == "https" { + apiURL.Scheme = "wss" + } + apiURL.Path = path + + client := &http.Client{ + Jar: c.HTTPClient.Jar, + } + cookies := append(client.Jar.Cookies(c.URL), &http.Cookie{ + Name: httpmw.AuthCookie, + Value: c.SessionToken, + }) + client.Jar.SetCookies(c.URL, cookies) + + //nolint:bodyclose + conn, _, err := websocket.Dial(context.Background(), apiURL.String(), &websocket.DialOptions{ + HTTPClient: c.HTTPClient, + }) + if err != nil { + return nil, xerrors.Errorf("dial websocket: %w", err) + } + + return conn, nil +} + // readBodyAsError reads the response as an httpapi.Message, and // wraps it in a codersdk.Error type for easy marshaling. func readBodyAsError(res *http.Response) error { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 6e4ab7afd6e57..a2a241f59ae9e 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -9,6 +9,8 @@ import ( "github.com/google/uuid" "golang.org/x/xerrors" + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" "github.com/coder/coder/coderd/database" ) @@ -98,6 +100,40 @@ func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } +type WorkspaceWatcher struct { + conn *websocket.Conn +} + +func (w *WorkspaceWatcher) Read(ctx context.Context) (Workspace, error) { + var ws Workspace + err := wsjson.Read(ctx, w.conn, &ws) + if err != nil { + return ws, xerrors.Errorf("read workspace: %w") + } + + return ws, nil +} + +func (w *WorkspaceWatcher) Close() error { + err := w.conn.Close(websocket.StatusNormalClosure, "") + if err != nil { + return xerrors.Errorf("closing workspace watcher: %w", err) + } + + return nil +} + +func (c *Client) WorkspaceWatcher(ctx context.Context, id uuid.UUID) (*WorkspaceWatcher, error) { + conn, err := c.websocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id)) + if err != nil { + return nil, err + } + + return &WorkspaceWatcher{ + conn: conn, + }, nil +} + // UpdateWorkspaceAutostartRequest is a request to update a workspace's autostart schedule. type UpdateWorkspaceAutostartRequest struct { Schedule string `json:"schedule"` From dbaeb2cf596fe8f4a1d58f35010d9848e87b9eaf Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 17 May 2022 20:10:40 +0000 Subject: [PATCH 04/13] add tests --- coderd/httpmw/apikey.go | 10 ++++++++-- coderd/workspaces_test.go | 25 +++++++++++++++++++++++++ codersdk/client.go | 16 +++++----------- codersdk/workspaces.go | 2 +- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index c3038ace73b6a..34754c67e0c6c 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -43,14 +43,20 @@ type OAuth2Configs struct { func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var cookieValue string cookie, err := r.Cookie(AuthCookie) if err != nil { + cookieValue = r.URL.Query().Get(AuthCookie) + } else { + cookieValue = cookie.Value + } + if cookieValue == "" { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("%q cookie must be provided", AuthCookie), + Message: fmt.Sprintf("%q cookie or query parameter must be provided", AuthCookie), }) return } - parts := strings.Split(cookie.Value, "-") + parts := strings.Split(cookieValue, "-") // APIKeys are formatted: ID-SECRET if len(parts) != 2 { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 58140b1d00e1d..979cb784d2750 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -621,3 +621,28 @@ func mustLocation(t *testing.T, location string) *time.Location { return loc } + +func TestWorkspaceWatcher(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + w, err := client.Workspace(context.Background(), workspace.ID) + require.NoError(t, err) + + ww, err := client.WatchWorkspace(context.Background(), w.ID) + require.NoError(t, err) + defer ww.Close() + for i := 0; i < 5; i++ { + _, err := ww.Read(context.Background()) + require.NoError(t, err) + } + err = ww.Close() + require.NoError(t, err) + _, err = ww.Read(context.Background()) + require.Error(t, err) +} diff --git a/codersdk/client.go b/codersdk/client.go index 1930c5c3b0b65..fd7cc1199c18f 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -81,8 +81,8 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return resp, err } -// request performs an HTTP request with the body provided. -// The caller is responsible for closing the response body. +// websocket opens a websocket connection on that path provided. +// The caller is responsible for closing the websocket.Conn. func (c *Client) websocket(ctx context.Context, path string) (*websocket.Conn, error) { serverURL, err := c.URL.Parse(path) if err != nil { @@ -95,15 +95,9 @@ func (c *Client) websocket(ctx context.Context, path string) (*websocket.Conn, e apiURL.Scheme = "wss" } apiURL.Path = path - - client := &http.Client{ - Jar: c.HTTPClient.Jar, - } - cookies := append(client.Jar.Cookies(c.URL), &http.Cookie{ - Name: httpmw.AuthCookie, - Value: c.SessionToken, - }) - client.Jar.SetCookies(c.URL, cookies) + q := apiURL.Query() + q.Add(httpmw.AuthCookie, c.SessionToken) + apiURL.RawQuery = q.Encode() //nolint:bodyclose conn, _, err := websocket.Dial(context.Background(), apiURL.String(), &websocket.DialOptions{ diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index a2a241f59ae9e..2b339d090f004 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -123,7 +123,7 @@ func (w *WorkspaceWatcher) Close() error { return nil } -func (c *Client) WorkspaceWatcher(ctx context.Context, id uuid.UUID) (*WorkspaceWatcher, error) { +func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (*WorkspaceWatcher, error) { conn, err := c.websocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id)) if err != nil { return nil, err From 596cb8c5120f1058483a4c604220027b8884be6b Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 17 May 2022 20:15:47 +0000 Subject: [PATCH 05/13] lint --- codersdk/client.go | 7 +++++-- codersdk/workspaces.go | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/codersdk/client.go b/codersdk/client.go index fd7cc1199c18f..f1fdfce9622f8 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -86,10 +86,13 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac func (c *Client) websocket(ctx context.Context, path string) (*websocket.Conn, error) { serverURL, err := c.URL.Parse(path) if err != nil { - return nil, xerrors.Errorf("parse url: %w", err) + return nil, xerrors.Errorf("parse path: %w", err) } apiURL, err := url.Parse(serverURL.String()) + if err != nil { + return nil, xerrors.Errorf("parse server url: %w", err) + } apiURL.Scheme = "ws" if serverURL.Scheme == "https" { apiURL.Scheme = "wss" @@ -100,7 +103,7 @@ func (c *Client) websocket(ctx context.Context, path string) (*websocket.Conn, e apiURL.RawQuery = q.Encode() //nolint:bodyclose - conn, _, err := websocket.Dial(context.Background(), apiURL.String(), &websocket.DialOptions{ + conn, _, err := websocket.Dial(ctx, apiURL.String(), &websocket.DialOptions{ HTTPClient: c.HTTPClient, }) if err != nil { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 2b339d090f004..31ed2a8dea67f 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -108,7 +108,7 @@ func (w *WorkspaceWatcher) Read(ctx context.Context) (Workspace, error) { var ws Workspace err := wsjson.Read(ctx, w.conn, &ws) if err != nil { - return ws, xerrors.Errorf("read workspace: %w") + return ws, xerrors.Errorf("read workspace: %w", err) } return ws, nil From 31b14adcac54093c317e6185431c46752341930a Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 17 May 2022 20:28:15 +0000 Subject: [PATCH 06/13] Add test for query param session token --- coderd/httpmw/apikey.go | 16 +++---- coderd/httpmw/apikey_test.go | 53 +++++++++++++++++----- coderd/httpmw/authorize_test.go | 2 +- coderd/httpmw/organizationparam_test.go | 2 +- coderd/httpmw/templateparam_test.go | 2 +- coderd/httpmw/templateversionparam_test.go | 2 +- coderd/httpmw/userparam_test.go | 2 +- coderd/httpmw/workspaceagent.go | 4 +- coderd/httpmw/workspaceagent_test.go | 2 +- coderd/httpmw/workspaceagentparam_test.go | 2 +- coderd/httpmw/workspacebuildparam_test.go | 2 +- coderd/httpmw/workspaceparam_test.go | 2 +- coderd/users.go | 4 +- coderd/users_test.go | 2 +- codersdk/client.go | 4 +- codersdk/workspaceagents.go | 6 +-- 16 files changed, 69 insertions(+), 38 deletions(-) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 34754c67e0c6c..abf55128d55d7 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -17,8 +17,8 @@ import ( "github.com/coder/coder/coderd/httpapi" ) -// AuthCookie represents the name of the cookie the API key is stored in. -const AuthCookie = "session_token" +// SessionTokenKey represents the name of the cookie or query paramater the API key is stored in. +const SessionTokenKey = "session_token" type apiKeyContextKey struct{} @@ -44,15 +44,15 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { var cookieValue string - cookie, err := r.Cookie(AuthCookie) + cookie, err := r.Cookie(SessionTokenKey) if err != nil { - cookieValue = r.URL.Query().Get(AuthCookie) + cookieValue = r.URL.Query().Get(SessionTokenKey) } else { cookieValue = cookie.Value } if cookieValue == "" { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("%q cookie or query parameter must be provided", AuthCookie), + Message: fmt.Sprintf("%q cookie or query parameter must be provided", SessionTokenKey), }) return } @@ -60,7 +60,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h // APIKeys are formatted: ID-SECRET if len(parts) != 2 { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("invalid %q cookie api key format", AuthCookie), + Message: fmt.Sprintf("invalid %q cookie api key format", SessionTokenKey), }) return } @@ -69,13 +69,13 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h // Ensuring key lengths are valid. if len(keyID) != 10 { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("invalid %q cookie api key id", AuthCookie), + Message: fmt.Sprintf("invalid %q cookie api key id", SessionTokenKey), }) return } if len(keySecret) != 22 { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("invalid %q cookie api key secret", AuthCookie), + Message: fmt.Sprintf("invalid %q cookie api key secret", SessionTokenKey), }) return } diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go index 0c8d8d396e55b..3be2877426134 100644 --- a/coderd/httpmw/apikey_test.go +++ b/coderd/httpmw/apikey_test.go @@ -56,7 +56,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: "test-wow-hello", }) @@ -74,7 +74,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: "test-wow", }) @@ -92,7 +92,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: "testtestid-wow", }) @@ -111,7 +111,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -130,7 +130,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -157,7 +157,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -182,7 +182,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -209,6 +209,37 @@ func TestAPIKey(t *testing.T) { require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt) }) + t.Run("QueryParameter", func(t *testing.T) { + t.Parallel() + var ( + db = databasefake.New() + id, secret = randomAPIKeyParts() + hashed = sha256.Sum256([]byte(secret)) + r = httptest.NewRequest("GET", "/", nil) + rw = httptest.NewRecorder() + ) + q := r.URL.Query() + q.Add(httpmw.SessionTokenKey, fmt.Sprintf("%s-%s", id, secret)) + r.URL.RawQuery = q.Encode() + + _, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ + ID: id, + HashedSecret: hashed[:], + ExpiresAt: database.Now().AddDate(0, 0, 1), + }) + require.NoError(t, err) + httpmw.ExtractAPIKey(db, nil)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // Checks that it exists on the context! + _ = httpmw.APIKey(r) + httpapi.Write(rw, http.StatusOK, httpapi.Response{ + Message: "it worked!", + }) + })).ServeHTTP(rw, r) + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + }) + t.Run("ValidUpdateLastUsed", func(t *testing.T) { t.Parallel() var ( @@ -219,7 +250,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -252,7 +283,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -285,7 +316,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) @@ -319,7 +350,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go index 59ee50c80d9be..0e2466da403cd 100644 --- a/coderd/httpmw/authorize_test.go +++ b/coderd/httpmw/authorize_test.go @@ -94,7 +94,7 @@ func TestExtractUserRoles(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) req.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: token, }) diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index b062e63bc3819..2b3a01cc6aaa5 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -29,7 +29,7 @@ func TestOrganizationParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/templateparam_test.go b/coderd/httpmw/templateparam_test.go index b4db9925391c3..201961ba26c54 100644 --- a/coderd/httpmw/templateparam_test.go +++ b/coderd/httpmw/templateparam_test.go @@ -29,7 +29,7 @@ func TestTemplateParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/templateversionparam_test.go b/coderd/httpmw/templateversionparam_test.go index f168661d8bde8..d4487b183b788 100644 --- a/coderd/httpmw/templateversionparam_test.go +++ b/coderd/httpmw/templateversionparam_test.go @@ -29,7 +29,7 @@ func TestTemplateVersionParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/userparam_test.go b/coderd/httpmw/userparam_test.go index 48e1da72e2142..d7a467d65940c 100644 --- a/coderd/httpmw/userparam_test.go +++ b/coderd/httpmw/userparam_test.go @@ -29,7 +29,7 @@ func TestUserParam(t *testing.T) { rw = httptest.NewRecorder() ) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/workspaceagent.go b/coderd/httpmw/workspaceagent.go index 05e7fe213c242..8bcf10f086285 100644 --- a/coderd/httpmw/workspaceagent.go +++ b/coderd/httpmw/workspaceagent.go @@ -28,10 +28,10 @@ func WorkspaceAgent(r *http.Request) database.WorkspaceAgent { func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie(AuthCookie) + cookie, err := r.Cookie(SessionTokenKey) if err != nil { httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("%q cookie must be provided", AuthCookie), + Message: fmt.Sprintf("%q cookie must be provided", SessionTokenKey), }) return } diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go index 650e68436b836..0661183abffcf 100644 --- a/coderd/httpmw/workspaceagent_test.go +++ b/coderd/httpmw/workspaceagent_test.go @@ -22,7 +22,7 @@ func TestWorkspaceAgent(t *testing.T) { token := uuid.New() r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: token.String(), }) return r, token diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index c7f931438901d..c985234824458 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go index 0e72e02fcc9b9..7ed74e274cc9b 100644 --- a/coderd/httpmw/workspacebuildparam_test.go +++ b/coderd/httpmw/workspacebuildparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceBuildParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index 2f1f522b5a211..52731dc10f1cb 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceParam(t *testing.T) { ) r := httptest.NewRequest("GET", "/", nil) r.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: fmt.Sprintf("%s-%s", id, secret), }) diff --git a/coderd/users.go b/coderd/users.go index fbbbde5e250c1..92e2135b2fd37 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -690,7 +690,7 @@ func (*api) postLogout(rw http.ResponseWriter, _ *http.Request) { cookie := &http.Cookie{ // MaxAge < 0 means to delete the cookie now MaxAge: -1, - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Path: "/", } @@ -748,7 +748,7 @@ func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat // This format is consumed by the APIKey middleware. sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret) http.SetCookie(rw, &http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: sessionToken, Path: "/", HttpOnly: true, diff --git a/coderd/users_test.go b/coderd/users_test.go index 9c2846ed96cbd..e0e1033819526 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -122,7 +122,7 @@ func TestPostLogout(t *testing.T) { cookies := response.Cookies() require.Len(t, cookies, 1, "Exactly one cookie should be returned") - require.Equal(t, cookies[0].Name, httpmw.AuthCookie, "Cookie should be the auth cookie") + require.Equal(t, cookies[0].Name, httpmw.SessionTokenKey, "Cookie should be the auth cookie") require.Equal(t, cookies[0].MaxAge, -1, "Cookie should be set to delete") }) } diff --git a/codersdk/client.go b/codersdk/client.go index f1fdfce9622f8..39d08fa758e2a 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -64,7 +64,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return nil, xerrors.Errorf("create request: %w", err) } req.AddCookie(&http.Cookie{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }) if body != nil { @@ -99,7 +99,7 @@ func (c *Client) websocket(ctx context.Context, path string) (*websocket.Conn, e } apiURL.Path = path q := apiURL.Query() - q.Add(httpmw.AuthCookie, c.SessionToken) + q.Add(httpmw.SessionTokenKey, c.SessionToken) apiURL.RawQuery = q.Encode() //nolint:bodyclose diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index d64b42bc5faaa..c634b1de7ea2a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -188,7 +188,7 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context, logger slog.Logger) ( return agent.Metadata{}, nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }}) httpClient := &http.Client{ @@ -263,7 +263,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }}) httpClient := &http.Client{ @@ -351,7 +351,7 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec return nil, xerrors.Errorf("create cookie jar: %w", err) } jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, + Name: httpmw.SessionTokenKey, Value: c.SessionToken, }}) httpClient := &http.Client{ From 078c711308e2b1b0086f6cfe6cd8b90b4b767424 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 17 May 2022 20:40:15 +0000 Subject: [PATCH 07/13] fix auth test --- coderd/coderd_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index cc34d89ed6bee..de478615d2c12 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -128,6 +128,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "PUT:/api/v2/workspaces/{workspace}/autostop": {NoAuthorize: true}, "GET:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, "POST:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, + "POST:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, "POST:/api/v2/files": {NoAuthorize: true}, "GET:/api/v2/files/{hash}": {NoAuthorize: true}, From 184cd47c6e1d9a293bbc7f34d5adb4037c187990 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 17 May 2022 20:43:41 +0000 Subject: [PATCH 08/13] fix lint --- coderd/workspaces.go | 3 ++- codersdk/client.go | 6 +++--- codersdk/workspaces.go | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 70379c4afe31c..c5b5669e49889 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -9,7 +9,6 @@ import ( "net/http" "time" - "cdr.dev/slog" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" @@ -18,6 +17,8 @@ import ( "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" + "cdr.dev/slog" + "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" diff --git a/codersdk/client.go b/codersdk/client.go index 39d08fa758e2a..71786de4f0cf4 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -81,9 +81,9 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return resp, err } -// websocket opens a websocket connection on that path provided. -// The caller is responsible for closing the websocket.Conn. -func (c *Client) websocket(ctx context.Context, path string) (*websocket.Conn, error) { +// dialWebsocket opens a dialWebsocket connection on that path provided. +// The caller is responsible for closing the dialWebsocket.Conn. +func (c *Client) dialWebsocket(ctx context.Context, path string) (*websocket.Conn, error) { serverURL, err := c.URL.Parse(path) if err != nil { return nil, xerrors.Errorf("parse path: %w", err) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 31ed2a8dea67f..923a1e423f3be 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -124,7 +124,7 @@ func (w *WorkspaceWatcher) Close() error { } func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (*WorkspaceWatcher, error) { - conn, err := c.websocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id)) + conn, err := c.dialWebsocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id)) if err != nil { return nil, err } From 509beef1c4a6fdc36784b154f90d6b22b8f3dc39 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 17 May 2022 20:45:06 +0000 Subject: [PATCH 09/13] fix auth test --- coderd/coderd_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index de478615d2c12..30c6ad44aae01 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -128,7 +128,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "PUT:/api/v2/workspaces/{workspace}/autostop": {NoAuthorize: true}, "GET:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, "POST:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, - "POST:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, + "CONNECT:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, "POST:/api/v2/files": {NoAuthorize: true}, "GET:/api/v2/files/{hash}": {NoAuthorize: true}, From be27df51402dad44431dfd6eedf8607957ed7d5e Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 17 May 2022 20:57:54 +0000 Subject: [PATCH 10/13] fix auth again --- coderd/coderd.go | 2 +- coderd/coderd_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index a639982248596..743bc5a0cbe9c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -311,7 +311,7 @@ func New(options *Options) (http.Handler, func()) { r.Put("/", api.putWorkspaceAutostop) }) }) - r.HandleFunc("/watch", api.watchWorkspace) + r.Get("/watch", api.watchWorkspace) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 30c6ad44aae01..23b82b0d374c6 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -128,7 +128,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { "PUT:/api/v2/workspaces/{workspace}/autostop": {NoAuthorize: true}, "GET:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, "POST:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true}, - "CONNECT:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, + "GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true}, "POST:/api/v2/files": {NoAuthorize: true}, "GET:/api/v2/files/{hash}": {NoAuthorize: true}, From f029650d30328d483651bd2b63b858cf6d7ba5c5 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 18 May 2022 16:12:12 +0000 Subject: [PATCH 11/13] move to channel over WorkspaceWatcher --- coderd/workspaces_test.go | 17 +++++++------ codersdk/workspaces.go | 50 ++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 979cb784d2750..6658aa8481940 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -634,15 +634,14 @@ func TestWorkspaceWatcher(t *testing.T) { w, err := client.Workspace(context.Background(), workspace.ID) require.NoError(t, err) - ww, err := client.WatchWorkspace(context.Background(), w.ID) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + wc, err := client.WatchWorkspace(ctx, w.ID) require.NoError(t, err) - defer ww.Close() - for i := 0; i < 5; i++ { - _, err := ww.Read(context.Background()) - require.NoError(t, err) + for i := 0; i < 3; i++ { + _, more := <-wc + require.True(t, more) } - err = ww.Close() - require.NoError(t, err) - _, err = ww.Read(context.Background()) - require.Error(t, err) + cancel() + require.EqualValues(t, codersdk.Workspace{}, <-wc) } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 923a1e423f3be..d43219e0f9d65 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -100,38 +100,34 @@ func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } -type WorkspaceWatcher struct { - conn *websocket.Conn -} - -func (w *WorkspaceWatcher) Read(ctx context.Context) (Workspace, error) { - var ws Workspace - err := wsjson.Read(ctx, w.conn, &ws) - if err != nil { - return ws, xerrors.Errorf("read workspace: %w", err) - } - - return ws, nil -} - -func (w *WorkspaceWatcher) Close() error { - err := w.conn.Close(websocket.StatusNormalClosure, "") - if err != nil { - return xerrors.Errorf("closing workspace watcher: %w", err) - } - - return nil -} - -func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (*WorkspaceWatcher, error) { +func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Workspace, error) { conn, err := c.dialWebsocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id)) if err != nil { return nil, err } + wc := make(chan Workspace, 256) + + go func() { + defer close(wc) + defer conn.Close(websocket.StatusNormalClosure, "") + + for { + select { + case <-ctx.Done(): + return + default: + var ws Workspace + err := wsjson.Read(ctx, conn, &ws) + if err != nil { + conn.Close(websocket.StatusInternalError, "failed to read workspace") + return + } + wc <- ws + } + } + }() - return &WorkspaceWatcher{ - conn: conn, - }, nil + return wc, nil } // UpdateWorkspaceAutostartRequest is a request to update a workspace's autostart schedule. From 534bcdba192d5e4c22619fe251c195c92a50a986 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 18 May 2022 16:41:27 +0000 Subject: [PATCH 12/13] rebase and fix tests --- coderd/coderd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 743bc5a0cbe9c..cc411ef71e255 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -310,8 +310,8 @@ func New(options *Options) (http.Handler, func()) { r.Route("/autostop", func(r chi.Router) { r.Put("/", api.putWorkspaceAutostop) }) + r.Get("/watch", api.watchWorkspace) }) - r.Get("/watch", api.watchWorkspace) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( From db49672a6baf08d3df3258250b3303751cedcdc3 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 18 May 2022 16:45:24 +0000 Subject: [PATCH 13/13] rebase and fix merge --- coderd/workspaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index c5b5669e49889..1e567dfe77c40 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -589,7 +589,7 @@ func (api *api) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) return } - build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { _ = wsjson.Write(ctx, c, httpapi.Response{ Message: fmt.Sprintf("get workspace build: %s", err), pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy