# Elevate Your Flutter Web Hosting with Dart_Frog

[Link to the GitHub Repo](https://github.com/jxstxn1/hash_demo)

Everyone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the following:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1691525897533/5137f6e7-1424-4949-bdf9-352276200e3e.png align="center")

This can be very frustrating especially if you start whitelisting scripts to your Content Security Policy and those scripts are requesting resources from somewhere else.

**So what's the solution for this?**

Content Security Policy errors can only be solved by the web server which is hosting the Web App. There are probably multiple solutions to tackle this problem, but today we are going to take a deep dive into `dart_frog` and how to utilize it to host our Flutter Web App with some advanced capabilities.

### Key Topics

> Note: Content Security Policy will be called CSP in this Blog Post

* Serve a Flutter Web App with `dart_frog`
    
* Set all recommended security headers with `shelf_helmet`
    
* Create a Content Security Policy that works with CSP2 and 3
    
* Create Hashes for `inline-scripts` to whitelist them in our Content Security Policy
    

### Setting everything up

We'll start by creating a brand new dart\_frog project

```bash
dart_frog create hash_demo
```

We can then open our `pubpsec.yaml` and replace the contents with

```yaml
name: hash_demo
description: An example of how to use the CSP Hasher package in combination with shelf_helmet.
version: 1.0.0+1
publish_to: none

environment:
  sdk: ">=3.0.0 <4.0.0"

dependencies:
  csp_hasher: ^1.0.0
  dart_frog: ^1.0.0
  path: ^1.8.3
  shelf_helmet: ^2.1.1

dev_dependencies:
  mocktail: ^0.3.0
  test: ^1.19.2
  very_good_analysis: ^5.0.0
```

Now we have to create our Flutter project with:  
`flutter create counter --platform web`

### Build and copy the Flutter App

To build our Flutter App we run in `counter`:

```bash
flutter build web --web-renderer canvaskit --release --csp
```

After the build, we can copy the output from `counter/build/web` to `public/`  
To simplify this you can run from `hash_demo`:

```bash
cp -r counter/build/web/ public/
```

### Serve Flutter App at /

To serve the `index.html` file directly at the `/` location we modify the `routes/index.dart` file with:

```dart
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:path/path.dart' as path;

Response onRequest(RequestContext context) {
  final file = File(
    path.join(Directory.current.path, 'public', 'index.html'),
  );
  final indexHtml = file.readAsStringSync();
  return Response(body: indexHtml, headers: {'Content-Type': 'text/html'});
}
```

To ensure that everything works as expected we will write a test for it.  
Let's create `test/routes/index_test.dart` :

```dart
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
import '../../routes/index.dart' as route;

class _MockRequestContext extends Mock implements RequestContext {}

final htmlString = File(
  '${Directory.current.path}/public/index.html',
).readAsStringSync();

void main() {
  group('GET /', () {
    test('responds with a 200, an html and the content_type header text/html',
        () async {
      final context = _MockRequestContext();
      final response = route.onRequest(context);
      expect(response.statusCode, equals(200));
      expect(
        response.headers,
        equals({
          'Content-Type': 'text/html',
          'content-length': '1830',
        }),
      );
      expect(response.body(), completion(htmlString));
    });
  });
}
```

Perfect! We can now serve our Flutter Web App at `localhost:8080/`  
To test it in a browser you can run:

```bash
dart_frog dev
```

### Creating our middleware

We can now create our middleware where most of the magic is happening.  
To do so run:

```bash
dart_frog new middleware /
```

We can now replace the content in the newly created middleware with this:

```dart
import 'package:dart_frog/dart_frog.dart';
import 'package:shelf_helmet/shelf_helmet.dart';

Handler middleware(Handler handler) {
  return handler.use(requestLogger()).use(
        fromShelfMiddleware(
          helmet(
            options: const HelmetOptions(
              cspOptions: ContentSecurityPolicyOptions.useDefaults(
                directives: {
                  'script-src': [
                    "'strict-dynamic'",
                    "'wasm-unsafe-eval'",
                    "'self'",
                    'blob:',
                    'https://unpkg.com/',
                    'https://www.gstatic.com/flutter-canvaskit/',
                  ],
                  'script-src-elem': [
                    "'self'",
                    'blob:',
                    'https://unpkg.com/',
                    'https://www.gstatic.com/flutter-canvaskit/',
                  ],
                  'connect-src': [
                    "'self'",
                    'https://unpkg.com/',
                    'https://www.gstatic.com/flutter-canvaskit/',
                    'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ',
                  ],
                  'style-src': [
                    "'self'",
                    'https:',
                  ],
                  'require-trusted-types-for': ["'script'"],
                },
              ),
            ),
          ),
        ),
      );
}
```

We use `shelf_helmet` to set nearly all security headers. Since we are using a Flutter Web App we have to define some Urls in our CSP. This CSP is working with CSP3 and all previous versions.

> [You can find a deeper explanation of CSP here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
> 
> [And the documentation of shelf\_helmet](https://pub.dev/packages/shelf_helmet)

We ensure that all recommended headers are set by creating a test for our middleware. So let's create `test/routes/_middleware_test.dart`:

```dart
import 'package:dart_frog/dart_frog.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
import '../../routes/_middleware.dart';

class _MockRequestContext extends Mock implements RequestContext {}

void main() {
  group('Middleware', () {
    test('add all required headers', () async {
      final handler = middleware((context) => Response());
      final request = Request.get(Uri.parse('http://localhost/'));
      final context = _MockRequestContext();

      when(() => context.request).thenReturn(request);

      final finishedHandler = await handler(context);

      const cspRules =
          '''script-src 'strict-dynamic' 'wasm-unsafe-eval' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https:;require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests''';
      _expectedHeaders(finishedHandler.headers, cspRules);
    });
  });
}

void _expectedHeaders(Map<String, String> headers, String cspRules) {
  expect(headers['content-length'], '0');
  expect(headers['x-xss-protection'], '0');
  expect(headers['x-permitted-cross-domain-policies'], 'none');
  expect(headers['x-frame-options'], 'SAMEORIGIN');
  expect(headers['x-download-options'], 'noopen');
  expect(headers['x-dns-prefetch-control'], 'off');
  expect(headers['x-content-type-options'], 'nosniff');
  expect(
    headers['strict-transport-security'],
    'max-age=15552000; includeSubDomains',
  );
  expect(headers['referrer-policy'], 'no-referrer');
  expect(headers['origin-agent-cluster'], '?1');
  expect(headers['cross-origin-resource-policy'], 'same-origin');
  expect(headers['cross-origin-opener-policy'], 'same-origin');
  expect(headers['Content-Security-Policy'], cspRules);
}
```

In our `_expectedHeaders` we can see the work of `shelf_helmet`.

### Create hashes for `inline-scripts` with the `csp_hasher` package

Since Flutter is using `inline-scripts` like this:

```xml
<script>
    // The value below is injected by flutter build, do not touch.
    var serviceWorkerVersion = "1515245720";
</script>
```

which is causing our Content Security Policy to throw errors and cause our web app to not load we have to generate Hashes for those functions.

Let's start by hashing our `inline-scripts` in a custom entrypoint at our project root

```dart
// custom entrypoint for the app
import 'dart:io';

import 'package:csp_hasher/csp_hasher.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:path/path.dart' as path;

List<CspHash> cspScriptHashes = [];
List<CspHash> cspStyleHashes = [];

Future<HttpServer> run(Handler handler, InternetAddress ip, int port) {
  generateCspHashes();
  return serve(handler, ip, port, poweredByHeader: null);
}

/// Generates CSP hashes for the scripts and styles in the index.html file
void generateCspHashes() {
  final file = File(
    path.join(Directory.current.path, 'public', '-.html'),
  );
  if (!file.existsSync()) {
    throw Exception('Index Not found\nPlease run build_web.sh first');
  }

  cspScriptHashes = hashScripts(htmlFile: file);
  cspStyleHashes = hashScripts(
    htmlFile: file,
    hashMode: HashMode.style,
  );
}
```

> [You can find a link to the `csp_hasher` package here](https://pub.dev/packages/csp_hasher)

This is creating hashes for all our `inline-scripts` at every hot-reload.

We can now inject the hashes into our Content Security Policy in the `_middleware.dart` file so that it looks like this:

```dart
import 'package:dart_frog/dart_frog.dart';
import 'package:shelf_helmet/shelf_helmet.dart';

import '../main.dart';

Handler middleware(Handler handler) {
  return handler.use(requestLogger()).use(
        fromShelfMiddleware(
          helmet(
            options: HelmetOptions(
              cspOptions: ContentSecurityPolicyOptions.useDefaults(
                directives: {
                  'script-src': [
                    "'strict-dynamic'",
                    "'wasm-unsafe-eval'",
                    cspScriptHashes.join(' ').replaceAll('"', ''),
                    "'self'",
                    'blob:',
                    'https://unpkg.com/',
                    'https://www.gstatic.com/flutter-canvaskit/',
                  ],
                  'script-src-elem': [
                    cspScriptHashes.join(' ').replaceAll('"', ''),
                    "'self'",
                    'blob:',
                    'https://unpkg.com/',
                    'https://www.gstatic.com/flutter-canvaskit/',
                  ],
                  'connect-src': [
                    "'self'",
                    'https://unpkg.com/',
                    'https://www.gstatic.com/flutter-canvaskit/',
                    'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ',
                  ],
                  'style-src': [
                    "'self'",
                    'https:',
                    cspStyleHashes.join(' ').replaceAll('"', ''),
                  ],
                  'require-trusted-types-for': ["'script'"],
                },
              ),
            ),
          ),
        ),
      );
}
```

And our updated test is looking like this:

```dart
import 'package:csp_hasher/csp_hasher.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

import '../../main.dart';
import '../../routes/_middleware.dart';

class _MockRequestContext extends Mock implements RequestContext {}

void main() {
  group('Middleware', () {
    setUp(() {
      cspScriptHashes.addAll([
        CspHash(
          lineNumber: 1,
          hashType: sha256,
          hash: 'abcdef',
          hashMode: HashMode.script,
        ),
      ]);
      cspStyleHashes.addAll([
        CspHash(
          lineNumber: 1,
          hashType: sha256,
          hash: 'ghijkl',
          hashMode: HashMode.style,
        ),
      ]);
    });
    test('add all required headers', () async {
      final handler = middleware((context) => Response());
      final request = Request.get(Uri.parse('http://localhost/'));
      final context = _MockRequestContext();

      when(() => context.request).thenReturn(request);

      final finishedHandler = await handler(context);

      const cspRules =
          '''script-src 'unsafe-inline' 'strict-dynamic' 'wasm-unsafe-eval' 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https: 'sha256-ghijkl';require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests''';
      _expectedHeaders(finishedHandler.headers, cspRules);
    });
  });
}

void _expectedHeaders(Map<String, String> headers, String cspRules) {
  expect(headers['content-length'], '0');
  expect(headers['x-xss-protection'], '0');
  expect(headers['x-permitted-cross-domain-policies'], 'none');
  expect(headers['x-frame-options'], 'SAMEORIGIN');
  expect(headers['x-download-options'], 'noopen');
  expect(headers['x-dns-prefetch-control'], 'off');
  expect(headers['x-content-type-options'], 'nosniff');
  expect(
    headers['strict-transport-security'],
    'max-age=15552000; includeSubDomains',
  );
  expect(headers['referrer-policy'], 'no-referrer');
  expect(headers['origin-agent-cluster'], '?1');
  expect(headers['cross-origin-resource-policy'], 'same-origin');
  expect(headers['cross-origin-opener-policy'], 'same-origin');
  expect(headers['Content-Security-Policy'], cspRules);
}
```

### Conclusion

By utilizing `dart_frog` in combination with `shelf_helmet` and `csp_hasher` it is quite easy to serve a Flutter Web App with the recommended Security Headers and a very strong `Content-Security-Policy`. Nevertheless, we can avoid all the hashing by removing all `inline-scripts` from our `index.html`

[Link to the GitHub Repo](https://github.com/jxstxn1/hash_demo)
