[dtd] Add support for '--disable-service-auth-codes'

See https://github.com/dart-lang/sdk/issues/54932

Change-Id: Ie85eb76eb5e8c01dda957d038286ae498748ca70
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/373340
Reviewed-by: Dan Chevalier <danchevalier@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dartdev/lib/src/commands/tooling_daemon.dart b/pkg/dartdev/lib/src/commands/tooling_daemon.dart
index 44e6f14..d1c3b22 100644
--- a/pkg/dartdev/lib/src/commands/tooling_daemon.dart
+++ b/pkg/dartdev/lib/src/commands/tooling_daemon.dart
@@ -4,7 +4,6 @@
 
 import 'dart:async';
 
-import 'package:args/args.dart';
 import 'package:dtd_impl/dart_tooling_daemon.dart' as dtd
     show DartToolingDaemonOptions;
 
@@ -23,12 +22,10 @@
           commandDescription,
           verbose,
           hidden: !verbose,
-        );
-
-  @override
-  ArgParser createArgParser() {
-    return dtd.DartToolingDaemonOptions.createArgParser(
-      usageLineLength: dartdevUsageLineLength,
+        ) {
+    dtd.DartToolingDaemonOptions.populateArgOptions(
+      argParser,
+      verbose: verbose,
     );
   }
 
diff --git a/pkg/dtd_impl/lib/dart_tooling_daemon.dart b/pkg/dtd_impl/lib/dart_tooling_daemon.dart
index 4b6be36..d83e5e2 100644
--- a/pkg/dtd_impl/lib/dart_tooling_daemon.dart
+++ b/pkg/dtd_impl/lib/dart_tooling_daemon.dart
@@ -26,69 +26,91 @@
 enum DartToolingDaemonOptions {
   // Used when executing a training run while generating an AppJIT snapshot as
   // part of an SDK build.
-  train.flag(negatable: false, hide: true),
+  train.flag('train', negatable: false, hide: true),
   machine.flag(
+    'machine',
     negatable: false,
     help: 'Sets output format to JSON for consumption in tools.',
   ),
   port.option(
+    'port',
     defaultsTo: '0',
     help: 'Sets the port to bind DTD to (0 for automatic port).',
   ),
   unrestricted.flag(
+    'unrestricted',
     negatable: false,
     help: 'Disables restrictions on services registered by DTD.',
   ),
+  disableServiceAuthCodes.flag(
+    'disable-service-auth-codes',
+    negatable: false,
+    // This text mirrors what's in dartdev/commands/run for VM Service.
+    help: 'Disables the requirement for an authentication code to '
+        'communicate with DTD. Authentication codes help '
+        'protect against CSRF attacks, so it is not recommended to '
+        'disable them unless behind a firewall on a secure device.',
+    verbose: true,
+  ),
   fakeAnalytics.flag(
+    'fakeAnalytics',
     negatable: false,
     help: 'Uses fake analytics instances for the UnifiedAnalytics service.',
     hide: true,
   );
 
-  const DartToolingDaemonOptions.flag({
+  const DartToolingDaemonOptions.flag(
+    this.name, {
     this.negatable = true,
+    this.verbose = false,
     this.hide = false,
     this.help,
   })  : _kind = _DartToolingDaemonOptionKind.flag,
         defaultsTo = null;
 
-  const DartToolingDaemonOptions.option({
+  const DartToolingDaemonOptions.option(
+    this.name, {
     this.defaultsTo,
     this.help,
   })  : _kind = _DartToolingDaemonOptionKind.option,
         negatable = false,
+        verbose = false,
         hide = false;
 
+  final String name;
   final _DartToolingDaemonOptionKind _kind;
   final String? defaultsTo;
   final bool negatable;
   final bool hide;
   final String? help;
 
-  /// Returns an argument parser that can be used to configure the daemon.
-  static ArgParser createArgParser({
-    int? usageLineLength,
+  /// Show in help only when --verbose passed.
+  final bool verbose;
+
+  /// Populates an argument parser that can be used to configure the daemon.
+  static void populateArgOptions(
+    ArgParser argParser, {
+    bool verbose = false,
   }) {
-    final argParser = ArgParser(usageLineLength: usageLineLength);
     for (final entry in DartToolingDaemonOptions.values) {
+      final hide = entry.hide || (entry.verbose && !verbose);
       switch (entry._kind) {
         case _DartToolingDaemonOptionKind.flag:
           argParser.addFlag(
             entry.name,
             negatable: entry.negatable,
-            hide: entry.hide,
+            hide: hide,
             help: entry.help,
           );
         case _DartToolingDaemonOptionKind.option:
           argParser.addOption(
             entry.name,
-            hide: entry.hide,
+            hide: hide,
             help: entry.help,
             defaultsTo: entry.defaultsTo,
           );
       }
     }
-    return argParser;
   }
 }
 
@@ -105,10 +127,12 @@
   DartToolingDaemon._({
     required this.secret,
     required bool unrestrictedMode,
+    bool disableServiceAuthCodes = false,
     bool ipv6 = false,
     bool shouldLogRequests = false,
     bool useFakeAnalytics = false,
   })  : _ipv6 = ipv6,
+        _uriAuthCode = disableServiceAuthCodes ? null : _generateSecret(),
         _shouldLogRequests = shouldLogRequests {
     streamManager = DTDStreamManager(this);
     clientManager = DTDClientManager();
@@ -135,12 +159,12 @@
 
   final String secret;
 
-  /// Any requests to DTD must have this token as the first element of the
-  /// uri path.
+  /// If non-null, any requests to DTD must have this code as the first element
+  /// of the uri path.
   ///
   /// This provides an obfuscation step to prevent bad actors from stumbling
   /// onto the dtd address.
-  final String _uriToken = _generateSecret();
+  final String? _uriAuthCode;
 
   /// The uri of the current [DartToolingDaemon] service.
   Uri? get uri => _uri;
@@ -187,7 +211,7 @@
       scheme: 'ws',
       host: host,
       port: _server.port,
-      path: '/$_uriToken',
+      path: _uriAuthCode != null ? '/$_uriAuthCode' : '',
     );
   }
 
@@ -205,7 +229,8 @@
     bool shouldLogRequests = false,
     SendPort? sendPort,
   }) async {
-    final argParser = DartToolingDaemonOptions.createArgParser();
+    final argParser = ArgParser();
+    DartToolingDaemonOptions.populateArgOptions(argParser);
     final parsedArgs = argParser.parse(args);
     if (parsedArgs.wasParsed(DartToolingDaemonOptions.train.name)) {
       return null;
@@ -213,6 +238,8 @@
     final machineMode = parsedArgs[DartToolingDaemonOptions.machine.name];
     final unrestrictedMode =
         parsedArgs[DartToolingDaemonOptions.unrestricted.name];
+    final disableServiceAuthCodes =
+        parsedArgs[DartToolingDaemonOptions.disableServiceAuthCodes.name];
     final useFakeAnalytics =
         parsedArgs[DartToolingDaemonOptions.fakeAnalytics.name];
     final port =
@@ -222,6 +249,7 @@
     final dtd = DartToolingDaemon._(
       secret: secret,
       unrestrictedMode: unrestrictedMode,
+      disableServiceAuthCodes: disableServiceAuthCodes,
       ipv6: ipv6,
       shouldLogRequests: shouldLogRequests,
       useFakeAnalytics: useFakeAnalytics,
@@ -262,15 +290,17 @@
   }
 
   Handler _uriTokenHandler(Handler innerHandler) => (Request request) {
-        final forbidden =
-            Response.forbidden('missing or invalid authentication code');
-        final pathSegments = request.url.pathSegments;
-        if (pathSegments.isEmpty) {
-          return forbidden;
-        }
-        final token = pathSegments[0];
-        if (token != _uriToken) {
-          return forbidden;
+        if (_uriAuthCode != null) {
+          final forbidden =
+              Response.forbidden('missing or invalid authentication code');
+          final pathSegments = request.url.pathSegments;
+          if (pathSegments.isEmpty) {
+            return forbidden;
+          }
+          final clientProvidedCode = pathSegments[0];
+          if (clientProvidedCode != _uriAuthCode) {
+            return forbidden;
+          }
         }
         return innerHandler(request);
       };
diff --git a/pkg/dtd_impl/test/dtd_test.dart b/pkg/dtd_impl/test/dtd_test.dart
index 7857696..21d5c06 100644
--- a/pkg/dtd_impl/test/dtd_test.dart
+++ b/pkg/dtd_impl/test/dtd_test.dart
@@ -20,38 +20,57 @@
     await dtd?.close();
   });
 
-  test(
-      'forbids connections where the uri token is not the first element in the path',
-      () async {
-    dtd = await DartToolingDaemon.startService([]);
+  group('auth tokens', () {
+    test('forbids connections where the URI auth code is invalid', () async {
+      dtd = await DartToolingDaemon.startService([]);
+      expect(dtd!.uri!.path, isNotEmpty); // Has code.
 
-    expect(
-      () async => await WebSocket.connect(
-        dtd!.uri!.replace(path: 'someInvalidToken').toString(), // invalid token
-      ),
-      throwsA(
-        predicate(
-          (p0) =>
-              p0 is WebSocketException &&
-              RegExp("^Connection to '.*' was not upgraded to websocket\$")
-                  .hasMatch(p0.message),
+      expect(
+        () async => await WebSocket.connect(
+          dtd!.uri!.replace(path: 'someInvalidCode').toString(),
         ),
-      ),
-    );
+        throwsA(
+          predicate(
+            (p0) =>
+                p0 is WebSocketException &&
+                RegExp("^Connection to '.*' was not upgraded to websocket\$")
+                    .hasMatch(p0.message),
+          ),
+        ),
+      );
+    });
 
-    expect(
-      () async => await WebSocket.connect(
-        dtd!.uri!.replace(path: '').toString(), // no token
-      ),
-      throwsA(
-        predicate(
-          (p0) =>
-              p0 is WebSocketException &&
-              RegExp("^Connection to '.*' was not upgraded to websocket\$")
-                  .hasMatch(p0.message),
+    test('forbids connections where the URI auth code is missing', () async {
+      dtd = await DartToolingDaemon.startService([]);
+
+      expect(
+        () async => await WebSocket.connect(
+          dtd!.uri!.replace(path: '').toString(),
         ),
-      ),
-    );
+        throwsA(
+          predicate(
+            (p0) =>
+                p0 is WebSocketException &&
+                RegExp("^Connection to '.*' was not upgraded to websocket\$")
+                    .hasMatch(p0.message),
+          ),
+        ),
+      );
+    });
+
+    test(
+        'allows connections with no URI auth code if started with --disable-service-auth-codes',
+        () async {
+      dtd = await DartToolingDaemon.startService([
+        '--disable-service-auth-codes',
+      ]);
+
+      expect(dtd!.uri!.path, isEmpty); // No code.
+
+      // Expect no exception.
+      final ws = await WebSocket.connect(dtd!.uri!.toString());
+      await ws.close();
+    });
   });
 
   group('dtd', () {