diff --git a/Cargo.lock b/Cargo.lock index 624be28f7c..ef66013bef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,6 +665,7 @@ dependencies = [ "ic-cdk-macros", "ic-certified-map", "ic-types 0.1.5", + "lazy_static", "rand 0.8.4", "rand_chacha 0.2.2", "rand_core 0.5.1", diff --git a/Dockerfile b/Dockerfile index ef34a2ccba..2ab1618729 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,7 +62,6 @@ FROM deps as build COPY . . -ENV CANISTER_ID=rdmx6-jaaaa-aaaaa-aaadq-cai ARG II_ENV=production RUN npm ci diff --git a/README.md b/README.md index c0aac5af15..01d79449f5 100644 --- a/README.md +++ b/README.md @@ -86,11 +86,6 @@ Then open `http://localhost:8080` in your browser. Webpack will reload the page npm run format && npm run lint ``` -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 -``` -CANISTER_ID=rrkah-fqaaa-aaaaa-aaaaq-cai -``` - Finally, to test workflows like authentication from a client application, you start the sample app: ```bash diff --git a/src/frontend/assets/index.html b/src/frontend/assets/index.html index 19decbceb3..805616526d 100644 --- a/src/frontend/assets/index.html +++ b/src/frontend/assets/index.html @@ -11,22 +11,10 @@
- - + + + diff --git a/src/frontend/src/utils/iiConnection.ts b/src/frontend/src/utils/iiConnection.ts index ad0fbe371e..8f39b69dba 100644 --- a/src/frontend/src/utils/iiConnection.ts +++ b/src/frontend/src/utils/iiConnection.ts @@ -34,10 +34,23 @@ import { Principal } from "@dfinity/principal"; import { MultiWebAuthnIdentity } from "./multiWebAuthnIdentity"; import { hasOwnProperty } from "./utils"; import * as tweetnacl from "tweetnacl"; +import { displayError } from "../components/displayError"; import { fromMnemonicWithoutValidation } from "../crypto/ed25519"; -// eslint-disable-next-line -const canisterId: string = process.env.CANISTER_ID!; +declare const canisterId: string; + +// Check if the canister ID was defined before we even try to read it +if (typeof canisterId !== undefined) { + displayError({ + title: "Canister ID not set", + message: + "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.", + primaryButton: "Reload", + }).then(() => { + window.location.reload(); + }); +} + export const canisterIdPrincipal: Principal = Principal.fromText(canisterId); export const baseActor = Actor.createActor<_SERVICE>(internet_identity_idl, { agent: new HttpAgent({}), diff --git a/src/internet_identity/Cargo.toml b/src/internet_identity/Cargo.toml index d03ce3de65..dc2a79ff65 100644 --- a/src/internet_identity/Cargo.toml +++ b/src/internet_identity/Cargo.toml @@ -11,6 +11,7 @@ ic-cdk = "0.3.2" ic-cdk-macros = "0.3" ic-certified-map = "0.3.0" ic-types = "0.1.1" +lazy_static = "1.4.0" serde = "1" serde_bytes = "0.11" serde_cbor = "0.11" diff --git a/src/internet_identity/src/assets.rs b/src/internet_identity/src/assets.rs index 7a7d8fe7ff..657857baaf 100644 --- a/src/internet_identity/src/assets.rs +++ b/src/internet_identity/src/assets.rs @@ -3,6 +3,8 @@ // This file describes which assets are used and how (content, content type and content encoding). use sha2::Digest; +use lazy_static::lazy_static; +use ic_cdk::api; #[derive(Debug, PartialEq, Eq)] pub enum ContentEncoding { @@ -19,15 +21,43 @@ pub enum ContentType { SVG } -pub fn for_each_asset(mut f: impl FnMut(&'static str, ContentEncoding, ContentType, &'static [u8], &[u8; 32])) { +lazy_static! { + // The "#, + &format!(r#""#).to_string() + ); + index_html + }; +} + +// Get all the assets. Duplicated assets like index.html are shared and generally all assets are +// prepared only once (like injecting the canister ID). +pub fn get_assets() -> [ (&'static str, &'static [u8], ContentEncoding, ContentType); 8] { + let index_html: &[u8] = INDEX_HTML_STR.as_bytes(); + [ ("/", - index_html, - ContentEncoding::Identity, - ContentType::HTML, + index_html, + ContentEncoding::Identity, + ContentType::HTML, ), // The FAQ and about pages are the same webapp, but the webapp routes to the correct page ( @@ -72,16 +102,6 @@ pub fn for_each_asset(mut f: impl FnMut(&'static str, ContentEncoding, ContentTy ContentEncoding::Identity, ContentType::SVG, ), - ]; - - for (name, content, encoding, content_type) in assets { - let hash = hash_content(content); - f(name, encoding, content_type, content, &hash); - } -} - + ] -// Hash the content of an asset in an `ic_certified_map` friendly way -fn hash_content(bytes: &[u8]) -> [u8; 32] { - sha2::Sha256::digest(bytes).into() } diff --git a/src/internet_identity/src/main.rs b/src/internet_identity/src/main.rs index 60dc42c08b..338faf2262 100644 --- a/src/internet_identity/src/main.rs +++ b/src/internet_identity/src/main.rs @@ -11,6 +11,7 @@ use internet_identity::signature_map::SignatureMap; use rand_chacha::rand_core::{RngCore, SeedableRng}; use serde::Serialize; use serde_bytes::{ByteBuf, Bytes}; +use sha2::Digest; use std::borrow::Cow; use std::cell::{Cell, RefCell}; use std::collections::HashMap; @@ -811,6 +812,7 @@ fn http_request(req: HttpRequest) -> HttpResponse { /// These headers enable browser security features (like limit access to platform apis and set /// iFrame policies, etc.). fn security_headers() -> Vec { + let hash = assets::INDEX_HTML_SETUP_JS_SRI_HASH.to_string(); vec![ ("X-Frame-Options".to_string(), "DENY".to_string()), ("X-Content-Type-Options".to_string(), "nosniff".to_string()), @@ -832,19 +834,24 @@ fn security_headers() -> Vec { // style-src 'unsafe-inline' is currently required due to the way styles are handled by the // application. Adding hashes would require a big restructuring of the application and build // infrastructure. + // + // NOTE about `script-src`: we cannot use a normal script tag like this + // + // because Firefox does not support SRI with CSP: https://bugzilla.mozilla.org/show_bug.cgi?id=1409200 + // Instead, we add it to the CSP policy ( "Content-Security-Policy".to_string(), - "default-src 'none';\ + format!("default-src 'none';\ connect-src 'self' https://ic0.app;\ img-src 'self' data:;\ - script-src 'sha256-syYd+YuWeLD80uCtKwbaGoGom63a0pZE5KqgtA7W1d8=' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https:;\ + script-src '{hash}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https:;\ base-uri 'none';\ frame-ancestors 'none';\ form-action 'none';\ style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;\ style-src-elem 'unsafe-inline' https://fonts.googleapis.com;\ font-src https://fonts.gstatic.com;\ - upgrade-insecure-requests;" + upgrade-insecure-requests;") .to_string() ), ( @@ -922,9 +929,9 @@ fn init_assets() { ASSETS.with(|a| { let mut assets = a.borrow_mut(); - assets::for_each_asset(|name, encoding, content_type, contents, hash| { - asset_hashes.insert(name, *hash); - let mut headers = match encoding { + for (path, content, content_encoding, content_type) in assets::get_assets() { + asset_hashes.insert(path, sha2::Sha256::digest(content).into()); + let mut headers = match content_encoding { ContentEncoding::Identity => vec![], ContentEncoding::GZip => { vec![("Content-Encoding".to_string(), "gzip".to_string())] @@ -934,8 +941,8 @@ fn init_assets() { "Content-Type".to_string(), content_type.to_mime_type_string(), )); - assets.insert(name, (headers, contents)); - }); + assets.insert(path, (headers, content)); + } }); }); } diff --git a/webpack.config.js b/webpack.config.js index 8340769f44..beb5ca23e6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,15 +3,10 @@ const webpack = require("webpack"); const CopyPlugin = require("copy-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); const CompressionPlugin = require("compression-webpack-plugin"); +const HttpProxyMiddlware = require("http-proxy-middleware"); const dfxJson = require("./dfx.json"); require("dotenv").config(); -let localCanister; - -try { - localCanister = require("./.dfx/local/canister_ids.json").internet_identity.local; -} catch {} - /** * Generate a webpack configuration for a canister. */ @@ -43,13 +38,60 @@ function generateWebpackConfigForCanister(name, info) { path: path.join(__dirname, "dist"), }, devServer: { + + // Set up a proxy that redirects API calls and /index.html to the + // replica; the rest we serve from here. + onBeforeSetupMiddleware: (devServer) => { + const dfxJson = './dfx.json'; + let replicaHost; + + try { + replicaHost = require(dfxJson).networks.local.bind; + } catch (e) { + throw Error(`Could get host from ${dfxJson}: ${e}`); + } + + // If the replicaHost lacks protocol (e.g. 'localhost:8000') the + // requests are not forwarded properly + if(!replicaHost.startsWith("http://")) { + replicaHost = `http://${replicaHost}`; + } + + const canisterIdsJson = './.dfx/local/canister_ids.json'; + + let canisterId; + + try { + canisterId = require(canisterIdsJson).internet_identity.local; + } catch (e) { + throw Error(`Could get canister ID from ${canisterIdsJson}: ${e}`); + } + + // basically everything _except_ for index.js, because we want live reload + devServer.app.get(['/', '/index.html', '/faq', '/faq', 'about' ], HttpProxyMiddlware.createProxyMiddleware( { + target: replicaHost, + pathRewrite: (pathAndParams, req) => { + let queryParamsString = `?`; + + const [path, params] = pathAndParams.split("?"); + + if (params) { + queryParamsString += `${params}&`; + } + + queryParamsString += `canisterId=${canisterId}`; + + return path + queryParamsString; + }, + + })); + }, port: 8080, proxy: { + // Make sure /api calls land on the replica (and not on webpack) "/api": "http://localhost:8000", - "/authorize": "http://localhost:8081", }, allowedHosts: [".localhost", ".local", ".ngrok.io"], - historyApiFallback: true, // makes sure our index is served on all endpoints, e.g. `/faq` }, // Depending in the language or framework you are using for @@ -81,7 +123,6 @@ function generateWebpackConfigForCanister(name, info) { process: require.resolve("process/browser"), }), new webpack.EnvironmentPlugin({ - "CANISTER_ID": localCanister, "II_ENV": "production" }), new CompressionPlugin({