Image for post
Image for post

Teapot: Web Programming Made Easy

On the heels of my recently published Smalltalk tutorial, I wanted to write yet another web tutorial, this time highlighting a wonderful micro web framework in Pharo. (If you’ve not followed the original tutorial, I suggest you at least read Chapters 2, 3, and 4. Note that it’s not necessary to use the Raspberry Pi for this tutorial; any PC will do. Install Pharo from here.)

  1. a database store for login credentials (use MongoDB and VoyageMongo)
  2. a send email mechanism (use Zodiac’s #ZdcSecureSMTPClient)
  3. a method of generating UUID (use #UUIDGenerator)
Metacello new
baseline: 'Teapot';
repository: 'github://zeroflag/teapot:master/source';
load.
sudo apt-get install libssl-dev
make
Metacello new 
baseline: 'PasswordCrypt';
repository: 'github://PierceNg/PasswordCrypt/src-st';
load
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install mongodb-server
mongo # run mongo shellsudo service mongodb start # start mongodb as a servicesudo service mongodb stop # stop the service
|repo|
repo := VOMongoRepository
host: VOMongoRepository defaultHost
database: 'NCTDB'.
VORepository setRepository: repo.
Object subclass: #NCTUser
instanceVariableNames: 'name user pwdHash pwdSalt uuid
creationDate accessDate'
classVariableNames: ''
poolDictionaries: ''
category: 'NCT-Tutorial'
NCTUser class>>isVoyageRoot "a class-side method"
^ true
NCTUser class>>voyageCollectionName "a class-side method"
^ 'NCTUsers'
  • user — this is the email address of the user, guaranteed to be unique
  • pwdHash and pwdSalt — the encrypted password along with its associated salt
  • uuid — a UUID is a 128-bit number used to (almost) uniquely identify something or someone (in our case, the user)
  • creationDate — the date when the user registered; potentially useful for auditing purposes or account expiry
  • accessDate — the date when the user last logged in; potentially useful for determining how “stale” the account is
pwd := 'Amber2017'. "Amber Heard will be my girlfriend in 2017!"
salt := 'et6jm465sdf9b1sd'.
(NCTUser new)
name: 'Richard Eng';
user: 'horrido.hobbies@protonmail.com';
pwdHash: PCPasswordCrypt sha256Crypt: pwd withSalt: salt;
pwdSalt: salt;
uuid: UUID new hex asUppercase;
creationDate: DateAndTime today;
save.
Image for post
Image for post

Tea Horse Road

Teapot is based on the idea of routes. A route consists of three parts: 1) HTTP method; 2) URL pattern; 3) Action — it can be a block or a message send or an object. A list of routes basically comprises your web application.

initialize
Teapot stopAll. "reset everything"
Teapot on
GET: '/register' -> [ self registerPage: 0 name: ''
user: '' pwd: '' pwd2: '' ];
POST: '/register' -> [ :req | self verifyRegistration: req ];
GET: '/verify/<uuid>' -> [ :req | self verifyUUID: req ];

GET: '/login' -> [ self loginPage: 0 user: '' pwd: '' ];
POST: '/login' -> [ :req | self verifyLogin: req ];

before: '/welcome/*' -> [ :req |
req session attributeAt: #user
ifAbsent: [ req abort: (TeaResponse redirect
location: '/login') ] ];
GET: '/welcome/<name>' -> [ :req | self mainPage: req ];

GET: '/forgot' -> [ self forgotPage: '' ];
POST: '/forgot' -> [ :req | self handleForgot: req ];

before: '/profile/*' -> [ :req |
req session attributeAt: #user
ifAbsent: [ req abort: (TeaResponse redirect
location: '/login') ] ];
GET: '/profile' -> [ :req | self profilePage: req ];
POST: '/profile' -> [ :req | self handleProfile: req ];

GET: '/logout' -> [ :req | self logout: req ];
GET: '/books' -> [ :req | 'Check ',(req at: #title),' and ',
(req at: #limit) ]; "this route demonstrates how to pass
parameters in the URL, eg,
/books?title=The Expanse&limit=8"
start
http://nct.gov/login "nct.gov is a fictitious domain;
normally, you will register your own domain
and configure your web app to use it"
GET: '/login' -> [ self loginPage: 0 user: '' pwd: '' ]
POST: '/login' -> [ :req | self verifyLogin: req ]
GET: '/welcome/<name>' -> [ :req | self mainPage: req ]

What the user sees

The various webpages represent the public-facing view of our application. It consists of a “stylesheet” (which contains CSS instructions) and a whole bunch of HTML code. The following is the Login page:

loginPage: code user: user pwd: pwd
^ '<html> <head>',self stylesheet,'</head>
<body>
<h2>Login</h2>
<div>
<form method="POST">
Email:<br>', (self errCode: (code bitAnd:
self class ErrBadEmail)), '
<input type="text" name="user" value="',user,'"><br>
Password:<br>', (self errCode: (code bitAnd:
self class ErrBadPassword)), '
<input type="password" name="pwd" value="',pwd,'"><br><br>
<input type="submit" value="Submit">
</form>
<p><a href="/forgot">Forgot your password?</a></p>
<p><a href="/register">Sign up now!!</a></p>
</div>
</body>
</html>'
registerPage: code name: name user: user pwd: pwd pwd2: pwd2
^ '<html> <head>',self stylesheet,'</head>
<body>
<h2>Register</h2>
<div>
<form method="POST">
Fullname:<br>
<input type="text" name="name" value="',name,'"><br>
Email:<br>', (self errCode: (code bitAnd:
self class ErrBadEmail)), '
<input type="text" name="user" value="',user,'"><br>
Password:<br>', (self errCode: (code bitAnd:
self class ErrBadPassword)), '
<input type="password" name="pwd" value="',pwd,'"><br>
Password (confirm):<br>', (self errCode: (code bitAnd:
self class ErrNoPasswordMatch)), '
<input type="password" name="pwd2" value="',pwd2,'">
<br><br>
<input type="submit" value="Submit">
</form>
</div>
</body>
</html>'
  • 0 & 1 -> 0
  • 1 & 0 -> 0
  • 1 & 1 -> 1
Image for post
Image for post
stylesheet
^ '<style>
body {
background-image:
url(https://cdn-images-1.medium.com/max/2000/1*QVTC39_gW_wMXKwNxUvooA.jpeg);
background-size: 100%;
/*font-family: arial, helvetica, sans-serif;*/
text-align: center;
}
/* from https://www.w3schools.com/howto/howto_js_sidenav.asp */
body {
font-family: "Lato", sans-serif;
}
.sidenav {
height: 100%;
width: 0;
position: fixed;
z-index: 1;
top: 0;
left: 0;
background-color: #111;
overflow-x: hidden;
transition: 0.5s;
padding-top: 60px;
}
.sidenav a {
padding: 8px 8px 8px 32px;
text-decoration: none;
font-size: 25px;
color: #818181;
display: block;
transition: 0.3s;
}
.sidenav a:hover, .offcanvas a:focus{
color: #f1f1f1;
}
.sidenav .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
}
@media screen and (max-height: 450px) {
.sidenav {padding-top: 15px;}
.sidenav a {font-size: 18px;}
}
</style>'

Handling POSTs and special requests

The most important function of a web application is to handle or process HTTP requests beyond merely presenting webpages. Our application has several types of requests that need to be handled:

  • Logout — the user wants to terminate their login session
  • Registration — a potential user has submitted username and password
  • Account verification — a potential user has clicked on a verification link sent to them via email
  • User profile update — the user wants to change the password
  • Recovering from a forgotten password — the user needs a temporary password sent to them via email
verifyLogin: req
| code name user pwd doc tries |
user := req at: #user.
pwd := req at: #pwd.
code := 0.
(self validateEmail: user)
ifFalse: [ code := code + self class ErrBadEmail ].
(self validatePassword: pwd)
ifFalse: [ code := code + self class ErrBadPassword ].
code > 0 ifTrue: [ ^ self loginPage: code user: user pwd: pwd ].
doc := NCTUser selectOne: [ :each | each user = user ].
doc ifNil: [ ^ req abort: (TeaResponse redirect
location: '/register') ].
(PCPasswordCrypt sha256Crypt: pwd withSalt: doc pwdSalt) ~=
doc pwdHash ifTrue: [
tries := req session attributeAt: #tries
ifAbsentPut: [ tries := 0 ].
tries = 3 ifTrue: [ ^ self messagePage: 'Login'
msg: 'Exceeded limit. You''ve been locked out.' ].
tries := tries + 1.
req session attributeAt: #tries put: tries.
^ self messagePage: 'Login' msg: 'Wrong password.' ].
req session attributeAt: #user ifAbsentPut: user.
req session attributeAt: #uuid ifAbsentPut: doc uuid.
doc accessDate: DateAndTime today; save.
name := doc name.
^ TeaResponse redirect location: '/welcome/',
(name substrings = #() ifTrue: [ 'friend' ]
ifFalse: [ name ])
Extract username and password from the HTTP request.
Validate username and password. If this fails, report the error(s)
back to the user.
Query the database for the user document.
Compare the password hash from the database to the hash of the
submitted password. If they don't match, report the error back
to the user. If this happens three times in succession, lock the
user out (presumably, a hacker is trying to breach security).
The passwords match, so keep track of login status by storing #user
and #uuid in the HTTP session.
Update the access date for the user in the database.
Redirect the user to the main page after successful login. If the
user didn't provide a full name, use the name "friend" instead.
Extract username and password from the HTTP request.
Validate username and password. If this fails, report the error(s)
back to the potential user.
Query the database for the user document. If it exists, report to
the potential user that the user already exists.
We're ready to create a new user document, so generate a UUID and
encrypt the password. Create a new database document for the
user, storing the creation date, too.
Send an account verification email to the new user.
verifyRegistration: req
| code name user pwd pwd2 uuid salt |
name := req at: #name.
user := req at: #user.
pwd := req at: #pwd.
pwd2 := req at: #pwd2.
code := 0.
(self validateEmail: user)
ifFalse: [ code := code + self class ErrBadEmail ].
(self validatePassword: pwd)
ifFalse: [ code := code + self class ErrBadPassword ].
pwd = pwd2
ifFalse: [ code := code + self class ErrNoPasswordMatch ].
code > 0 ifTrue: [ ^ self registerPage: code
name: name user: user pwd: pwd pwd2: pwd2 ].
(NCTUser selectOne: [ :each | each user = user ]) ifNotNil: [
^ req abort: (self messagePage: 'Register'
msg: 'User already exists.') ].
uuid := UUID new hex asUppercase.
salt := self generateSalt.
(NCTUser new)
name: name;
user: user;
pwdHash: (PCPasswordCrypt sha256Crypt: pwd withSalt: salt);
pwdSalt: salt;
uuid: uuid;
creationDate: DateAndTime today;
save.
self sendEmail: user subject: 'NCTDB Account Verification'
content: 'Please click on the following link to verify your email: http://nct.gov/verify/',uuid.
^ self messagePage: 'Register'
msg: 'Check your email for account verification.'

Utilities

We use regular expressions for validating passwords and email addresses. For example,

validatePassword: aPassword
(aPassword size >= 8) & "at least 8 characters"
(aPassword matchesRegex: '^.*[A-Z].*$') &
(aPassword matchesRegex: '^.*[a-z].*$') &
(aPassword matchesRegex: '^.*\d.*$')
ifFalse: [ ^ false ].
^ true
generateSalt
^ (String new: 16) collect: [ :each |
'0123456789abcdefghijklmnopqrstuvwxyz' atRandom ]

See NCTDB in action

After you’ve written the web application, you can run it to see what it looks like. From Playground, do this instruction:

NCTDB new
http://localhost:1701/login
initialize
| secureServer teapot |
Teapot stopAll.
secureServer := (ZnSecureServer on: 1443)
certificate: '/home/pi/server.pem';
logToTranscript;
start;
yourself.
teapot := Teapot configure: { #znServer -> secureServer }.
teapot
GET: '/register' -> [ self registerPage: 0 name: ''
user: '' pwd: '' pwd2: '' ];
POST: '/register' -> [ :req | self verifyRegistration: req ];
GET: '/verify/<uuid>' -> [ :req | self verifyUUID: req ];
" ... "
start

Source code

This zip file contains the Pharo code for ‘FileIn’ with the System Browser. Unzip first. Then drag-and-drop the file into the Pharo window. FileIn the entire file.

Image for post
Image for post
Smalltalk

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store