# webserver-tutorial.md -rw-r--r-- 6.0 KiB View raw

#Building a Simple Login System with Hunchentoot in Common Lisp

In this tutorial, we'll walk through creating a basic login system using Hunchentoot, a web server written in Common Lisp. This is perfect for beginners who are just starting with web development in Lisp. We'll cover setting up a server, handling sessions, and creating protected routes.

#Prerequisites

Before we begin, make sure you have:

#Step 1: Setting Up the Project

First, let's set up our project and load the necessary libraries:

(ql:quickload '(:hunchentoot :spinneret))

(defpackage :login-example
  (:use :cl :hunchentoot :spinneret))

(in-package :login-example)

#Step 2: Creating the Server

Now, let's create functions to start and stop our server:

(defvar *server*)

(defun start-server (&key (port 8080))
  (setf *server* (make-instance 'easy-acceptor :port port))
  (start *server*))

(defun stop-server ()
  (stop *server*))

#Step 3: Setting Up User Data

For this example, we'll use a simple in-memory user database:

(defvar *login* '(:user "foo" :password "bar"))

#Step 4: Session Management

We'll create a function to check if a user is logged in:

(defun loggedin-p ()
  (and (session-value 'user)
       (session-value 'loggedin)))

#Step 5: Creating HTML Templates

We'll use Spinneret to create HTML templates for our login and welcome pages:

(defun login-page (&key (error nil))
  (with-html-string
    (:html
     (:head (:title "Login"))
     (:body
      (when error
        (:p (:style "color: red;") "Invalid username or password"))
      (:form :method "post" :action "/"
             (:p "Username: " (:input :type "text" :name "user"))
             (:p "Password: " (:input :type "password" :name "password"))
             (:p (:input :type "submit" :value "Log In")))))))

(defun welcome-page (username)
  (with-html-string
    (:html
     (:head (:title "Welcome"))
     (:body
      (:h1 (format nil "Welcome, ~A!" username))
      (:p "You are logged in.")
      (:a :href "/logout" "Log out")))))

#Step 6: Creating the Main Handler

Now, let's create our main handler that will manage both GET and POST requests:

(define-easy-handler (home :uri "/") ()
  (start-session)
  (ecase (request-method*)
    (:get (if (loggedin-p)
              (welcome-page (session-value 'user))
              (login-page)))
    (:post (let ((user (post-parameter "user"))
                 (password (post-parameter "password")))
             (if (and (string= user (getf *login* :user))
                      (string= password (getf *login* :password)))
                 (progn
                   (setf (session-value 'user) user)
                   (setf (session-value 'loggedin) t)
                   (welcome-page user))
                 (login-page :error t))))))

#Step 7: Adding a Logout Handler

Let's add a handler for logging out:

(define-easy-handler (logout :uri "/logout") ()
  (setf (session-value 'user) nil)
  (setf (session-value 'loggedin) nil)
  (redirect "/"))

#Step 8: Starting the Server

To run our application, we simply call:

(start-server)

Now, you can visit http://localhost:8080 in your browser to see the login page.

#Understanding the Code

Let's break down some key points:

  1. Session Management: We use start-session at the beginning of our main handler. This is safe because if a session already exists, it won't create a new one.

  2. Login Logic: We check the credentials against our simple in-memory database and set session values if they match.

  3. Logout Handling: We clear session values and redirect to the home page.

#Improving Security

For a production application, you'd want to implement several security enhancements:

  1. Use HTTPS to protect login credentials in transit.
  2. Hash passwords instead of storing them in plain text.
  3. Implement protection against brute-force attacks.

#Creating Protected Routes

What if we want to create routes that are only accessible to logged-in users? We can create a macro for this:

(defmacro define-protected-handler ((name &key uri) &body body)
  `(define-easy-handler (,name :uri ,uri) ()
     (if (loggedin-p)
         (progn ,@body)
         (redirect "/"))))

Now we can easily create protected routes:

(define-protected-handler (user-profile :uri "/profile")
  (with-html-string
    (:html
     (:head (:title "User Profile"))
     (:body
      (:h1 "Your Profile")
      (:p "Welcome to your profile page, " (session-value 'user) "!")
      (:a :href "/" "Back to Home")))))

#Advanced Concept: Middleware-Style Protection

For more flexibility, we can create a middleware-like function:

(defun require-login (next)
  (lambda ()
    (if (loggedin-p)
        (funcall next)
        (redirect "/"))))

(defmacro define-protected-handler-with-middleware ((name &key uri) &body body)
  `(define-easy-handler (,name :uri ,uri) ()
     (funcall (require-login (lambda () ,@body)))))

This approach uses higher-order functions to wrap our handler logic with authentication checks.

#Why use funcall here?

In the define-protected-handler-with-middleware macro, we use funcall because:

  1. require-login returns a function, not the result of calling a function.
  2. We need to actually call this returned function when the handler is invoked.
  3. funcall is used in Lisp to call a function object.

This pattern demonstrates the power of Lisp's functional programming features in creating flexible web application structures.

#Conclusion

This tutorial introduced you to building a simple login system with Hunchentoot in Common Lisp. We covered setting up a server, managing sessions, creating handlers, and even touched on more advanced concepts like creating protected routes and using higher-order functions for middleware-like functionality.

Remember, this is a basic example and should be enhanced with proper security measures for any production use. Happy Lisp coding!