forked from SquareBracketAssociates/EnterprisePharo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Zinc-HTTP-Server.pillar
721 lines (558 loc) · 32.5 KB
/
Zinc-HTTP-Server.pillar
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
{
"metadata" : {
"title": "Zinc HTTP: The Server Side",
"attribution": "Sven Van Caekenberghe with Luc Fabresse and Johan Fabry"
},
"headingLevelOffset":2
}
@cha:zinc-server
Zinc is both a client and server HTTP library written and maintained by Sven van Caekenberghe. HTTP clients and servers are each others' mirror: An HTTP client sends
a request and receives a response. An HTTP server receives a request
and sends a response. Hence the fundamental Zn framework objects are used to implement both clients and servers.
This chapter focuses on the server-side features of Zinc and
demonstrates through small, elegant and robust examples some
possibilities of this powerful library. The client side is described
in Chapter *Zinc Client side>../Zinc-HTTP-Client/Zinc-HTTP-Client.pillar@cha:zinc-client*
!Running a Simple HTTP Server
Getting an independent HTTP server up and running inside a Pharo image is surprisingly easy.
[[[
ZnServer startDefaultOn: 1701.
]]]
Don't try this just yet. To be able to see what is going on, it is
better to enable logging, as follows:
[[[
(ZnServer defaultOn: 1701)
logToTranscript;
start.
]]]
This starts the default HTTP server, listening on port 1701. We use
1701 in the example because using a port below 1024 requires special OS level privileges, and ports like 8080 might already be in use.
Visiting *http://localhost:1701* with a browser yields the Zn welcome
page. The Transcript produces output related to the server's
activities, for example:
[[[language=no language
2015-06-11 18:06:31 001 565881 Server Socket Bound 0.0.0.0:1701
2015-06-11 18:06:31 002 275888 Started ZnManagingMultiThreadedServer HTTP port 1701
2015-06-11 18:06:35 003 565881 Connection Accepted 127.0.0.1
2015-06-11 18:06:35 004 097901 Request Read a ZnRequest(GET /) 0ms
2015-06-11 18:06:35 005 097901 Request Handled a ZnRequest(GET /) 0ms
2015-06-11 18:06:35 006 097901 Response Written a ZnResponse(200 OK text/html;charset=utf-8 977B) 2ms
2015-06-11 18:06:35 007 097901 GET / 200 977B 2ms
2015-06-11 18:06:35 008 097901 Request Read a ZnRequest(GET /favicon.ico) 129ms
2015-06-11 18:06:35 009 097901 Request Handled a ZnRequest(GET /favicon.ico) 0ms
2015-06-11 18:06:35 010 097901 Response Written a ZnResponse(200 OK image/vnd.microsoft.icon 318B) 2ms
2015-06-11 18:06:35 011 097901 GET /favicon.ico 200 318B 2ms
2015-06-11 18:06:35 012 097901 Request Read a ZnRequest(GET /favicon.ico) 32ms
2015-06-11 18:06:35 013 097901 Request Handled a ZnRequest(GET /favicon.ico) 0ms
2015-06-11 18:06:35 014 097901 Response Written a ZnResponse(200 OK image/vnd.microsoft.icon 318B) 0ms
2015-06-11 18:06:35 015 097901 GET /favicon.ico 200 318B 0ms
2015-06-11 18:07:05 016 097901 Server Read Error ConnectionTimedOut: Data receive timed out.
2015-06-11 18:07:05 017 097901 Server Connection Closed 127.0.0.1
]]]
You can see the server starting and initializing its server socket on which it listens for incoming connections. When a connection comes in, it starts executing
its request-response loop. Then it gets a ==GET== request for ==/== (the home page), to which it answers a 200 OK response with 997 bytes of HTML. The browser also
asks for a ==favicon.ico==, which the server supplies. The request-response loop is kept alive for some time and usually closes when the other end does. Although it
looks like an error, it actually is normal, expected behavior.
The example uses the default server: Zn manages a default server to
ease interactive experimentation. The server object is obtained by:
==ZnServer default==. The default server also survives image save
and restart cycles and needs to be stopped with ==ZnServer stopDefault.== The Transcript output will confirm what happens:
[[[language=no language
2015-06-11 18:11:07 018 565881 Server Socket Released 0.0.0.0:1701
2015-06-11 18:11:07 019 275888 Stopped ZnManagingMultiThreadedServer HTTP port 1701
]]]
@@note Due to its implementation, the server will print a debug notification: ==Wait for accept timed out==, every 5 minutes. Again, although it looks like an error, it is by design and normal, expected behavior.
!Server Delegate, Testing and Debugging
The functional behavior of a ==ZnServer== is defined by an object called its delegate. A delegate implements the key method ==handleRequest:== which gets the
incoming request as parameter and has to produce a response as
result. The delegate only needs to reason in terms of a ==ZnRequest==
and a ==ZnResponse==. The technical side of being an HTTP server, like
the protocol itself, the networking and the (optional) multiprocessing, is handled by the
server object.
This allows us to write what is arguably the simplest possible HTTP
server behavior:
[[[
(ZnServer startDefaultOn: 1701)
onRequestRespond: [ :request |
ZnResponse ok: (ZnEntity text: 'Hello World!') ].
]]]
Now go to *http://localhost:1701* or do:
[[[
ZnEasy get: 'http://localhost:1701'.
]]]
This server does not look at the incoming request. It always answers ==200 OK== with a ==text/plain== string ==Hello World!==. The
==onRequestRespond:== method accepts a block that takes a request and that should produce a response. It is implemented using the helper object ==ZnValueDelegate==, which
converts ==handleRequest:== to ==value:== on a wrapped block.
!! The Default Server Delegate
Out of the box, a ==ZnServer== will have a certain functionality that is related to testing and debugging. The
==ZnDefaultServerDelegate== object implements this behavior. Assuming a server is running locally on port 1701, this is the list of URLs that are available.
- *http://localhost:1701/* the default for ==/==, equivalent to ==/welcome==
- *http://localhost:1701/bytes* a collection of bytes
- *http://localhost:1701/dw-bench* a dynamically generated page for benchmarking
- *http://localhost:1701/echo* a textual response echoing the request
- *http://localhost:1701/favicon.ico* nice Zn favicon used by browsers
- *http://localhost:1701/form-test-1* to ==/form-test-3== are form test pages
- *http://localhost:1701/help* this list of URLs
- *http://localhost:1701/random* a random string of characters
- *http://localhost:1701/session* information about the session
- *http://localhost:1701/status* a textual page showing some server internals
- *http://localhost:1701/unicode* a UTF-8 encoded page listing the first 591 Unicode characters
- *http://localhost:1701/welcome* the standard Zn greeting page
The random handler normally returns 64 characters, you can specify your own size as well. For example, ==/random/1024== will respond with a 1Kb random string.
The random pattern consists of hexadecimal digits and ends with a linefeed. The standard, slower UTF-8 encoding is used instead of the faster LATIN-1 encoding.
The bytes handler has a similar size option. Its output is in the form of a repeating BCDA pattern. When requesting equally sized byte patterns repeatably, some
extra server side caching will improve performance.
!! Testing and Debugging
The echo handler is used extensively by the unit tests. It not only lists the request headers as received by the server, but even the entity if there is one. In
case of a non-binary entity, the textual contents will be included. This is really useful to debug PUT or POST requests.
In general, to help in debugging a server, enabling logging is
important to learn what is going on. Breakpoints can be put anywhere
in the server, but interrupting a running
server can sometimes be a bit hard or produce strange results. This is
because the server and its spawned handler subprocesses are different from the UI process.
When logging is enabled, the server will also keep track of the last request and response it processed. You can inspect these to find out what happened, even if
there was no debugger raised.
!Server Authenticator
Similar to the delegate, a ==ZnServer== also has an authenticator object whose function is to authenticate requests. An authenticator has to implement the
==authenticateRequest:do:== method whose first argument is the incoming request and second argument a block. This method has to produce a response, like
==handleRequest:== does. If the request is allowed, the block should be evaluated, which will produce the response. If the request is denied, the authenticator should generate a 401 Unauthorized response. One simple authenticator is available to add basic HTTP authentication:
[[[
(ZnServer startDefaultOn: 1701)
authenticator: (ZnBasicAuthenticator username: 'admin' password: 'secret').
]]]
Now, when you try to visit the server at *http://localhost:1701* you will have to provide a username and password. Note that it is also possible to use ==ZnEasy== to send
a get request to this URL with these credentials.
[[[
ZnEasy
get: 'http://localhost:1701'
username: 'admin'
password: 'secret'.
]]]
@@note Using ==ZnBasicAuthenticator== or implementing an alternative authenticator is only one of several possibilities to address the problem of adding security to a web site or web application.
!Logging
Log output consists of a log message preceded by a number of fixed fields. Here is an example of a server log.
[[[language=no language
2015-06-11 10:19:59 001 220937 Server Socket Bound 0.0.0.0:1701
2015-06-11 10:19:59 002 233075 Started ZnManagingMultiThreadedServer HTTP port 1701
2015-06-11 10:25:36 003 220937 Connection Accepted 127.0.0.1
2015-06-11 10:25:36 004 879540 Request Read a ZnRequest(GET /help) 2ms
2015-06-11 10:25:36 005 879540 Request Handled a ZnRequest(GET /help) 0ms
2015-06-11 10:25:36 006 879540 Response Written a ZnResponse(200 OK text/html;charset=utf-8 867B) 0ms
2015-06-11 10:25:36 007 879540 GET /help 200 867B 0ms
2015-06-11 10:25:38 008 879540 Request Read a ZnRequest(GET /help) 1770ms
2015-06-11 10:25:38 009 879540 Request Handled a ZnRequest(GET /help) 0ms
2015-06-11 10:25:38 010 879540 Response Written a ZnResponse(200 OK text/html;charset=utf-8 867B) 0ms
2015-06-11 10:25:38 011 879540 GET /help 200 867B 0ms
2015-06-11 10:25:44 012 879540 Request Read a ZnRequest(GET /unicode) 6082ms
2015-06-11 10:25:44 013 879540 Request Handled a ZnRequest(GET /unicode) 5ms
2015-06-11 10:25:44 014 879540 Response Written a ZnResponse(200 OK text/html;charset=utf-8 11454B) 2ms
2015-06-11 10:25:44 015 879540 GET /unicode 200 11454B 7ms
]]]
The first two fields are the date and time in a fixed sized format.
The next field is the id of the log entry.
The next number is a fixed sized hash of the process ID. Note how 3 different processes are
involved: the one starting the server (probably the UI process), the actual server listening process, and the client worker process spawned to handle the request.
Both ==ZnClient== and ==ZnServer== implement logging using a similar mechanism based on the announcements framework. ==ZnLogEvents== are subclasses of the ==Announcement== class and are
sent by an HTTP server or client containing logging information. A log event has a TimeStamp, an id, and a message.
To log something, a server or client uses its own log methods.
For example, a server receives a ==logConnectionAccepted:== message with the socket that will process the request as argument. In ==ZnSingleThreadedServer==, the implementation of ==logConnectionAccepted:== is:
[[[
logConnectionAccepted: socket
logLevel < 3 ifTrue: [ ^ nil ].
^ (self newLogEvent: ZnConnectionAcceptedEvent)
address: ([ socket remoteAddress ] on: Error do: [ nil ]);
emit
]]]
This logging mechnism can be easily customized by implementing subclasses of ==ZnLogEvent==. For example, ==ZnConnectionAcceptedEvent== is a subclass of ==ZnLogEvent== customized for connection acceptation.
You can also provide your own listener for ==ZnLogEvents==.
The following example shows how to log events in a file named
==zn.log==, next to the image.
[[[
| logger |
loggerStream := (Smalltalk imageDirectory / 'zn.log') writeStream.
ZnLogEvent announcer
when: ZnLogEvent
do: [ :event | loggerStream lf; print: event ].
(ZnServer defaultOn: 1701) start.
]]]
!Server Variants and Life Cycle
The class side of ==ZnServer== is actually a factory to instantiate a particular concrete ==ZnServer== subclass, as can be seen in ==defaultServerClass==. The
hierarchy looks as follows.
[[[language=no language
ZnServer
+ ZnSingleThreadedServer
+ ZnMultiThreadedServer
+ ZnManagedMultiThreadedServer
]]]
==ZnServer== is an abstract class. ==ZnSingleThreadedServer== implements the core server functionality. It runs in one single process, which means it can only
handle one request at a time, making it easier to understand and debug. ==ZnMultiThreadedServer== spawns a new process on each incoming request, possibly
handling multiple request/response cycles on the same connection. ==ZnManagedMultiThreadedServers== keeps explicit track of which connections are alive so that
they can be stopped when the server stops instead of letting them die out.
Server instances can be started and stopped using ==start== and ==stop==. By registering a server instance, by sending it ==register==, it becomes managed. That
means it will survive image save and restart. This only happens automatically with the ==default== server, for other server instances it needs to be enabled manually.
The main parameter a server needs is the port on which it will listen. Additionally, you can restrict the network interface the server should listen on by
setting its ==bindingAddress:== to some IP address. The default, which is ==nil== or ==#[0 0 0 0]==, means to listen on all interfaces. With == #[127 0 0 1]==, the
server will not respond to requests over its normal network, but only to requests coming from the same host. This is often used to increase security while proxying.
[[[
(ZnServer defaultOn: 1701)
bindingAddress: #[127 0 0 1];
logToTranscript;
start.
]]]
!Static File Server
When most people think about a web server, they imagine what is technically called static file serving. There is a directory full of HTML, image, CSS, and other files, somewhere on a machine, and the web server serves
these files over HTTP to web browser clients anywhere on the network. This is indeed what Apache does in its most basic form.
Zn can do this by using a ==ZnStaticFileServerDelegate==. Given a directory and an optional prefix, this delegate will serve all files it finds in that directory, for example:
[[[
(ZnServer startDefaultOn: 1701)
delegate: (
ZnStaticFileServerDelegate new
directory: '/var/www' asFileReference;
prefixFromString: 'static-files';
yourself).
]]]
If we suppose the contents of ==/var/www== is
- index.html
- small.html
You can access these files with these URLs
- *http://localhost:1701/static-files/index.html*
- *http://localhost:1701/static-files/small.html*
The prefix is added in front of all files being served, the actual directory where the files reside is of course invisible to the end web user. If no prefix is specified, the files will be served directly.
Note how all other URLs result in a 404 Not found error. Note that while the ==ZnStaticFileServerDelegate== is very simple, it does have a couple of capabilities. Most importantly, it will do what most
people expect with respect to directories. Consider the following URLs:
- *http://localhost:1701/static-files*
- *http://localhost:1701/static-files/*
The first URL above will result in a redirect to the second. The second URL will look for either an ==index.html== or ==index.htm== file and serve that. Automatic
generation of an index page when there is no index file is not implemented.
As a static file server, the following features are implemented:
- automatic determination of the content mime-type based on the file extension
- correct setting of the content length based on the file length
- usage of streaming
- addition of correct modification date based on the files' last modification date
- correct reaction to the if-modified-since protocol
- optional expiration and caching control
Here is a more complex example:
[[[
(ZnServer startDefaultOn: 1701)
logToTranscript;
delegate: (
ZnStaticFileServerDelegate new
directory: '/var/www' asFileReference;
mimeTypeExpirations: ZnStaticFileServerDelegate defaultMimeTypeExpirations;
yourself);
authenticator: (
ZnBasicAuthenticator username: 'admin' password: 'secret').
]]]
In the above example, we add the optional expiration and caching control based on default settings. Note that it is easy to combine static file serving with logging and authentication.
% JF: Commented out because not in the scope of the book
% !!Monticello repository server
% Another, interesting, proof of concept example is ==ZnMonticelloServerDelegate==. It allows you to run your own Smalltalk source code repository. You can try it out as follows.
% [[[
% (ZnServer startDefaultOn: 1701)
% delegate: (ZnMonticelloServerDelegate new
% directory: '/Users/sven/Tmp/monticello' asFileReference;
% yourself).
% ]]]
% Now create an MCHttpRepository pointing to your own server.
% [[[
% MCHttpRepository
% location: 'http://localhost:1701'
% user: ''
% password: ''.
% ]]]
% It should work just like any other repository, although it is for package storage only. What makes this example interesting is that it implements 3 different kinds of requests:
% - list the repository, a GET for /
% - get a version, a GET for a specific file
% - store a version, a PUT of a specific file
!Dispatching
Dispatching or routing is HTTP application server speak for deciding what part of the software will handle an incoming request. This decision can be made on
any of the properties of the request: the HTTP method, the URL or part of it, the query parameters, the meta headers and the entity body. Different applications
will prefer different kinds of solutions to this problem.
Zinc HTTP Components is a general framework that offers all the necessary components to build your own dispatcher. Out of the box, there are the different
delegates that we discussed before. Most of these have hand coded dispatching in their ==handleRequest:== method.
==ZnDefaultServerDelegate== can be configured to perform dispatching as it uses a prefix map internally that maps URI prefixes to internal methods. Configuration is by installing a block as the value to a prefix, which accepts the request and produces a response. Here is an example of using that capability:
[[[
| staticFileServerDelegate |
ZnServer startDefaultOn: 8080.
(staticFileServerDelegate := ZnStaticFileServerDelegate new)
prefixFromString: 'zn';
directory: '/home/ubuntu/zn' asFileReference.
ZnServer default delegate prefixMap
at: 'zn'
put: [ :request | staticFileServerDelegate handleRequest: request ];
at: 'redirect-to-zn'
put: [ :request | ZnResponse redirect: '/zn/index.html' ];
at: '/'
put: 'redirect-to-zn'.
]]]
This is taken from the configuration of what runs at *http://zn.stfx.eu*. A static web server is set up under the ==zn== prefix pointing to the directory
==/home/ubuntu/zn==. The prefix map of the default delegate is kept as is, with its standard functionality, but is modified, such that
- anything with a ==zn== prefix is directly forwarded to the static file server
- a special ==redirect-to-zn== prefix is set up which will issue a redirect to ==/zn/index.html==
- the default ==/== handler is linked to ==redirect-to-zn== instead of the default ==welcome:==
Another option is to use ==ZnDispatcherDelegate==.
[[[
(ZnServer startDefaultOn: 9090) delegate: (
ZnDispatcherDelegate new
map: '/hello'
to: [ :request :response |
response entity: (ZnEntity html: '<h1>hello!</h1>') ]).
]]]
You configure the dispatcher using ==map:to:== methods. First argument is the prefix, second argument is a block taking two arguments: the incoming request and
an already instantiated response.
!Character Encoding
Proper character encoding and decoding is crucial in today's international world. Pharo Smalltalk encodes characters and strings using Unicode. The primary
internet encoding is UTF-8, but a couple of others are used as well. To translate between these two, a concrete ==ZnCharacterEncoding== subclass like ==ZnUTF8Encoder== is used.
==ZnCharacterEncoding== is an extension and reimplementation of regular ==TextConverter==. It only works on binary input and generated binary output and it adds the
ability to compute the encoded length of a source character, a crucial operation for HTTP. It is more correct and will throw proper exceptions when things go wrong.
Character encoding is mostly invisible. Here are some code snippets using the encoders directly, feel free to substitute any Unicode character to make the test more interesting.
[[[
| encoder string |
encoder := ZnUTF8Encoder new.
string := 'any Unicode'.
self assert: (encoder decodeBytes: (encoder encodeString: string)) equals: string.
encoder encodedByteCountForString: string.
]]]
There are no automatic conversions in Zinc, so no defaults are assumed. Instead you should specify a proper Content-Type header including the charset information. Otherwise Zinc has no chance of knowing what to use and the default NullEncoder will make your string wrong.
Consider the following example:
[[[
ZnServer startDefaultOn: 1701.
ZnClient new
url: 'http://localhost:1701/echo';
entity: (ZnEntity with: 'An der schönen blauen Donau');
post.
ZnClient new
url: 'http://localhost:1701/echo';
entity: (
ZnEntity
with: 'An der schönen blauen Donau'
type: (ZnMimeType textPlain charSet: #'iso-8859-1'; yourself));
post;
yourself.
]]]
In the first case, a UTF-8 encoded string is POST-ed and correctly returned (in a UTF-8 encoded response).
In the second case, an ISO-8859-1 encoded string is POST-ed and correctly returned (in a UTF-8 encoded response).
In both cases the decoding was done correctly, using the specified charset (if that is missing, the ZnNullEncoder is used). Now, ö is not a perfect test example because its Unicode encoding value is 246 in decimal, U\+00F6 in hex, still fits in 1 byte and hence survives null encoding/decoding (it would not be the case with € for example). That is why the following still works, although it is wrong to drop the charset.
[[[
ZnClient new
url: 'http://localhost:1701/echo';
entity: (
ZnEntity
with: 'An der schönen blauen Donau'
type: (ZnMimeType textPlain clearCharSet; yourself));
post;
yourself.
]]]
!Resource Protection Limits, Content and Transfer Encoding
Internet facing HTTP servers will come under attack by malicious clients. Good security is thus important. The first step is a correct and safe implementation of
the HTTP protocol. Another way a server protects itself is by implementing some resource limits.
Zinc HTTP Components currently implements and enforces the following limits:
- maximumLineLength (4Kb), impacting mainly the size of a header pair
- maximumEntitySize (16Mb), the size of incoming entities
- maximumNumberOfDictionaryEntries (256), which is used in headers, URLs and some entities
Of course these values may be customized if one needs to.
Also, Zn implements two important techniques used by HTTP servers when they send entity bodies to clients: Gzip encoding and chunked transfer encoding. The
first one adds compression. The second one is used when the size of an entity is not known up front. Instead chunks of certain sizes are sent until the entity is complete.
All this is handled internally and invisibly. The main object dealing with content and transfer encoding is ==ZnEntityReader==. When necessary, the binary socket
stream is wrapped with either a ==ZnChunkedReadStream== and/or a ==GZipReadStream==. Zn also makes use of a ==ZnLimitedReadStream== to make sure there is no read
beyond the boundaries of one single request's body, provided the content length is set.
!Seaside Adaptor
*Seaside>http://www.seaside.st/* is a well known, cross platform, advanced Smalltalk web application framework. It does not provide its own HTTP server but
relies on an existing one by means of an adaptor. It works well with Zn, through the use of a ==ZnZincServerAdaptor==. It comes already included with certain Seaside distributions and on Pharo Smalltalk it is the default.
Starting this adaptor can be done using the Seaside Control panel in the normal way. Alternatively, the adaptor can be started programmatically.
[[[
ZnZincServerAdaptor startOn: 8080.
]]]
Since Seaside does its own character conversions, the Zn adaptor is configured to work in binary mode for maximum efficiency. There is complete support for POST
and PUT requests with entities in form URL, multipart or raw encoding.
There is even a special adaptor that combines being a Seaside adaptor with static file serving, which is useful if you don't like the WAFileLibrary machinery
and prefer plain static files served directly.
[[[
ZnZincStaticServerAdaptor startOn: 8080 andServeFilesFrom: '/var/www/'.
]]]
!Scripting a REST Web Service with Zinc
As a last example of the use of Zinc HTTP, we now show the implementation of REST web services, both the client and the server parts. REST or
*Representational State Transfer>http://en.wikipedia.org/wiki/Representational_state_transfer* is an architectural style most easily described as using HTTP
verbs and URIs to deal with encoded resources. Some kind of framework is needed to successfully implement a non-trivial REST service. There is one available in
the Zinc-REST-Server package, for example. Here we will implement a very small, simplified example by hand, for educational purposes.
The service will allow arbitrary *JSON>http://www.json.org/* objects to be stored on the server, each identified by an URI allocated by the server. Here is the REST API exposed by the server:
;==GET /==
:Returns a list of all known stored object URIs;
;==GET /n==
:Returns the JSON object known under URI ==/n==;
;==POST /==
:Creates a new entry with JSON as contents, returns the new URI;
;==PUT /n==
:Updates (replaces) the contents of an existing JSON object known under URI ==/n==;
;==DELETE /n==
:Removes the JSON object known under URI ==/n==.
!!The Server Code
A proper implementation should best use a couple of classes. However for brevity, the following implementation is written in a workspace, not using any classes. It requires STON (see
Chapter *STON>../STON/STON.pillar@cha:ston*) and starts by creating two global variables to hold the stored objects and the last ID used. The former is a standard dictionary mapping string URIs to objects.
[[[
JSONStore := Dictionary new.
ServerLastId := 0.
]]]
The server implementation uses two helper objects: a ==jsonEntityBuilder== and a ==mapper==. Both make use of block closures.
[[[
| jsonEntityBuilder mapper |
jsonEntityBuilder := [ :object |
ZnEntity
with: ((String streamContents: [ :stream |
STON jsonWriter
on: stream;
prettyPrint: true;
nextPut: object.
stream cr ])
replaceAll: Character cr with: Character lf)
type: ZnMimeType applicationJson ].
]]]
The ==jsonEntityBuilder== block helps in transforming Smalltalk objects to a JSON entity. We use the STON writer and reader here because they are backwards compatible with JSON. We
use linefeeds to improve compatibility with internet conventions as well as pretty printing to help human interpretation of the data.
[[[
mapper := {
[ :request |
request uri isSlash and: [ request method = #GET ] ]
->
[ :request |
ZnResponse
ok: (jsonEntityBuilder value: JSONStore keys asArray) ].
"----------------------------------------------------------------"
[ :request |
request uri pathSegments size = 1 and: [ request method = #GET ] ]
->
[ :request | | uri |
uri := request uri pathPrintString.
JSONStore
at: uri
ifPresent: [ :object |
ZnResponse ok: (jsonEntityBuilder value: object) ]
ifAbsent: [ ZnResponse notFound: uri ] ].
"----------------------------------------------------------------"
[ :request |
(request uri isSlash
and: [ request method = #POST ])
and: [ request contentType = ZnMimeType applicationJson ] ]
->
[ :request | | uri |
uri := '/', (ServerLastId := ServerLastId + 1) asString.
JSONStore at: uri put: (STON fromString: request contents).
(ZnResponse created: uri)
entity: (jsonEntityBuilder value: 'Created ', uri);
yourself ].
"----------------------------------------------------------------"
[ :request |
(request uri pathSegments size = 1
and: [ request method = #PUT ])
and: [ request contentType = ZnMimeType applicationJson ]]
->
[ :request | | uri |
uri := request uri pathPrintString.
(JSONStore includesKey: uri)
ifTrue: [
JSONStore
at: uri
put: (STON fromString: request contents).
ZnResponse ok: (jsonEntityBuilder value: 'Updated') ]
ifFalse: [ ZnResponse notFound: uri ] ].
"----------------------------------------------------------------"
[ :request |
request uri pathSegments size = 1
and: [ request method = #DELETE ] ]
->
[ :request | | uri |
uri := request uri pathPrintString.
(JSONStore removeKey: uri ifAbsent: [ nil ])
ifNil: [ ZnResponse notFound: uri ]
ifNotNil: [
ZnResponse ok: (jsonEntityBuilder value: 'Deleted') ] ].
}.
]]]
The mapper is a dynamically created array of associations (not a dictionary). Each association consists of two blocks. The first block is a condition: it tests a request and returns true when it matches. The second block is a handler that is evaluated with the incoming request to produce a response (if and only if the first condition matched).
The associations in the mapper follow exactly the list of the REST API as shown earlier. The server is set up with a block based delegate using the
==onRequestRepond:== method. Again, a more object-oriented implementation would use a proper delegate object here, but for this example, the block is sufficient.
The server logic thus becomes: find a matching entry in the mapper and invoke it. If no matching entry is found, we have a bad request. Error handling is of course rather limited in this small example.
[[[
(ZnServer startDefaultOn: 1701)
logToTranscript;
onRequestRespond: [ :request |
(mapper
detect: [ :each | each key value: request ]
ifNone: [ nil ])
ifNil: [ ZnResponse badRequest: request ]
ifNotNil: [ :handler | handler value value: request ] ].
]]]
!!Using the Server
Here is an example command line session using the Unix utility *curl>http://en.wikipedia.org/wiki/CURL*, interacting with the server.
[[[language=bash
$ curl http://localhost:1701/
[ ]
$ curl -X POST -d '[1,2,3]' -H'Content-type:application/json' http://localhost:1701/
"Created /1"
$ curl http://localhost:1701/1
[
1,
2,
3
]
$ curl -X POST -d '{"bar":-2}' -H'Content-type:application/json' http://localhost:1701/
"Created /2"
$ curl http://localhost:1701/2
{
"bar" : -2
}
$ curl -X PUT -d '{"bar":-1}' -H'Content-type:application/json' http://localhost:1701/2
"Updated /2"
$ curl http://localhost:1701/2
{
"bar" : -1
}
$ curl http://localhost:1701/
[
"/1",
"/2"
]
$ curl -X DELETE http://localhost:1701/2
"Deleted /2"
$ curl http://localhost:1701/2
Not Found /2
]]]
!!A Zinc Client
It is trivial to use ==ZnClient== to have the same interaction. But we can do better: using a contentWriter and contentReader, we can customise the client to do the JSON conversions automatically.
[[[
| client |
client := ZnClient new
url: 'http://localhost:1701';
enforceHttpSuccess: true;
accept: ZnMimeType applicationJson;
contentWriter: [ :object |
ZnEntity
with: (String streamContents: [ :stream |
STON jsonWriter on: stream; nextPut: object ])
type: ZnMimeType applicationJson ];
contentReader: [ :entity | STON fromString: entity contents ];
yourself.
]]]
Now we can hold the same conversation as above, only in this case in terms of real Smalltalk objects.
[[[
client get: '/'
--> #()
client post: '/' contents: #(1 2 3)
--> 'Created /1'
client get: '/1'
--> #(1 2 3)
client post: '/' contents: (Dictionary with: #bar -> -2)
---> 'Created /2'
client put: '/2' contents: (Dictionary with: #bar -> -1)
--> 'Updated'
client get: '/2'
--> a Dictionary('bar'->-1 )
client get: '/'
--> #('/1' '/2')
client delete: '/2'
--> 'Deleted'
client get: '/2'
--> throws a ZnHttpUnsuccessful exception
]]]
!Conclusion
Zinc HTTP Components was written with the explicit goal of allowing users to explore the implementation. The test suite contains many examples that can serve as learning material. This carefulness while writing Zinc HTTP Components code now enable users to customize it to their need or to build on top of it. Zinc is indeed an extremely malleable piece of software.