Added `strictProtocolChecks` named parameter to `Peer` and `Server` constructors (#51)

Allows creating servers which are lenient towards misbehaving clients.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c95f7f1..c3eec9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.2.0
+
+* Added `strictProtocolChecks` named parameter to `Server` and `Peer`
+  constructors. Setting this parameter to false will result in the server not
+  rejecting requests missing the `jsonrpc` parameter.
+
 ## 2.1.1
 
 * Fixed issue where throwing `RpcException.methodNotFound` in an asynchronous
diff --git a/lib/src/peer.dart b/lib/src/peer.dart
index cd51a7c..eeb7cd9 100644
--- a/lib/src/peer.dart
+++ b/lib/src/peer.dart
@@ -44,6 +44,9 @@
   @override
   ErrorCallback get onUnhandledError => _server?.onUnhandledError;
 
+  @override
+  bool get strictProtocolChecks => _server.strictProtocolChecks;
+
   /// Creates a [Peer] that communicates over [channel].
   ///
   /// Note that the peer won't begin listening to [channel] until [Peer.listen]
@@ -51,10 +54,17 @@
   ///
   /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
   /// If this is not provided, unhandled exceptions will be swallowed.
-  Peer(StreamChannel<String> channel, {ErrorCallback onUnhandledError})
+  ///
+  /// If [strictProtocolChecks] is false, the underlying [Server] will accept
+  /// some requests which are not conformant with the JSON-RPC 2.0
+  /// specification. In particular, requests missing the `jsonrpc` parameter
+  /// will be accepted.
+  Peer(StreamChannel<String> channel,
+      {ErrorCallback onUnhandledError, bool strictProtocolChecks = true})
       : this.withoutJson(
             jsonDocument.bind(channel).transform(respondToFormatExceptions),
-            onUnhandledError: onUnhandledError);
+            onUnhandledError: onUnhandledError,
+            strictProtocolChecks: strictProtocolChecks);
 
   /// Creates a [Peer] that communicates using decoded messages over [channel].
   ///
@@ -66,11 +76,18 @@
   ///
   /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
   /// If this is not provided, unhandled exceptions will be swallowed.
-  Peer.withoutJson(StreamChannel channel, {ErrorCallback onUnhandledError})
+  ///
+  /// If [strictProtocolChecks] is false, the underlying [Server] will accept
+  /// some requests which are not conformant with the JSON-RPC 2.0
+  /// specification. In particular, requests missing the `jsonrpc` parameter
+  /// will be accepted.
+  Peer.withoutJson(StreamChannel channel,
+      {ErrorCallback onUnhandledError, bool strictProtocolChecks = true})
       : _manager = ChannelManager('Peer', channel) {
     _server = Server.withoutJson(
         StreamChannel(_serverIncomingForwarder.stream, channel.sink),
-        onUnhandledError: onUnhandledError);
+        onUnhandledError: onUnhandledError,
+        strictProtocolChecks: strictProtocolChecks);
     _client = Client.withoutJson(
         StreamChannel(_clientIncomingForwarder.stream, channel.sink));
   }
diff --git a/lib/src/server.dart b/lib/src/server.dart
index 127a6d5..83129ea 100644
--- a/lib/src/server.dart
+++ b/lib/src/server.dart
@@ -61,6 +61,13 @@
   /// invoked. If it is not set, the exception will be swallowed.
   final ErrorCallback onUnhandledError;
 
+  /// Whether to strictly enforce the JSON-RPC 2.0 specification for received messages.
+  ///
+  /// If `false`, this [Server] will accept some requests which are not conformant
+  /// with the JSON-RPC 2.0 specification. In particular, requests missing the
+  /// `jsonrpc` parameter will be accepted.
+  final bool strictProtocolChecks;
+
   /// Creates a [Server] that communicates over [channel].
   ///
   /// Note that the server won't begin listening to [requests] until
@@ -68,10 +75,16 @@
   ///
   /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
   /// If this is not provided, unhandled exceptions will be swallowed.
-  Server(StreamChannel<String> channel, {ErrorCallback onUnhandledError})
+  ///
+  /// If [strictProtocolChecks] is false, this [Server] will accept some
+  /// requests which are not conformant with the JSON-RPC 2.0 specification. In
+  /// particular, requests missing the `jsonrpc` parameter will be accepted.
+  Server(StreamChannel<String> channel,
+      {ErrorCallback onUnhandledError, bool strictProtocolChecks = true})
       : this.withoutJson(
             jsonDocument.bind(channel).transform(respondToFormatExceptions),
-            onUnhandledError: onUnhandledError);
+            onUnhandledError: onUnhandledError,
+            strictProtocolChecks: strictProtocolChecks);
 
   /// Creates a [Server] that communicates using decoded messages over
   /// [channel].
@@ -84,7 +97,12 @@
   ///
   /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
   /// If this is not provided, unhandled exceptions will be swallowed.
-  Server.withoutJson(StreamChannel channel, {this.onUnhandledError})
+  ///
+  /// If [strictProtocolChecks] is false, this [Server] will accept some
+  /// requests which are not conformant with the JSON-RPC 2.0 specification. In
+  /// particular, requests missing the `jsonrpc` parameter will be accepted.
+  Server.withoutJson(StreamChannel channel,
+      {this.onUnhandledError, this.strictProtocolChecks = true})
       : _manager = ChannelManager('Server', channel);
 
   /// Starts listening to the underlying stream.
@@ -217,14 +235,15 @@
           'an Array or an Object.');
     }
 
-    if (!request.containsKey('jsonrpc')) {
+    if (strictProtocolChecks && !request.containsKey('jsonrpc')) {
       throw RpcException(
           error_code.INVALID_REQUEST,
           'Request must '
           'contain a "jsonrpc" key.');
     }
 
-    if (request['jsonrpc'] != '2.0') {
+    if ((strictProtocolChecks || request.containsKey('jsonrpc')) &&
+        request['jsonrpc'] != '2.0') {
       throw RpcException(
           error_code.INVALID_REQUEST,
           'Invalid JSON-RPC '
@@ -246,12 +265,14 @@
           'be a string, but was ${jsonEncode(method)}.');
     }
 
-    var params = request['params'];
-    if (request.containsKey('params') && params is! List && params is! Map) {
-      throw RpcException(
-          error_code.INVALID_REQUEST,
-          'Request params must '
-          'be an Array or an Object, but was ${jsonEncode(params)}.');
+    if (request.containsKey('params')) {
+      var params = request['params'];
+      if (params is! List && params is! Map) {
+        throw RpcException(
+            error_code.INVALID_REQUEST,
+            'Request params must '
+            'be an Array or an Object, but was ${jsonEncode(params)}.');
+      }
     }
 
     var id = request['id'];
diff --git a/pubspec.yaml b/pubspec.yaml
index 0f55fb7..abd2520 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: json_rpc_2
-version: 2.1.1
+version: 2.2.0
 description: >-
   Utilities to write a client or server using the JSON-RPC 2.0 spec.
 homepage: https://github.com/dart-lang/json_rpc_2
diff --git a/test/server/invalid_request_test.dart b/test/server/invalid_request_test.dart
index 4dbca0c..a67afec 100644
--- a/test/server/invalid_request_test.dart
+++ b/test/server/invalid_request_test.dart
@@ -74,4 +74,21 @@
           }
         })));
   });
+
+  group('strict protocol checks disabled', () {
+    setUp(() => controller = ServerController(strictProtocolChecks: false));
+
+    test('and no jsonrpc param', () {
+      expectErrorResponse(controller, {'method': 'foo', 'id': 1234},
+          error_code.METHOD_NOT_FOUND, 'Unknown method "foo".');
+    });
+
+    test('the jsonrpc version must be 2.0', () {
+      expectErrorResponse(
+          controller,
+          {'jsonrpc': '1.0', 'method': 'foo', 'id': 1234},
+          error_code.INVALID_REQUEST,
+          'Invalid JSON-RPC version "1.0", expected "2.0".');
+    });
+  });
 }
diff --git a/test/server/utils.dart b/test/server/utils.dart
index bf1db6f..7ff491c 100644
--- a/test/server/utils.dart
+++ b/test/server/utils.dart
@@ -23,10 +23,13 @@
   json_rpc.Server get server => _server;
   json_rpc.Server _server;
 
-  ServerController({json_rpc.ErrorCallback onUnhandledError}) {
+  ServerController(
+      {json_rpc.ErrorCallback onUnhandledError,
+      bool strictProtocolChecks = true}) {
     _server = json_rpc.Server(
         StreamChannel(_requestController.stream, _responseController.sink),
-        onUnhandledError: onUnhandledError);
+        onUnhandledError: onUnhandledError,
+        strictProtocolChecks: strictProtocolChecks);
     _server.listen();
   }