Allow the zed app to connect to both the old and new rpc endpoints

Max Brunsfeld created

In the case of the new Next.js app, the app will follow a redirect
from 'zed.dev/rpc' to the subdomain where the rust service is hosted.
Until then, the app will connect directly to zed.dev/rpc.

Change summary

Cargo.lock                            | 19 +----
crates/client/Cargo.toml              |  2 
crates/client/src/channel.rs          |  2 
crates/client/src/client.rs           | 96 ++++++++++++++++++++--------
crates/diagnostics/src/diagnostics.rs |  2 
crates/project/src/project.rs         |  6 
crates/project/src/worktree.rs        | 24 +++---
crates/rpc/Cargo.toml                 |  2 
crates/server/Cargo.toml              |  2 
crates/server/src/auth.rs             |  7 ++
crates/server/src/rpc.rs              |  4 
crates/workspace/src/workspace.rs     |  2 
crates/zed/Cargo.toml                 |  1 
crates/zed/src/main.rs                |  2 
crates/zed/src/test.rs                |  6 
script/zed_with_local_servers         |  2 
16 files changed, 107 insertions(+), 72 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -486,9 +486,9 @@ dependencies = [
 
 [[package]]
 name = "async-tungstenite"
-version = "0.14.0"
+version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8645e929ec7964448a901db9da30cd2ae8c7fecf4d6176af427837531dbbb63b"
+checksum = "5682ea0913e5c20780fe5785abacb85a411e7437bf52a1bedb93ddb3972cb8dd"
 dependencies = [
  "async-tls",
  "futures-io",
@@ -2452,15 +2452,6 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
 
-[[package]]
-name = "input_buffer"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413"
-dependencies = [
- "bytes 1.0.1",
-]
-
 [[package]]
 name = "instant"
 version = "0.1.9"
@@ -5186,16 +5177,15 @@ checksum = "85e00391c1f3d171490a3f8bd79999b0002ae38d3da0d6a3a306c754b053d71b"
 
 [[package]]
 name = "tungstenite"
-version = "0.13.0"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fe8dada8c1a3aeca77d6b51a4f1314e0f4b8e438b7b1b71e3ddaca8080e4093"
+checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1"
 dependencies = [
  "base64 0.13.0",
  "byteorder",
  "bytes 1.0.1",
  "http",
  "httparse",
- "input_buffer",
  "log",
  "rand 0.8.3",
  "sha-1 0.9.6",
@@ -5698,7 +5688,6 @@ dependencies = [
  "anyhow",
  "async-recursion",
  "async-trait",
- "async-tungstenite",
  "chat_panel",
  "client",
  "clock",

crates/client/Cargo.toml 🔗

@@ -16,7 +16,7 @@ rpc = { path = "../rpc" }
 sum_tree = { path = "../sum_tree" }
 anyhow = "1.0.38"
 async-recursion = "0.3"
-async-tungstenite = { version = "0.14", features = ["async-tls"] }
+async-tungstenite = { version = "0.16", features = ["async-tls"] }
 futures = "0.3"
 image = "0.23"
 lazy_static = "1.4.0"

crates/client/src/channel.rs 🔗

@@ -599,8 +599,8 @@ mod tests {
     #[gpui::test]
     async fn test_channel_messages(mut cx: TestAppContext) {
         let user_id = 5;
-        let mut client = Client::new();
         let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
+        let mut client = Client::new(http_client.clone());
         let server = FakeServer::for_client(user_id, &mut client, &cx).await;
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 

crates/client/src/client.rs 🔗

@@ -12,6 +12,7 @@ use async_tungstenite::tungstenite::{
     http::{Request, StatusCode},
 };
 use gpui::{action, AsyncAppContext, Entity, ModelContext, MutableAppContext, Task};
+use http::HttpClient;
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
 use postage::{prelude::Stream, watch};
@@ -26,7 +27,7 @@ use std::{
     sync::{Arc, Weak},
     time::{Duration, Instant},
 };
-use surf::Url;
+use surf::{http::Method, Url};
 use thiserror::Error;
 use util::{ResultExt, TryFutureExt};
 
@@ -35,10 +36,8 @@ pub use rpc::*;
 pub use user::*;
 
 lazy_static! {
-    static ref COLLAB_URL: String =
-        std::env::var("ZED_COLLAB_URL").unwrap_or("https://collab.zed.dev:443".to_string());
-    static ref SITE_URL: String =
-        std::env::var("ZED_SITE_URL").unwrap_or("https://zed.dev".to_string());
+    static ref ZED_SERVER_URL: String =
+        std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string());
     static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
         .ok()
         .and_then(|s| if s.is_empty() { None } else { Some(s) });
@@ -56,6 +55,7 @@ pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
 
 pub struct Client {
     peer: Arc<Peer>,
+    http: Arc<dyn HttpClient>,
     state: RwLock<ClientState>,
     authenticate:
         Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
@@ -131,7 +131,7 @@ struct ClientState {
     heartbeat_interval: Duration,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct Credentials {
     pub user_id: u64,
     pub access_token: String,
@@ -171,9 +171,10 @@ impl Drop for Subscription {
 }
 
 impl Client {
-    pub fn new() -> Arc<Self> {
+    pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
         Arc::new(Self {
             peer: Peer::new(),
+            http,
             state: Default::default(),
             authenticate: None,
             establish_connection: None,
@@ -405,7 +406,6 @@ impl Client {
 
         match self.establish_connection(&credentials, cx).await {
             Ok(conn) => {
-                log::info!("connected to rpc address {}", *COLLAB_URL);
                 self.state.write().credentials = Some(credentials.clone());
                 if !used_keychain && IMPERSONATE_LOGIN.is_none() {
                     write_credentials_to_keychain(&credentials, cx).log_err();
@@ -416,7 +416,7 @@ impl Client {
             Err(EstablishConnectionError::Unauthorized) => {
                 self.state.write().credentials.take();
                 if used_keychain {
-                    cx.platform().delete_credentials(&COLLAB_URL).log_err();
+                    cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
                     self.set_status(Status::SignedOut, cx);
                     self.authenticate_and_connect(cx).await
                 } else {
@@ -523,20 +523,57 @@ impl Client {
                 format!("{} {}", credentials.user_id, credentials.access_token),
             )
             .header("X-Zed-Protocol-Version", rpc::PROTOCOL_VERSION);
+
+        let http = self.http.clone();
         cx.background().spawn(async move {
-            if let Some(host) = COLLAB_URL.strip_prefix("https://") {
-                let stream = smol::net::TcpStream::connect(host).await?;
-                let request = request.uri(format!("wss://{}/rpc", host)).body(())?;
-                let (stream, _) =
-                    async_tungstenite::async_tls::client_async_tls(request, stream).await?;
-                Ok(Connection::new(stream))
-            } else if let Some(host) = COLLAB_URL.strip_prefix("http://") {
-                let stream = smol::net::TcpStream::connect(host).await?;
-                let request = request.uri(format!("ws://{}/rpc", host)).body(())?;
-                let (stream, _) = async_tungstenite::client_async(request, stream).await?;
-                Ok(Connection::new(stream))
-            } else {
-                Err(anyhow!("invalid server url: {}", *COLLAB_URL))?
+            let mut rpc_url = format!("{}/rpc", *ZED_SERVER_URL);
+            let rpc_request = surf::Request::new(
+                Method::Get,
+                surf::Url::parse(&rpc_url).context("invalid ZED_SERVER_URL")?,
+            );
+            let rpc_response = http.send(rpc_request).await?;
+
+            if rpc_response.status().is_redirection() {
+                rpc_url = rpc_response
+                    .header("Location")
+                    .ok_or_else(|| anyhow!("missing location header in /rpc response"))?
+                    .as_str()
+                    .to_string();
+            }
+            // Until we switch the zed.dev domain to point to the new Next.js app, there
+            // will be no redirect required, and the app will connect directly to
+            // wss://zed.dev/rpc.
+            else if rpc_response.status() != surf::StatusCode::UpgradeRequired {
+                Err(anyhow!(
+                    "unexpected /rpc response status {}",
+                    rpc_response.status()
+                ))?
+            }
+
+            let mut rpc_url = surf::Url::parse(&rpc_url).context("invalid rpc url")?;
+            let rpc_host = rpc_url
+                .host_str()
+                .zip(rpc_url.port_or_known_default())
+                .ok_or_else(|| anyhow!("missing host in rpc url"))?;
+            let stream = smol::net::TcpStream::connect(rpc_host).await?;
+
+            log::info!("connected to rpc endpoint {}", rpc_url);
+
+            match rpc_url.scheme() {
+                "https" => {
+                    rpc_url.set_scheme("wss").unwrap();
+                    let request = request.uri(rpc_url.as_str()).body(())?;
+                    let (stream, _) =
+                        async_tungstenite::async_tls::client_async_tls(request, stream).await?;
+                    Ok(Connection::new(stream))
+                }
+                "http" => {
+                    rpc_url.set_scheme("ws").unwrap();
+                    let request = request.uri(rpc_url.as_str()).body(())?;
+                    let (stream, _) = async_tungstenite::client_async(request, stream).await?;
+                    Ok(Connection::new(stream))
+                }
+                _ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
             }
         })
     }
@@ -564,7 +601,7 @@ impl Client {
             // that the user is signing in from a Zed app running on the same device.
             let mut url = format!(
                 "{}/native_app_signin?native_app_port={}&native_app_public_key={}",
-                *SITE_URL, port, public_key_string
+                *ZED_SERVER_URL, port, public_key_string
             );
 
             if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
@@ -595,7 +632,8 @@ impl Client {
                             }
                         }
 
-                        let post_auth_url = format!("{}/native_app_signin_succeeded", *SITE_URL);
+                        let post_auth_url =
+                            format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
                         req.respond(
                             tiny_http::Response::empty(302).with_header(
                                 tiny_http::Header::from_bytes(
@@ -668,7 +706,7 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
 
     let (user_id, access_token) = cx
         .platform()
-        .read_credentials(&COLLAB_URL)
+        .read_credentials(&ZED_SERVER_URL)
         .log_err()
         .flatten()?;
     Some(Credentials {
@@ -679,7 +717,7 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
 
 fn write_credentials_to_keychain(credentials: &Credentials, cx: &AsyncAppContext) -> Result<()> {
     cx.platform().write_credentials(
-        &COLLAB_URL,
+        &ZED_SERVER_URL,
         &credentials.user_id.to_string(),
         credentials.access_token.as_bytes(),
     )
@@ -705,7 +743,7 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test::FakeServer;
+    use crate::test::{FakeHttpClient, FakeServer};
     use gpui::TestAppContext;
 
     #[gpui::test(iterations = 10)]
@@ -713,7 +751,7 @@ mod tests {
         cx.foreground().forbid_parking();
 
         let user_id = 5;
-        let mut client = Client::new();
+        let mut client = Client::new(FakeHttpClient::with_404_response());
         let server = FakeServer::for_client(user_id, &mut client, &cx).await;
 
         cx.foreground().advance_clock(Duration::from_secs(10));
@@ -733,7 +771,7 @@ mod tests {
         cx.foreground().forbid_parking();
 
         let user_id = 5;
-        let mut client = Client::new();
+        let mut client = Client::new(FakeHttpClient::with_404_response());
         let server = FakeServer::for_client(user_id, &mut client, &cx).await;
         let mut status = client.status();
         assert!(matches!(

crates/diagnostics/src/diagnostics.rs 🔗

@@ -582,7 +582,7 @@ mod tests {
     async fn test_diagnostics(mut cx: TestAppContext) {
         let settings = cx.update(WorkspaceParams::test).settings;
         let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
-        let client = Client::new();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let fs = Arc::new(FakeFs::new());
 

crates/project/src/project.rs 🔗

@@ -898,7 +898,7 @@ impl Collaborator {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use client::{http::ServerResponse, test::FakeHttpClient};
+    use client::test::FakeHttpClient;
     use fs::RealFs;
     use gpui::TestAppContext;
     use language::LanguageRegistry;
@@ -1004,8 +1004,8 @@ mod tests {
     fn build_project(cx: &mut TestAppContext) -> ModelHandle<Project> {
         let languages = Arc::new(LanguageRegistry::new());
         let fs = Arc::new(RealFs);
-        let client = client::Client::new();
-        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
+        let http_client = FakeHttpClient::with_404_response();
+        let client = client::Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
         cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
     }

crates/project/src/worktree.rs 🔗

@@ -3054,8 +3054,8 @@ mod tests {
         )
         .await;
 
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         let tree = Worktree::open_local(
@@ -3092,8 +3092,8 @@ mod tests {
             "file1": "the old contents",
         }));
 
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         let tree = Worktree::open_local(
@@ -3127,8 +3127,8 @@ mod tests {
         }));
         let file_path = dir.path().join("file1");
 
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         let tree = Worktree::open_local(
@@ -3176,7 +3176,8 @@ mod tests {
         }));
 
         let user_id = 5;
-        let mut client = Client::new();
+        let http_client = FakeHttpClient::with_404_response();
+        let mut client = Client::new(http_client.clone());
         let server = FakeServer::for_client(user_id, &mut client, &cx).await;
         let user_store = server.build_user_store(client.clone(), &mut cx).await;
         let tree = Worktree::open_local(
@@ -3221,7 +3222,7 @@ mod tests {
             1,
             1,
             initial_snapshot.to_proto(),
-            Client::new(),
+            Client::new(http_client.clone()),
             user_store,
             Default::default(),
             &mut cx.to_async(),
@@ -3327,8 +3328,8 @@ mod tests {
             }
         }));
 
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         let tree = Worktree::open_local(
@@ -3369,7 +3370,8 @@ mod tests {
     #[gpui::test]
     async fn test_buffer_deduping(mut cx: gpui::TestAppContext) {
         let user_id = 100;
-        let mut client = Client::new();
+        let http_client = FakeHttpClient::with_404_response();
+        let mut client = Client::new(http_client);
         let server = FakeServer::for_client(user_id, &mut client, &cx).await;
         let user_store = server.build_user_store(client.clone(), &mut cx).await;
 
@@ -3433,8 +3435,8 @@ mod tests {
             "file2": "def",
             "file3": "ghi",
         }));
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         let tree = Worktree::open_local(
@@ -3570,8 +3572,8 @@ mod tests {
 
         let initial_contents = "aaa\nbbbbb\nc\n";
         let dir = temp_tree(json!({ "the-file": initial_contents }));
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         let tree = Worktree::open_local(
@@ -3687,8 +3689,8 @@ mod tests {
             "b.rs": "const y: i32 = 1",
         }));
 
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         let tree = Worktree::open_local(
@@ -3755,8 +3757,8 @@ mod tests {
     #[gpui::test]
     async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
         let fs = Arc::new(FakeFs::new());
-        let client = Client::new();
         let http_client = FakeHttpClient::with_404_response();
+        let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
 
         fs.insert_tree(

crates/rpc/Cargo.toml 🔗

@@ -13,7 +13,7 @@ test-support = []
 [dependencies]
 anyhow = "1.0"
 async-lock = "2.4"
-async-tungstenite = "0.14"
+async-tungstenite = "0.16"
 base64 = "0.13"
 futures = "0.3"
 log = "0.4"

crates/server/Cargo.toml 🔗

@@ -19,7 +19,7 @@ rpc = { path = "../rpc" }
 anyhow = "1.0.40"
 async-std = { version = "1.8.0", features = ["attributes"] }
 async-trait = "0.1.50"
-async-tungstenite = "0.14"
+async-tungstenite = "0.16"
 base64 = "0.13"
 clap = "=3.0.0-beta.2"
 comrak = "0.10"

crates/server/src/auth.rs 🔗

@@ -112,6 +112,9 @@ pub fn add_routes(app: &mut Server<Arc<AppState>>) {
     app.at("/sign_in").get(get_sign_in);
     app.at("/sign_out").post(post_sign_out);
     app.at("/auth_callback").get(get_auth_callback);
+    app.at("/native_app_signin").get(get_sign_in);
+    app.at("/native_app_signin_succeeded")
+        .get(get_app_signin_success);
 }
 
 #[derive(Debug, Deserialize)]
@@ -166,6 +169,10 @@ async fn get_sign_in(mut request: Request) -> tide::Result {
     Ok(tide::Redirect::new(auth_url).into())
 }
 
+async fn get_app_signin_success(_: Request) -> tide::Result {
+    Ok(tide::Redirect::new("/").into())
+}
+
 async fn get_auth_callback(mut request: Request) -> tide::Result {
     #[derive(Debug, Deserialize)]
     struct Query {

crates/server/src/rpc.rs 🔗

@@ -2440,9 +2440,10 @@ mod tests {
         }
 
         async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
+            let http = FakeHttpClient::with_404_response();
             let user_id = self.app_state.db.create_user(name, false).await.unwrap();
             let client_name = name.to_string();
-            let mut client = Client::new();
+            let mut client = Client::new(http.clone());
             let server = self.server.clone();
             let connection_killers = self.connection_killers.clone();
             let forbid_connections = self.forbid_connections.clone();
@@ -2489,7 +2490,6 @@ mod tests {
                     })
                 });
 
-            let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) });
             client
                 .authenticate_and_connect(&cx.to_async())
                 .await

crates/workspace/src/workspace.rs 🔗

@@ -368,10 +368,10 @@ impl WorkspaceParams {
     pub fn test(cx: &mut MutableAppContext) -> Self {
         let fs = Arc::new(project::FakeFs::new());
         let languages = Arc::new(LanguageRegistry::new());
-        let client = Client::new();
         let http_client = client::test::FakeHttpClient::new(|_| async move {
             Ok(client::http::ServerResponse::new(404))
         });
+        let client = Client::new(http_client.clone());
         let theme =
             gpui::fonts::with_font_cache(cx.font_cache().clone(), || theme::Theme::default());
         let settings = Settings::new("Courier", cx.font_cache(), Arc::new(theme)).unwrap();

crates/zed/Cargo.toml 🔗

@@ -55,7 +55,6 @@ workspace = { path = "../workspace" }
 anyhow = "1.0.38"
 async-recursion = "0.3"
 async-trait = "0.1"
-async-tungstenite = { version = "0.14", features = ["async-tls"] }
 crossbeam-channel = "0.5.0"
 ctor = "0.1.20"
 dirs = "3.0"

crates/zed/src/main.rs 🔗

@@ -48,8 +48,8 @@ fn main() {
     languages.set_theme(&settings.borrow().theme.editor.syntax);
 
     app.run(move |cx| {
-        let client = client::Client::new();
         let http = http::client();
+        let client = client::Client::new(http.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         let mut entry_openers = Vec::new();
 

crates/zed/src/test.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{assets::Assets, build_window_options, build_workspace, AppState};
-use client::{http::ServerResponse, test::FakeHttpClient, ChannelList, Client, UserStore};
+use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
 use gpui::{AssetSource, MutableAppContext};
 use language::LanguageRegistry;
 use parking_lot::Mutex;
@@ -20,8 +20,8 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     editor::init(cx, &mut entry_openers);
     let (settings_tx, settings) = watch::channel_with(build_settings(cx));
     let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
-    let client = Client::new();
-    let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
+    let http = FakeHttpClient::with_404_response();
+    let client = Client::new(http.clone());
     let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
     let mut languages = LanguageRegistry::new();
     languages.add(Arc::new(language::Language::new(

script/zed_with_local_servers 🔗

@@ -1 +1 @@
-ZED_SITE_URL=http://localhost:3000 ZED_COLLAB_URL=http://localhost:8080 cargo run $@
+ZED_SERVER_URL=http://localhost:3000 cargo run $@