-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmain.mo
311 lines (268 loc) · 9.21 KB
/
main.mo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/*
This small Motoko canister demonstrates, as a proof of concept, how to serve
HTTP requests with dynamic data, and how to do that in a certified way.
To learn more about the theory behind certified variables, I recommend
my talk at https://dfinity.org/howitworks/response-certification
*/
/*
We start with s bunch of imports.
*/
import T "mo:base/Text";
import O "mo:base/Option";
import A "mo:base/Array";
import Nat8 "mo:base/Nat8";
import Blob "mo:base/Blob";
import Iter "mo:base/Iter";
import Error "mo:base/Error";
import Buffer "mo:base/Buffer";
import Principal "mo:base/Principal";
import CertifiedData "mo:base/CertifiedData";
import SHA256 "mo:sha256/SHA256";
/*
The actor functionality is pretty straight forward: We store
a string, provide an update call to set it, and we define a function
that includes that string in the main page of our service.
*/
actor Self {
stable var last_message : Text = "Nobody said anything yet.";
public shared func leave_message(msg : Text) : async () {
last_message := msg;
update_asset_hash(); // will be explained below
};
func my_id(): Principal = Principal.fromActor(Self);
func main_page(): Blob {
return T.encodeUtf8 (
"This canister demonstrates certified HTTP assets from Motoko.\n" #
"\n" #
"You can see this text at https://" # debug_show my_id() # ".ic0.app/\n" #
"(note, no raw!) and it will validate!\n" #
"\n" #
"And to demonstrate that this really is dynamic, you can leave a" #
"message at https://ic.rocks/principal/" # debug_show my_id() # "\n" #
"\n" #
"The last message submitted was:\n" #
last_message
)
};
/*
To serve HTTP assets, we have to define a query method called `http_request`,
and return the body and the headers. If you don’t care about certification and
just want to serve from <canisterid>.raw.ic0.app, you can do that without
worrying about the ic-certification header.
*/
type HeaderField = (Text, Text);
type HttpResponse = {
status_code: Nat16;
headers: [HeaderField];
body: Blob;
};
type HttpRequest = {
method: Text;
url: Text;
headers: [HeaderField];
body: Blob;
};
public query func http_request(req : HttpRequest) : async HttpResponse {
// check if / is requested
if ((req.method, req.url) == ("GET", "/")) {
// If so, return the main page with with right headers
return {
status_code = 200;
headers = [ ("content-type", "text/plain"), certification_header() ];
body = main_page()
}
} else {
// Else return an error code. Note that we cannot certify this response
// so a user going to https://ce7vw-haaaa-aaaai-aanva-cai.ic0.app/foo
// will not see the error message
return {
status_code = 404;
headers = [ ("content-type", "text/plain") ];
body = "404 Not found.\n This canister only serves /.\n"
}
}
};
/*
If it weren’t for certification, this would be it. The remainder of the file deals with certification.
*/
/*
To certify HTTP assets, we have to put them into a hash tree. The data structure for hash trees
can be defined as follows, straight from
https://sdk.dfinity.org/docs/interface-spec/index.html#_certificate
*/
type Hash = Blob;
type Key = Blob;
type Value = Blob;
type HashTree = {
#empty;
#pruned : Hash;
#fork : (HashTree, HashTree);
#labeled : (Key, HashTree);
#leaf : Value;
};
/*
The (undocumented) interface for certified assets requires the service to put
all HTTP resources into such a tree. We only have one resource, so that is simple:
*/
func asset_tree() : HashTree {
#labeled ("http_assets",
#labeled ("/",
#leaf (h(main_page()))
)
);
};
/*
We use this tree twice. In update calls that can change the assets, we have to
take the root hash of that tree and pass it to the system:
*/
func update_asset_hash() {
CertifiedData.set(hash_tree(asset_tree()));
};
/*
We should also do this after upgrades:
*/
system func postupgrade() {
update_asset_hash();
};
/*
In fact, we should do it during initialization as well, but Motoko’s definedness analysis is
too strict and will not allow the following, and there is no `system func init` in Motoko:
*/
// update_asset_hash();
/*
The other use of the tree is when calculating the ic-certificate header. This header
contains the certificate obtained from the system, which we just pass through,
and our hash tree. There is CBOR and Base64 encoding involved here.
*/
func certification_header() : HeaderField {
let cert = switch (CertifiedData.getCertificate()) {
case (?c) c;
case null {
// unfortunately, we cannot do
// throw Error.reject("getCertificate failed. Call this as a query call!")
// here, because this function isn’t async, but we can’t make it async
// because it is called from a query (and it would do the wrong thing) :-(
//
// So just return erronous data instead
"getCertificate failed. Call this as a query call!" : Blob
}
};
return
("ic-certificate",
"certificate=:" # base64(cert) # ":, " #
"tree=:" # base64(cbor_tree(asset_tree())) # ":"
)
};
/*
(Note that a more serious implementatin would not return the full tree here, but prune
any branches of the tree not relevant for the requested resource.)
*/
/*
That’s it! The rest is generic code that ought to be libraries, but wasn’t too bad
to write by hand either. The code below has hopefully reasonable performance
characteristics, even though they are not highly optimzed.
*/
/*
Helpers for hashing one, two or three blobs:
These can hopefully be simplified once https://github.com/dfinity-lab/motoko/issues/966 is resolved.
*/
func h(b1 : Blob) : Blob {
let d = SHA256.Digest();
d.write(Blob.toArray(b1));
Blob.fromArray(d.sum());
};
func h2(b1 : Blob, b2 : Blob) : Blob {
let d = SHA256.Digest();
d.write(Blob.toArray(b1));
d.write(Blob.toArray(b2));
Blob.fromArray(d.sum());
};
func h3(b1 : Blob, b2 : Blob, b3 : Blob) : Blob {
let d = SHA256.Digest();
d.write(Blob.toArray(b1));
d.write(Blob.toArray(b2));
d.write(Blob.toArray(b3));
Blob.fromArray(d.sum());
};
/*
Base64 encoding.
*/
func base64(b : Blob) : Text {
let base64_chars : [Text] = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/"];
let bytes = Blob.toArray(b);
let pad_len = if (bytes.size() % 3 == 0) { 0 } else {3 - bytes.size() % 3 : Nat};
let padded_bytes = A.append(bytes, A.tabulate<Nat8>(pad_len, func(_) { 0 }));
var out = "";
for (j in Iter.range(1,padded_bytes.size() / 3)) {
let i = j - 1 : Nat; // annoying inclusive upper bound in Iter.range
let b1 = padded_bytes[3*i];
let b2 = padded_bytes[3*i+1];
let b3 = padded_bytes[3*i+2];
let c1 = (b1 >> 2 ) & 63;
let c2 = (b1 << 4 | b2 >> 4) & 63;
let c3 = (b2 << 2 | b3 >> 6) & 63;
let c4 = (b3 ) & 63;
out #= base64_chars[Nat8.toNat(c1)]
# base64_chars[Nat8.toNat(c2)]
# (if (3*i+1 >= bytes.size()) { "=" } else { base64_chars[Nat8.toNat(c3)] })
# (if (3*i+2 >= bytes.size()) { "=" } else { base64_chars[Nat8.toNat(c4)] });
};
return out
};
/*
The root hash of a HashTree. This is the algorithm `reconstruct` described in
https://sdk.dfinity.org/docs/interface-spec/index.html#_certificate
*/
func hash_tree(t : HashTree) : Hash {
switch (t) {
case (#empty) {
h("\11ic-hashtree-empty");
};
case (#fork(t1,t2)) {
h3("\10ic-hashtree-fork", hash_tree(t1), hash_tree(t2));
};
case (#labeled(l,t)) {
h3("\13ic-hashtree-labeled", l, hash_tree(t));
};
case (#leaf(v)) {
h2("\10ic-hashtree-leaf", v)
};
case (#pruned(h)) {
h
}
}
};
/*
The CBOR encoding of a HashTree, according to
https://sdk.dfinity.org/docs/interface-spec/index.html#certification-encoding
This data structure needs only very few features of CBOR, so instead of writing
a full-fledged CBOR encoding library, I just directly write out the bytes for the
few construct we need here.
*/
func cbor_tree(tree : HashTree) : Blob {
let buf = Buffer.Buffer<Nat8>(100);
// CBOR self-describing tag
buf.add(0xD9);
buf.add(0xD9);
buf.add(0xF7);
func add_blob(b: Blob) {
// Only works for blobs with less than 256 bytes
buf.add(0x58);
buf.add(Nat8.fromNat(b.size()));
for (c in Blob.toArray(b).vals()) {
buf.add(c);
};
};
func go(t : HashTree) {
switch (t) {
case (#empty) { buf.add(0x81); buf.add(0x00); };
case (#fork(t1,t2)) { buf.add(0x83); buf.add(0x01); go(t1); go (t2); };
case (#labeled(l,t)) { buf.add(0x83); buf.add(0x02); add_blob(l); go (t); };
case (#leaf(v)) { buf.add(0x82); buf.add(0x03); add_blob(v); };
case (#pruned(h)) { buf.add(0x82); buf.add(0x04); add_blob(h); }
}
};
go(tree);
return Blob.fromArray(buf.toArray());
};
};