Skip to content

Commit 4873fc0

Browse files
authored
Read canister ID from canister (#528)
* webpack: redirect html to backend * Inject canister ID in assets * Clean up webpack * Clean up iiConnection * Clean up assets * Clean up and document assets * Show error if canister ID is not set * Document webpack config * Fixes * Remove lines * Format, lint, README * Update Dockerfile * s/foo/setup_js/ * Add note about http:// * Use double-quotes to please formatter
1 parent f7bc0c9 commit 4873fc0

File tree

9 files changed

+123
-58
lines changed

9 files changed

+123
-58
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ FROM deps as build
6262

6363
COPY . .
6464

65-
ENV CANISTER_ID=rdmx6-jaaaa-aaaaa-aaadq-cai
6665
ARG II_ENV=production
6766

6867
RUN npm ci

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ Then open `http://localhost:8080` in your browser. Webpack will reload the page
8686
npm run format && npm run lint
8787
```
8888

89-
To customize your canister ID for deployment or particular local development, create a [`.env`](https://www.npmjs.com/package/dotenv) file in the root of the project and add a `CANISTER_ID` attribute. It should look something like
90-
```
91-
CANISTER_ID=rrkah-fqaaa-aaaaa-aaaaq-cai
92-
```
93-
9489
Finally, to test workflows like authentication from a client application, you start the sample app:
9590

9691
```bash

src/frontend/assets/index.html

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,10 @@
1111
<main id="pageContent" aria-live="polite"></main>
1212
<div id="notification"></div>
1313
<div id="loaderContainer"></div>
14-
<!--
15-
Note: we cannot use a normal script tag like this
16-
<script src="index.js" integrity="sha256-QKc0t+gyMRWWDNty0lxQKWpPz18K4pD8q3S0YoeQMdo=" defer></script>
17-
because Firefox does not support SRI with CSP: https://bugzilla.mozilla.org/show_bug.cgi?id=1409200
1814

19-
DO NOT MODIFY THE INLINE SCRIPT CONTENT!
20-
(or if you do, update the CSP hash)
21-
-->
22-
<script>
23-
const scripts = ["index.js"];
24-
scripts.forEach(function (scriptUrl) {
25-
let s = document.createElement("script");
26-
s.async = false;
27-
s.src = scriptUrl;
28-
document.head.appendChild(s);
29-
});
30-
</script>
15+
<!-- this is replaced by the backend before serving -->
16+
<!-- XXX: DO NOT CHANGE! or if you do, change the bit that matches on this
17+
exact string in the canister code-->
18+
<script id="setupJs"></script>
3119
</body>
3220
</html>

src/frontend/src/utils/iiConnection.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,23 @@ import { Principal } from "@dfinity/principal";
3434
import { MultiWebAuthnIdentity } from "./multiWebAuthnIdentity";
3535
import { hasOwnProperty } from "./utils";
3636
import * as tweetnacl from "tweetnacl";
37+
import { displayError } from "../components/displayError";
3738
import { fromMnemonicWithoutValidation } from "../crypto/ed25519";
3839

39-
// eslint-disable-next-line
40-
const canisterId: string = process.env.CANISTER_ID!;
40+
declare const canisterId: string;
41+
42+
// Check if the canister ID was defined before we even try to read it
43+
if (typeof canisterId !== undefined) {
44+
displayError({
45+
title: "Canister ID not set",
46+
message:
47+
"There was a problem contacting the IC. The host serving this page did not give us a canister ID. Try reloading the page and contact support if the problem persists.",
48+
primaryButton: "Reload",
49+
}).then(() => {
50+
window.location.reload();
51+
});
52+
}
53+
4154
export const canisterIdPrincipal: Principal = Principal.fromText(canisterId);
4255
export const baseActor = Actor.createActor<_SERVICE>(internet_identity_idl, {
4356
agent: new HttpAgent({}),

src/internet_identity/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ic-cdk = "0.3.2"
1111
ic-cdk-macros = "0.3"
1212
ic-certified-map = "0.3.0"
1313
ic-types = "0.1.1"
14+
lazy_static = "1.4.0"
1415
serde = "1"
1516
serde_bytes = "0.11"
1617
serde_cbor = "0.11"

src/internet_identity/src/assets.rs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// This file describes which assets are used and how (content, content type and content encoding).
44

55
use sha2::Digest;
6+
use lazy_static::lazy_static;
7+
use ic_cdk::api;
68

79
#[derive(Debug, PartialEq, Eq)]
810
pub enum ContentEncoding {
@@ -19,15 +21,43 @@ pub enum ContentType {
1921
SVG
2022
}
2123

22-
pub fn for_each_asset(mut f: impl FnMut(&'static str, ContentEncoding, ContentType, &'static [u8], &[u8; 32])) {
24+
lazy_static! {
25+
// The <script> tag that sets the canister ID and loads the 'index.js'
26+
static ref INDEX_HTML_SETUP_JS: String = {
27+
let canister_id = api::id();
28+
format!(r#"var canisterId = '{canister_id}';let s = document.createElement('script');s.async = false;s.src = 'index.js';document.head.appendChild(s);"#)
29+
};
2330

24-
let index_html = include_bytes!("../../../dist/index.html");
31+
// The SRI sha256 hash of the script tag, used by the CSP policy.
32+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
33+
pub static ref INDEX_HTML_SETUP_JS_SRI_HASH: String = {
34+
let hash = &sha2::Sha256::digest(INDEX_HTML_SETUP_JS.as_bytes());
35+
let hash = base64::encode(hash);
36+
format!("sha256-{hash}")
37+
};
2538

26-
let assets: [ (&str, &[u8], ContentEncoding, ContentType); 8] = [
39+
// The full content of the index.html, after the canister ID (and script tag) have been
40+
// injected
41+
static ref INDEX_HTML_STR: String = {
42+
let index_html = include_str!("../../../dist/index.html");
43+
let setup_js: String = INDEX_HTML_SETUP_JS.to_string();
44+
let index_html = index_html.replace(
45+
r#"<script id="setupJs"></script>"#,
46+
&format!(r#"<script id="setupJs">{setup_js}</script>"#).to_string()
47+
);
48+
index_html
49+
};
50+
}
51+
52+
// Get all the assets. Duplicated assets like index.html are shared and generally all assets are
53+
// prepared only once (like injecting the canister ID).
54+
pub fn get_assets() -> [ (&'static str, &'static [u8], ContentEncoding, ContentType); 8] {
55+
let index_html: &[u8] = INDEX_HTML_STR.as_bytes();
56+
[
2757
("/",
28-
index_html,
29-
ContentEncoding::Identity,
30-
ContentType::HTML,
58+
index_html,
59+
ContentEncoding::Identity,
60+
ContentType::HTML,
3161
),
3262
// The FAQ and about pages are the same webapp, but the webapp routes to the correct page
3363
(
@@ -72,16 +102,6 @@ pub fn for_each_asset(mut f: impl FnMut(&'static str, ContentEncoding, ContentTy
72102
ContentEncoding::Identity,
73103
ContentType::SVG,
74104
),
75-
];
76-
77-
for (name, content, encoding, content_type) in assets {
78-
let hash = hash_content(content);
79-
f(name, encoding, content_type, content, &hash);
80-
}
81-
}
82-
105+
]
83106

84-
// Hash the content of an asset in an `ic_certified_map` friendly way
85-
fn hash_content(bytes: &[u8]) -> [u8; 32] {
86-
sha2::Sha256::digest(bytes).into()
87107
}

src/internet_identity/src/main.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use internet_identity::signature_map::SignatureMap;
1111
use rand_chacha::rand_core::{RngCore, SeedableRng};
1212
use serde::Serialize;
1313
use serde_bytes::{ByteBuf, Bytes};
14+
use sha2::Digest;
1415
use std::borrow::Cow;
1516
use std::cell::{Cell, RefCell};
1617
use std::collections::HashMap;
@@ -811,6 +812,7 @@ fn http_request(req: HttpRequest) -> HttpResponse {
811812
/// These headers enable browser security features (like limit access to platform apis and set
812813
/// iFrame policies, etc.).
813814
fn security_headers() -> Vec<HeaderField> {
815+
let hash = assets::INDEX_HTML_SETUP_JS_SRI_HASH.to_string();
814816
vec![
815817
("X-Frame-Options".to_string(), "DENY".to_string()),
816818
("X-Content-Type-Options".to_string(), "nosniff".to_string()),
@@ -832,19 +834,24 @@ fn security_headers() -> Vec<HeaderField> {
832834
// style-src 'unsafe-inline' is currently required due to the way styles are handled by the
833835
// application. Adding hashes would require a big restructuring of the application and build
834836
// infrastructure.
837+
//
838+
// NOTE about `script-src`: we cannot use a normal script tag like this
839+
// <script src="index.js" integrity="sha256-..." defer></script>
840+
// because Firefox does not support SRI with CSP: https://bugzilla.mozilla.org/show_bug.cgi?id=1409200
841+
// Instead, we add it to the CSP policy
835842
(
836843
"Content-Security-Policy".to_string(),
837-
"default-src 'none';\
844+
format!("default-src 'none';\
838845
connect-src 'self' https://ic0.app;\
839846
img-src 'self' data:;\
840-
script-src 'sha256-syYd+YuWeLD80uCtKwbaGoGom63a0pZE5KqgtA7W1d8=' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https:;\
847+
script-src '{hash}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https:;\
841848
base-uri 'none';\
842849
frame-ancestors 'none';\
843850
form-action 'none';\
844851
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;\
845852
style-src-elem 'unsafe-inline' https://fonts.googleapis.com;\
846853
font-src https://fonts.gstatic.com;\
847-
upgrade-insecure-requests;"
854+
upgrade-insecure-requests;")
848855
.to_string()
849856
),
850857
(
@@ -922,9 +929,9 @@ fn init_assets() {
922929

923930
ASSETS.with(|a| {
924931
let mut assets = a.borrow_mut();
925-
assets::for_each_asset(|name, encoding, content_type, contents, hash| {
926-
asset_hashes.insert(name, *hash);
927-
let mut headers = match encoding {
932+
for (path, content, content_encoding, content_type) in assets::get_assets() {
933+
asset_hashes.insert(path, sha2::Sha256::digest(content).into());
934+
let mut headers = match content_encoding {
928935
ContentEncoding::Identity => vec![],
929936
ContentEncoding::GZip => {
930937
vec![("Content-Encoding".to_string(), "gzip".to_string())]
@@ -934,8 +941,8 @@ fn init_assets() {
934941
"Content-Type".to_string(),
935942
content_type.to_mime_type_string(),
936943
));
937-
assets.insert(name, (headers, contents));
938-
});
944+
assets.insert(path, (headers, content));
945+
}
939946
});
940947
});
941948
}

webpack.config.js

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,10 @@ const webpack = require("webpack");
33
const CopyPlugin = require("copy-webpack-plugin");
44
const TerserPlugin = require("terser-webpack-plugin");
55
const CompressionPlugin = require("compression-webpack-plugin");
6+
const HttpProxyMiddlware = require("http-proxy-middleware");
67
const dfxJson = require("./dfx.json");
78
require("dotenv").config();
89

9-
let localCanister;
10-
11-
try {
12-
localCanister = require("./.dfx/local/canister_ids.json").internet_identity.local;
13-
} catch {}
14-
1510
/**
1611
* Generate a webpack configuration for a canister.
1712
*/
@@ -43,13 +38,60 @@ function generateWebpackConfigForCanister(name, info) {
4338
path: path.join(__dirname, "dist"),
4439
},
4540
devServer: {
41+
42+
// Set up a proxy that redirects API calls and /index.html to the
43+
// replica; the rest we serve from here.
44+
onBeforeSetupMiddleware: (devServer) => {
45+
const dfxJson = './dfx.json';
46+
let replicaHost;
47+
48+
try {
49+
replicaHost = require(dfxJson).networks.local.bind;
50+
} catch (e) {
51+
throw Error(`Could get host from ${dfxJson}: ${e}`);
52+
}
53+
54+
// If the replicaHost lacks protocol (e.g. 'localhost:8000') the
55+
// requests are not forwarded properly
56+
if(!replicaHost.startsWith("http://")) {
57+
replicaHost = `http://${replicaHost}`;
58+
}
59+
60+
const canisterIdsJson = './.dfx/local/canister_ids.json';
61+
62+
let canisterId;
63+
64+
try {
65+
canisterId = require(canisterIdsJson).internet_identity.local;
66+
} catch (e) {
67+
throw Error(`Could get canister ID from ${canisterIdsJson}: ${e}`);
68+
}
69+
70+
// basically everything _except_ for index.js, because we want live reload
71+
devServer.app.get(['/', '/index.html', '/faq', '/faq', 'about' ], HttpProxyMiddlware.createProxyMiddleware( {
72+
target: replicaHost,
73+
pathRewrite: (pathAndParams, req) => {
74+
let queryParamsString = `?`;
75+
76+
const [path, params] = pathAndParams.split("?");
77+
78+
if (params) {
79+
queryParamsString += `${params}&`;
80+
}
81+
82+
queryParamsString += `canisterId=${canisterId}`;
83+
84+
return path + queryParamsString;
85+
},
86+
87+
}));
88+
},
4689
port: 8080,
4790
proxy: {
91+
// Make sure /api calls land on the replica (and not on webpack)
4892
"/api": "http://localhost:8000",
49-
"/authorize": "http://localhost:8081",
5093
},
5194
allowedHosts: [".localhost", ".local", ".ngrok.io"],
52-
historyApiFallback: true, // makes sure our index is served on all endpoints, e.g. `/faq`
5395
},
5496

5597
// Depending in the language or framework you are using for
@@ -81,7 +123,6 @@ function generateWebpackConfigForCanister(name, info) {
81123
process: require.resolve("process/browser"),
82124
}),
83125
new webpack.EnvironmentPlugin({
84-
"CANISTER_ID": localCanister,
85126
"II_ENV": "production"
86127
}),
87128
new CompressionPlugin({

0 commit comments

Comments
 (0)