Use request uri to establish SSE connection (#736)

* Use request uri to establish SSE connection
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 6fbafb1..64fb739 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## 0.7.5-dev
 
+- The injected client's connection is now based off the request URI.
 - Fix an issue where resuming while paused at the start would cause an error.
 - Expose the `ChromeDebugException` class for error handling purposes.
 
diff --git a/dwds/lib/src/handlers/injected_handler.dart b/dwds/lib/src/handlers/injected_handler.dart
index ba3e862..eafe0c7 100644
--- a/dwds/lib/src/handlers/injected_handler.dart
+++ b/dwds/lib/src/handlers/injected_handler.dart
@@ -30,7 +30,7 @@
         int extensionPort}) =>
     (innerHandler) {
       return (Request request) async {
-        if (request.url.path == '$_clientScript.js') {
+        if (request.url.path.endsWith('$_clientScript.js')) {
           var uri = await Isolate.resolvePackageUri(
               Uri.parse('package:$_clientScript.js'));
           var result = await File(uri.toFilePath()).readAsString();
@@ -67,9 +67,16 @@
             var mainFunction = bodyLines[extensionIndex + 1]
                 .replaceAll('main()', 'main')
                 .trim();
-            body += _injectedClientJs(configuration, appId, mainFunction,
-                extensionHostname: extensionHostname,
-                extensionPort: extensionPort);
+            var requestedUriBase = '${request.requestedUri.scheme}'
+                '://${request.requestedUri.authority}';
+            body += _injectedClientJs(
+              configuration,
+              appId,
+              mainFunction,
+              requestedUriBase,
+              extensionHostname: extensionHostname,
+              extensionPort: extensionPort,
+            );
             body += bodyLines.sublist(extensionIndex + 2).join('\n');
             // Change the hot restart handler to re-assign
             // `window.$dartRunMain` to the new main, instead of invoking it.
@@ -89,14 +96,20 @@
     };
 
 String _injectedClientJs(
-    ReloadConfiguration configuration, String appId, String mainFunction,
-    {String extensionHostname, int extensionPort}) {
+  ReloadConfiguration configuration,
+  String appId,
+  String mainFunction,
+  String requestedUriBase, {
+  String extensionHostname,
+  int extensionPort,
+}) {
   var injectedBody = '// Injected by webdev for build results support.\n'
       'window.\$dartAppId = "$appId";\n'
       'window.\$dartRunMain = $mainFunction;\n'
       'window.\$dartReloadConfiguration = "$configuration";\n'
       'window.\$dartLoader.forceLoadModule("$_clientScript");\n'
       'window.\$dartModuleStrategy = "$loadModule";\n'
+      'window.\$dartUriBase = "$requestedUriBase";\n'
       'window.\$loadModuleConfig = $loadModule;\n';
   if (extensionPort != null && extensionHostname != null) {
     injectedBody += 'window.\$extensionHostname = "$extensionHostname";\n'
diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js
index 51173eb..c4cdccf 100644
--- a/dwds/lib/src/injected/client.js
+++ b/dwds/lib/src/injected/client.js
@@ -2213,6 +2213,17 @@
         return string.replace(/[[\]{}()*+?.\\^$|]/g, "\\$&");
       return string;
     },
+    stringReplaceFirstUnchecked: function(receiver, pattern, replacement, startIndex) {
+      var index = receiver.indexOf(pattern, startIndex);
+      if (index < 0)
+        return receiver;
+      return H.stringReplaceRangeUnchecked(receiver, index, index + pattern.length, replacement);
+    },
+    stringReplaceRangeUnchecked: function(receiver, start, end, replacement) {
+      var prefix = receiver.substring(0, start),
+        suffix = receiver.substring(end);
+      return prefix + replacement + suffix;
+    },
     ConstantMapView: function ConstantMapView(t0, t1) {
       this._collection$_map = t0;
       this.$ti = t1;
@@ -2829,8 +2840,8 @@
     noSuchMethod$1$: function(receiver, a0) {
       return J.getInterceptor$(receiver).noSuchMethod$1(receiver, a0);
     },
-    replaceRange$3$s: function(receiver, a0, a1, a2) {
-      return J.getInterceptor$s(receiver).replaceRange$3(receiver, a0, a1, a2);
+    replaceRange$3$asx: function(receiver, a0, a1, a2) {
+      return J.getInterceptor$asx(receiver).replaceRange$3(receiver, a0, a1, a2);
     },
     skip$1$ax: function(receiver, a0) {
       return J.getInterceptor$ax(receiver).skip$1(receiver, a0);
@@ -4913,6 +4924,10 @@
     RangeError$range: function(invalidValue, minValue, maxValue, $name, message) {
       return new P.RangeError(minValue, maxValue, true, invalidValue, $name, "Invalid value");
     },
+    RangeError_checkValueInInterval: function(value, minValue, maxValue, $name) {
+      if (value < minValue || value > maxValue)
+        throw H.wrapException(P.RangeError$range(value, minValue, maxValue, $name, null));
+    },
     RangeError_checkValidRange: function(start, end, $length) {
       if (0 > start || start > $length)
         throw H.wrapException(P.RangeError$range(start, 0, $length, "start", null));
@@ -5088,7 +5103,7 @@
                   pathStart0 = pathStart - 4;
                   queryStart -= 4;
                   fragmentStart -= 4;
-                  uri = J.replaceRange$3$s(uri, portStart, pathStart, "");
+                  uri = J.replaceRange$3$asx(uri, portStart, pathStart, "");
                   end -= 3;
                   pathStart = pathStart0;
                 }
@@ -7485,7 +7500,7 @@
     DeserializationError_DeserializationError: function(json, type, error) {
       var limitedJson = J.toString$0$(json),
         t1 = limitedJson.length;
-      return new U.DeserializationError(t1 > 80 ? J.replaceRange$3$s(limitedJson, 77, t1, "...") : limitedJson, type, error);
+      return new U.DeserializationError(t1 > 80 ? J.replaceRange$3$asx(limitedJson, 77, t1, "...") : limitedJson, type, error);
     },
     Serializers_Serializers_closure: function Serializers_Serializers_closure() {
     },
@@ -7693,6 +7708,18 @@
     main: function() {
       return P.runZoned(new D.main_closure(), new D.main_closure0(), [P.Future, -1]);
     },
+    _fixProtocol: function(url) {
+      var _s8_ = "https://";
+      if (window.location.protocol === "https:" && !C.JSString_methods.startsWith$1(url, _s8_))
+        if (C.JSString_methods.startsWith$1(url, "http://localhost"))
+          return url;
+        else {
+          P.RangeError_checkValueInInterval(0, 0, url.length, "startIndex");
+          return H.stringReplaceFirstUnchecked(url, "http://", _s8_, 0);
+        }
+      else
+        return url;
+    },
     _isChrome: function() {
       return J.contains$1$asx(window.navigator.userAgent, "Chrome") && !J.contains$1$asx(window.navigator.userAgent, "Edg");
     },
@@ -8727,8 +8754,13 @@
         throw H.wrapException(H.diagnoseIndexError(receiver, index));
       return receiver.charCodeAt(index);
     },
-    allMatches$1: function(receiver, string) {
-      return new H._StringAllMatchesIterable(string, receiver, 0);
+    allMatches$2: function(receiver, string, start) {
+      if (start > string.length)
+        throw H.wrapException(P.RangeError$range(start, 0, string.length, null, null));
+      return new H._StringAllMatchesIterable(string, receiver, start);
+    },
+    allMatches$1: function($receiver, string) {
+      return this.allMatches$2($receiver, string, 0);
     },
     $add: function(receiver, other) {
       H.stringTypeCheck(other);
@@ -8744,11 +8776,8 @@
       return other === this.substring$1(receiver, t1 - otherLength);
     },
     replaceRange$3: function(receiver, start, end, replacement) {
-      var prefix, suffix;
       end = P.RangeError_checkValidRange(start, end, receiver.length);
-      prefix = receiver.substring(0, start);
-      suffix = receiver.substring(end);
-      return prefix + replacement + suffix;
+      return H.stringReplaceRangeUnchecked(receiver, start, end, replacement);
     },
     startsWith$2: function(receiver, pattern, index) {
       var endIndex;
@@ -10030,8 +10059,13 @@
         return;
       return new H._MatchImplementation(m);
     },
-    allMatches$1: function(_, string) {
-      return new H._AllMatchesIterable(this, string, 0);
+    allMatches$2: function(_, string, start) {
+      if (start > string.length)
+        throw H.wrapException(P.RangeError$range(start, 0, string.length, null, null));
+      return new H._AllMatchesIterable(this, string, start);
+    },
+    allMatches$1: function($receiver, string) {
+      return this.allMatches$2($receiver, string, 0);
     },
     _execGlobal$2: function(string, start) {
       var match,
@@ -22621,7 +22655,7 @@
     call$0: function() {
       var $async$goto = 0,
         $async$completer = P._makeAsyncAwaitCompleter(P.Null),
-        t1, t2, t3, client, clientId, t4, restarter, manager;
+        t1, t2, t3, t4, client, clientId, restarter, manager;
       var $async$call$0 = P._wrapJsFunctionForAsync(function($async$errorCode, $async$result) {
         if ($async$errorCode === 1)
           return P._asyncRethrow($async$result, $async$completer);
@@ -22633,23 +22667,24 @@
                 t1 = F.Uuid$().v1$0();
                 self.$dartAppInstanceId = t1;
               }
-              t1 = P.String;
-              t2 = P.StreamController_StreamController(t1);
-              t3 = P.StreamController_StreamController(t1);
-              client = new M.SseClient(t2, t3, N.Logger_Logger("SseClient"), P.StreamController_StreamController(null));
+              t1 = D._fixProtocol(H.S(self.$dartUriBase) + "/$sseHandler");
+              t2 = P.String;
+              t3 = P.StreamController_StreamController(t2);
+              t4 = P.StreamController_StreamController(t2);
+              client = new M.SseClient(t3, t4, N.Logger_Logger("SseClient"), P.StreamController_StreamController(null));
               clientId = F.Uuid$().v1$0();
-              client._eventSource = W.EventSource__factoryEventSource("/$sseHandler?sseClientId=" + clientId, P.LinkedHashMap_LinkedHashMap$_literal(["withCredentials", true], t1, null));
-              client._serverUrl = "/$sseHandler?sseClientId=" + clientId;
-              t1 = H.getTypeArgumentByIndex(t3, 0);
-              new P._ControllerStream(t3, [t1]).listen$2$onDone(client.get$_onOutgoingMessage(), client.get$_onOutgoingDone());
+              client._eventSource = W.EventSource__factoryEventSource(t1 + "?sseClientId=" + clientId, P.LinkedHashMap_LinkedHashMap$_literal(["withCredentials", true], t2, null));
+              client._serverUrl = t1 + "?sseClientId=" + clientId;
+              t1 = H.getTypeArgumentByIndex(t4, 0);
+              new P._ControllerStream(t4, [t1]).listen$2$onDone(client.get$_onOutgoingMessage(), client.get$_onOutgoingDone());
               C.EventSource_methods.addEventListener$2(client._eventSource, "message", client.get$_onIncomingMessage());
               C.EventSource_methods.addEventListener$2(client._eventSource, "control", client.get$_onIncomingControlMessage());
-              t4 = W.Event;
-              W._EventStreamSubscription$(client._eventSource, "error", H.functionTypeCheck(t2.get$addError(), {func: 1, ret: -1, args: [t4]}), false, t4);
+              t2 = W.Event;
+              W._EventStreamSubscription$(client._eventSource, "error", H.functionTypeCheck(t3.get$addError(), {func: 1, ret: -1, args: [t2]}), false, t2);
               client._startPostingMessages$0();
-              t4 = new W._EventStream(client._eventSource, "open", false, [t4]);
+              t2 = new W._EventStream(client._eventSource, "open", false, [t2]);
               $async$goto = 2;
-              return P._asyncAwait(t4.get$first(t4), $async$call$0);
+              return P._asyncAwait(t2.get$first(t2), $async$call$0);
             case 2:
               // returning from await.
               $async$goto = J.$eq$(self.$dartModuleStrategy, "require") ? 3 : 5;
@@ -22673,18 +22708,18 @@
             case 4:
               // join
               manager = new Q.ReloadingManager(client, restarter);
-              t4 = P.allowInterop(new D.main__closure(manager), {func: 1, ret: [S.Promise, -2]});
-              self.$dartHotRestart = t4;
-              t4 = P.allowInterop(new D.main__closure0(client), {func: 1, ret: -1});
-              self.$launchDevTools = t4;
-              new P._ControllerStream(t2, [H.getTypeArgumentByIndex(t2, 0)]).listen$2$onError(new D.main__closure1(manager, client), new D.main__closure2());
-              t2 = W.KeyboardEvent;
-              W._EventStreamSubscription$(window, "keydown", H.functionTypeCheck(new D.main__closure3(), {func: 1, ret: -1, args: [t2]}), false, t2);
+              t2 = P.allowInterop(new D.main__closure(manager), {func: 1, ret: [S.Promise, -2]});
+              self.$dartHotRestart = t2;
+              t2 = P.allowInterop(new D.main__closure0(client), {func: 1, ret: -1});
+              self.$launchDevTools = t2;
+              new P._ControllerStream(t3, [H.getTypeArgumentByIndex(t3, 0)]).listen$2$onError(new D.main__closure1(manager, client), new D.main__closure2());
+              t3 = W.KeyboardEvent;
+              W._EventStreamSubscription$(window, "keydown", H.functionTypeCheck(new D.main__closure3(), {func: 1, ret: -1, args: [t3]}), false, t3);
               if (D._isChrome()) {
                 t2 = $.$get$serializers();
-                t4 = new E.ConnectRequestBuilder();
-                H.functionTypeCheck(new D.main__closure4(), {func: 1, ret: -1, args: [E.ConnectRequestBuilder]}).call$1(t4);
-                t3.add$1(0, H.assertSubtypeOfRuntimeType(C.C_JsonCodec.encode$2$toEncodable(t2.serialize$1(t4.build$0()), null), t1));
+                t3 = new E.ConnectRequestBuilder();
+                H.functionTypeCheck(new D.main__closure4(), {func: 1, ret: -1, args: [E.ConnectRequestBuilder]}).call$1(t3);
+                t4.add$1(0, H.assertSubtypeOfRuntimeType(C.C_JsonCodec.encode$2$toEncodable(t2.serialize$1(t3.build$0()), null), t1));
               } else
                 self.$dartRunMain.call$0();
               // implicit return
diff --git a/dwds/web/client.dart b/dwds/web/client.dart
index dd6201f..4f331d0 100644
--- a/dwds/web/client.dart
+++ b/dwds/web/client.dart
@@ -33,7 +33,7 @@
     // Test apps may already have this set.
     dartAppInstanceId ??= Uuid().v1();
 
-    var client = SseClient(r'/$sseHandler');
+    var client = SseClient(_fixProtocol('$dartUriBase/\$sseHandler'));
     // Ensure the SSE connection is established before proceeding.
     // Note that `onOpen` is a broadcast stream so we must listen for this
     // immediately.
@@ -133,12 +133,31 @@
   });
 }
 
+/// Returns [url] modified if necessary so that, if the current page is served
+/// over `https`, then the URL is converted to `https`. Localhost is treated
+/// as a special case and not modified.
+String _fixProtocol(String url) {
+  if (window.location.protocol == 'https:' && !url.startsWith('https://')) {
+    // Chrome seems to allow mixed content from localhost.
+    if (url.startsWith('http://localhost')) {
+      return url;
+    } else {
+      return url.replaceFirst('http://', 'https://');
+    }
+  } else {
+    return url;
+  }
+}
+
 @JS(r'$dartAppId')
 external String get dartAppId;
 
 @JS(r'$dartAppInstanceId')
 external String get dartAppInstanceId;
 
+@JS(r'$dartUriBase')
+external String get dartUriBase;
+
 @JS(r'$dartAppInstanceId')
 external set dartAppInstanceId(String id);