Skip to content

Commit dc50b44

Browse files
committed
Add exercises to Chapter 20
1 parent ac58b8a commit dc50b44

File tree

12 files changed

+400
-25
lines changed

12 files changed

+400
-25
lines changed

17_http.txt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,16 @@ every format needs its own different way of escaping characters. This
173173
one, called “URL encoding”, uses a percent sign followed by two
174174
hexadecimal digits which encode the character code. In this case 3F,
175175
which is 63 in decimal notation, is the code of a question mark
176-
character.
176+
character. JavaScript provides the `encodeURIComponent` and
177+
`decodeURIComponent` functions to en- and decode this format.
178+
179+
[source,javascript]
180+
----
181+
console.log(encodeURIComponent("Hello & goodbye"));
182+
// → Hello%20%26%20goodbye
183+
console.log(decodeURIComponent("Hello%20%26%20goodbye"));
184+
// → Hello & goodbye
185+
----
177186

178187
If we change the `method` attribute of the form above to `POST`, the
179188
HTTP request made to submit the form will use the `POST` method, and

20_node.txt

Lines changed: 184 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,8 @@ http.createServer(function(request, response) {
632632
}).listen(8000);
633633

634634
function urlToPath(url) {
635-
return "." + require("url").parse(url).pathname;
635+
var path = require("url").parse(url).pathname;
636+
return "." + decodeURIComponent(path);
636637
}
637638
----
638639

@@ -650,8 +651,9 @@ or a string, and is passed directly to the response's `end` method.
650651

651652
To get a path from the URL in the request, the `urlToPath` function
652653
uses node's built-in `"url"` module to parse the URL. It takes its
653-
pathname, which will be something like `/file.txt`, and prefixes a
654-
single dot to produce a path relative to the current directory.
654+
pathname, which will be something like `/file.txt`, decodes that to
655+
get rid of the `%20`-style escape codes, and prefixes a single dot to
656+
produce a path relative to the current directory.
655657

656658
(If you are worried about the security of the `urlToPath` function,
657659
you are right. We will come back to it in the exercises.)
@@ -945,31 +947,193 @@ when the I/O you asked for is completed.
945947

946948
== Exercises ==
947949

948-
FIXME
949-
950950
=== Content negotiation, again ===
951951

952-
Repeat 17.1, with node
952+
In Chapter 17, the first exercise was to make several requests to
953+
_http://eloquentjavascript.net/author_, asking for different types of
954+
content by passing different `Accept` headers.
955+
956+
Do this again, using node's `http.request` function. Ask for at least
957+
the media types `text/plain`, `text/html`, and `application/json`.
958+
Remember that headers to a request can be given as an object, in the
959+
`headers` property of `http.request`’s first argument.
960+
961+
Write out the content of the responses to each request.
962+
963+
!!solution!!
964+
965+
Don't forget to call the `end` method on the object returned by
966+
`http.request`, in order to actually fire off the request.
967+
968+
The response object passed to `http.request`’s callback is a readable
969+
stream. This means that it is not entirely trivial to get the whole
970+
response body out of it. The following utility function reads a whole
971+
stream and calls a callback function with the result, using the usual
972+
pattern of passing any errors it encounters as first argument to the
973+
callback.
974+
975+
[source,text/javascript]
976+
----
977+
function readStreamAsString(stream, callback) {
978+
var data = "";
979+
stream.on("data", function(chunk) {
980+
data += chunk;
981+
});
982+
stream.on("end", function() {
983+
callback(null, data);
984+
});
985+
stream.on("error", function(error) {
986+
callback(error);
987+
});
988+
}
989+
----
990+
991+
!!solution!!
953992

954993
=== Fixing a leak ===
955994

956-
=== Creating directories ===
995+
For easy remote access to some files, I might get into the habit of
996+
having the file server defined in this chapter running on my machine,
997+
in the `/home/marijn/public` directory. Then, one day, I find that
998+
someone has gained access to all the passwords I stored in my browser.
999+
1000+
What happened?
1001+
1002+
If it isn't clear to you yet, think back to the `urlToPath` function,
1003+
defined like this:
9571004

9581005
[source,javascript]
9591006
----
960-
methods.MKCOL = function(path, request, response) {
961-
fs.stat(path, function(error, stats) {
962-
if (error && error.code == "ENOENT")
963-
fs.mkdir(path, respondErrorOrNothing(response));
964-
else if (error)
965-
respond(500, error.toString(), response);
966-
else if (stats.isDirectory())
967-
respond(204, null, response);
968-
else
969-
respond(400, "File exists", response);
970-
});
971-
};
1007+
function urlToPath(url) {
1008+
var path = require("url").parse(url).pathname;
1009+
return "." + decodeURIComponent(path);
1010+
}
1011+
----
1012+
1013+
Now consider the fact that paths passed to the `"fs"` functions can be
1014+
relative—they may contain `"../"` to go up a directory. What happens
1015+
when a client sends requests to URLs like the ones below?
1016+
1017+
----
1018+
http://myhostname:8000/../.config/config/google-chrome/Default/Web%20Data
1019+
http://myhostname:8000/../.ssh/id_dsa
1020+
http://myhostname:8000/../../../etc/passwd
9721021
----
9731022

974-
=== A file manager ===
1023+
Change `urlToPath` to fix this problem. Take into account the fact
1024+
that node on Windows allows both forward slashes and backslashes to
1025+
separate directories.
1026+
1027+
Also, meditate on the fact that as soon as you expose some half-baked
1028+
system on the Internet, the bugs in that system can often be used to
1029+
do bad things to the machine the it is running on.
1030+
1031+
!!solution!!
1032+
1033+
It is enough to strip out all occurrences of two dots which have a one
1034+
of a slash, backslash, or the end of the string on both sided. Using
1035+
the `replace` method with a regular expression is the easiest way to
1036+
do this. Do not forget the `g` flag on the expression, or `replace`
1037+
will only replace a single instance, and people could still get around
1038+
this safety measure by including additional double dots in their
1039+
paths! Also make sure you do the replace *after* decoding the string,
1040+
or it would be possible to foil the check by encoding a dot or a
1041+
slash.
1042+
1043+
Another potentially worrying case is paths starting with a slash,
1044+
which interpreted as absolute paths. But because `urlToPath` puts a
1045+
dot character in front of the path, it is impossible to create
1046+
requests that result in such a path. Multiple slashes in a row, inside
1047+
of the path, are odd, but will be treated as a single slash by the
1048+
file system.
1049+
1050+
!!solution!!
1051+
1052+
=== Creating directories ===
9751053

1054+
Though the `DELETE` method is wired up to delete directories (using
1055+
`fs.rmdir`) when applied to one, the file server currently does not
1056+
provide any way to _create_ a directory.
1057+
1058+
Add support for a method `MKCOL`, which should create a directory by
1059+
calling `fs.mkdir`. `MKCOL` is not one of the basic HTTP methods, but
1060+
it does exist, for this same purpose, in the _WebDAV_ standard, which
1061+
specifies a set of extensions to HTTP that make it suitable for
1062+
writing resources, not just reading them.
1063+
1064+
!!solution!!
1065+
1066+
You can use the function that implements the `DELETE` method as a
1067+
blueprint for methods.`MKCOL`. When no file is found, try to create a
1068+
directory with `fs.mkdir`. When a directory exists at that path, you
1069+
can return a 204 response, so that directory creation requests are
1070+
idempotent. If a non-directory file exists here, return an error code.
1071+
400 (“bad request”) would be appropriate here.
1072+
1073+
!!solution!!
1074+
1075+
=== A public space on the web ===
1076+
1077+
Since the file server serves up any kind of files, and even includes
1078+
the right `Content-Type` header, you can use it to serve a web site.
1079+
Since it allows everybody to delete and replace files, it would be an
1080+
interesting kind of web site: one that can be modified, vandalized,
1081+
and destroyed by everybody who takes the time to create the right HTTP
1082+
request. Still, it would be a web site.
1083+
1084+
Write a simple HTML page, which includes a simple JavaScript file, put
1085+
them in a directory served by the file server, and open them in your
1086+
browser.
1087+
1088+
Next, as an advanced exercise, or even a weekend project, combine all
1089+
the knowledge you gained from this book to build a more user-friendly
1090+
interface for modifying the website, *inside* of the website itself.
1091+
1092+
Include HTML forms (Chapter 18) to edit the content of the files that
1093+
make up the website, allowing the user to update them on the server
1094+
(using HTTP request as described in Chapter 17).
1095+
1096+
Start by making only a single file editable. Then try to extend the
1097+
code to allow the user to select a file to edit, using the fact that
1098+
our file server returns lists of files when reading a directory.
1099+
1100+
Don't work directly in the code on the file server, since if you make
1101+
a mistake, you are likely to damage the files there. Instead, keep you
1102+
work _outside_ of the publicly accessible directory, and copy it in to
1103+
test it.
1104+
1105+
If your computer is directly connected to the internet, without a
1106+
firewall, router, or other interfering device in between, you might be
1107+
able to invite a friend to use your web site. To check, go to
1108+
http://www.whatismyip.com/[_whatismyip.com_], copy the IP address it
1109+
gives you into the address bar of your browser, and add `:8000` after
1110+
it to select the right port. If that brings you to your site, it is
1111+
online for everybody to see.
1112+
1113+
!!solution!!
1114+
1115+
You can create a `<textarea>` element to hold the content of the file
1116+
that is being edited. A `GET` request, using `XMLHttpRequest`, can be
1117+
used to get the current content of the file. You can use relative URLs
1118+
like _index.html_, instead of _http://localhost:8000/index.html_, to
1119+
refer to files on the same server as the running script.
1120+
1121+
Then, when the user clicks a button (you can use a `<form>` element
1122+
and `"submit"` event, or simply a `"click"` handler), make a `PUT`
1123+
request to the same URL, with the content of the `<textarea>` as
1124+
request body, to save the file.
1125+
1126+
You can then add a `<select>` element that contains all the files in
1127+
the server's root directory, by adding `<option>` elements containing
1128+
the lines returned by a `GET` request to the URL `/`. When the user
1129+
selects another file (a `"change"` event on the field), the script
1130+
must fetch and display that file. Also make sure that when saving a
1131+
file, you use the currently selected file name.
1132+
1133+
Unfortunately, the server is too simplistic to be able to reliably
1134+
read files from subdirectories, since it does not tell us whether the
1135+
thing we fetched with a `GET` request is a regular file or a
1136+
directory. Can you think of a way to extend the server to address
1137+
this?
1138+
1139+
!!solution!!

bin/get_exercises.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var fs = require("fs");
66

77
var output = [], failed = false;
88

9-
var allSolutions = fs.readdirSync("code/solutions/");
9+
var allSolutions = fs.readdirSync("code/solutions/").filter(function(file) { return !/^20/.test(file); });
1010

1111
fs.readdirSync(".").forEach(function(file) {
1212
var match = /^(\d+).*\.txt$/.exec(file), chapNum = match && match[1];
@@ -62,6 +62,37 @@ fs.readdirSync(".").forEach(function(file) {
6262
if (chapter.exercises.length) output.push(chapter);
6363
});
6464

65+
output.push({number: 20, title: "Node.js", exercises: [
66+
{name: "Content negotiation, again",
67+
file: "code/solutions/20_1_content_negotiation_again.js",
68+
number: 1,
69+
type: "js",
70+
code: "// Node exercises can not be ran in the browser",
71+
solution: fs.readFileSync("code/solutions/20_1_content_negotiation_again.js", "utf8")
72+
},
73+
{name: "Fixing a leak",
74+
file: "code/solutions/20_2_fixing_a_leak.js",
75+
number: 2,
76+
type: "js",
77+
code: "// Node exercises can not be ran in the browser",
78+
solution: fs.readFileSync("code/solutions/20_2_fixing_a_leak.js", "utf8")
79+
},
80+
{name: "Creating directories",
81+
file: "code/solutions/20_3_creating_directories.js",
82+
number: 3,
83+
type: "js",
84+
code: "// Node exercises can not be ran in the browser",
85+
solution: fs.readFileSync("code/solutions/20_3_creating_directories.js", "utf8")
86+
},
87+
{name: "A public space on the web",
88+
file: "code/solutions/20_4_a_public_space_on_the_web",
89+
number: 4,
90+
type: "js",
91+
code: "// Node exercises can not be ran in the browser\n\n// The solution for this exercise consists of several files:\n//\n// * http://eloquentjavascript.net/code/solutions/20_4_a_public_space_on_the_web/index.html\n// * http://eloquentjavascript.net/code/solutions/20_4_a_public_space_on_the_web/public_space.js\n// * http://eloquentjavascript.net/code/solutions/20_4_a_public_space_on_the_web/other.html\n",
92+
solution: "// Node exercises can not be ran in the browser"
93+
}
94+
]});
95+
6596
if (allSolutions.length) {
6697
console.error("Solution files " + allSolutions + " were not used.");
6798
failed = true;

code/file_server.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ http.createServer(function(request, response) {
2020
}).listen(8000);
2121

2222
function urlToPath(url) {
23-
return "." + require("url").parse(url).pathname;
23+
var path = require("url").parse(url).pathname;
24+
var decoded = decodeURIComponent(path);
25+
return "." + decoded.replace(/(\/|\\)\.\.(\/|\\|$)/g, "/");
2426
}
2527

2628
methods.GET = function(path, respond) {

code/file_server_promises.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ var Promise = require("promise");
33

44
var methods = Object.create(null);
55

6+
// Remember that promises can either fail or succeed. The `then`
7+
// method takes two callbacks, one to handle success and one to handle
8+
// failure. The strategy for dealing with exceptions and other failure
9+
// is to notice them in the second callback passed here, and return a
10+
// 500 response.
11+
//
12+
// On success, the promise returned by respondTo should return an
13+
// object with a `code` property indicating the response code, and
14+
// optional `body` and `type` properties. The body can be a stream to
15+
// directly pipe into the response, or a string.
16+
617
http.createServer(function(request, response) {
718
respondTo(request).then(function(data) {
819
response.writeHead(data.code, {"Content-Type": data.type || "text/plain"});
@@ -25,21 +36,47 @@ function respondTo(request) {
2536
}
2637

2738
function urlToPath(url) {
28-
return "." + require("url").parse(url).pathname;
39+
var path = require("url").parse(url).pathname;
40+
var decoded = decodeURIComponent(path);
41+
return "." + decoded.replace(/(\/|\\)\.\.(\/|\\|$)/g, "/");
2942
}
3043

44+
// Wrap the fs functions that we need with Promise.denodeify, so that
45+
// they return promises instead of directly taking a callback and
46+
// passing it an error argument.
47+
3148
var fsp = {};
3249
["stat", "readdir", "rmdir", "unlink", "mkdir"].forEach(function(method) {
3350
fsp[method] = Promise.denodeify(fs[method]);
3451
});
3552

53+
// Since several functions need to call `fsp.stat` and handle failures
54+
// that indicate non-existant files in a special way, this is a
55+
// convenience wrapper that converts file-not-found failures into
56+
// success with a null value.
57+
//
58+
// Remember that calling the `then` method returns *another* promise,
59+
// and that having a failure handler return normally replaces the
60+
// failure a success (using the returned value). We're passing null
61+
// for the success handler here (letting through normall successes
62+
// unchanged), and changing one kind of failure into success.
63+
3664
function inspectPath(path) {
3765
return fsp.stat(path).then(null, function(error) {
3866
if (error.code == "ENOENT") return null;
3967
else throw error;
4068
});
4169
}
4270

71+
// We can get by with much less explicit error handling, now that
72+
// failures automatically propagate back. The new promise returned by
73+
// `then`, as returned from this function, will use one of the values
74+
// returned here (objects with `code` properties) as its value. When a
75+
// handler passed to `then` returns another promise (as in the case
76+
// when the path refers to a directory), that promise will be
77+
// connected the promise returned by `then`, determining when and how
78+
// it is resolved.
79+
4380
methods.GET = function(path) {
4481
return inspectPath(path).then(function(stats) {
4582
if (!stats) // Does not exist
@@ -58,6 +95,10 @@ methods.GET = function(path) {
5895
var noContent = {code: 204};
5996
function returnNoContent() { return noContent; }
6097

98+
// Though failure is propagated automatically, we still have to
99+
// arrange for `noContent` to be returned when an action finishes,
100+
// which is the role of `returnNoContent` success handler.
101+
61102
methods.DELETE = function(path) {
62103
return inspectPath(path).then(function(stats) {
63104
if (!stats)
@@ -69,6 +110,9 @@ methods.DELETE = function(path) {
69110
});
70111
};
71112

113+
// To wrap a stream, we have to define our own promise, since
114+
// Promise.denodeify can only wrap simple functions.
115+
72116
methods.PUT = function(path, request) {
73117
return new Promise(function(success, failure) {
74118
var outStream = fs.createWriteStream(path);

0 commit comments

Comments
 (0)