;; -*- lisp -*- (in-package :it.bese.ucw.core) (defclass basic-backend (backend) ((host :accessor host :initarg :host :initform nil) (port :accessor port :initarg :port :initform nil) (socket :initform nil :accessor socket) (server :accessor server :initarg :server) (handlers :accessor handlers :initform '()) (request-content-length-limit :initform *request-content-length-limit* :accessor request-content-length-limit-of :initarg :request-content-length-limit))) (defprint-object (self basic-backend) (write-string ":host ") (princ (host self)) (write-string " :port ") (princ (port self))) (defclass basic-message (message) ((headers :accessor headers :initform '()) (socket :accessor socket :initarg :socket))) (defclass basic-request (basic-message request) ((cookies :accessor cookies) (parameters :accessor parameters :initform '()) (raw-uri :accessor raw-uri :initform nil) (query-path :accessor query-path :initform nil) (raw-body :accessor raw-body :initform nil) (http-method :accessor http-method :initform nil))) (defclass basic-response (basic-message response) ((headers-are-sent :accessor headers-are-sent-p :initform nil :type boolean) (cookies :accessor cookies :initform '()) (request :accessor request :initarg :request :initform nil) (html-stream :accessor html-stream :initform (make-string-output-stream)) (status :accessor status :initform +http-ok+) (external-format :accessor external-format :initform nil) (content :accessor content :initform nil))) (defclass lockable-backend-mixin () ((lock :initform (make-recursive-lock "backend lock") :accessor lock-of))) (defmacro with-lock-held-on-backend (backend &body body) `(with-recursive-lock-held ((lock-of ,backend)) ,@body)) ;;;; Cookies (defmethod cookies ((request basic-request)) (if (slot-boundp request 'cookies) (slot-value request 'cookies) (setf (slot-value request 'cookies) ;; TODO consider calling safe-parse-cookies, see rfc2109 comments (rfc2109:parse-cookies (get-header request "Cookie"))))) (defmethod find-cookie ((request basic-request) cookie) (find-cookie-using-request (context.request *context*) cookie)) (defmethod find-cookie-using-request ((request basic-request) cookie) (let ((cookie-name (cond ((stringp cookie) cookie) ((rfc2109:cookie-p cookie) (rfc2109:cookie-name cookie)) (t (error "FIND-COOKIE only supports string and rfc2109:cookie struct as cookie name specifier"))))) (find cookie-name (cookies request) :test #'string= :key #'rfc2109:cookie-name))) (defun cookie-value (cookie &optional default) (cookie-value-using-request (context.request *context*) cookie default)) (defmethod cookie-value-using-request ((request basic-request) cookie &optional default) (aif (find-cookie request cookie) (unescape-as-uri (rfc2109:cookie-value it)) default)) (defun add-cookie (cookie) "Add cookie to the current response." (add-cookie-using-response (context.response *context*) cookie)) (defmethod add-cookie-using-response ((response basic-response) cookie) (assert (rfc2109:cookie-p cookie)) (push cookie (cookies response))) ;;;; Backend methods (defmethod initialize-backend ((backend basic-backend) &key server &allow-other-keys) (when (and (null *mime-types*) (probe-file *default-mime-types-file*)) (read-mime-types *default-mime-types-file*)) (setf (server backend) server) backend) (defmethod handle-request ((backend basic-backend) (request basic-request) (response basic-response)) (let ((start-time (get-internal-real-time)) (remote-address (remote-address request)) (raw-uri (raw-uri request))) (ucw.backend.info "Handling request from ~S for ~S" remote-address raw-uri) (or (block handle (dolist* ((can-match handler url-base) (handlers backend)) (declare (ignore url-base)) (when (funcall can-match (query-path request)) (funcall handler request response) (return-from handle t))) nil) (handle-request (server backend) request response) (error 'no-handler-for-request :raw-uri raw-uri :request request)) (let ((seconds (/ (- (get-internal-real-time) start-time) internal-time-units-per-second))) (when (> seconds 0.05) (ucw.backend.info "Handled request in ~,3f secs (request came from ~S for ~S)" seconds remote-address raw-uri))))) (defmethod publish-directory ((backend basic-backend) directory-pathname url-base) (push (list (lambda (request-url) (ucw.backend.dribble "Trying to match '~S' under url-base '~S' to serve it as a file from '~S'" request-url url-base directory-pathname) (starts-with request-url url-base)) (lambda (request response) (aif (map-query-path-to-file (query-path request) url-base directory-pathname) (progn (ucw.backend.debug "Serving [~S] as a file under url-base [~S]" it url-base) (serve-file it :request request :response response)) (progn (ucw.backend.debug "Failed to serve [~S] as a file under url-base [~S]" (query-path request) url-base) (error 'no-handler-for-request :raw-uri (raw-uri request) :request request)))) url-base) (handlers backend))) ;;;; Message headers methods (defmethod get-header ((message basic-message) header-name) (cdr (assoc header-name (headers message) :test #'string-equal))) (defmethod (setf get-header) (value (message basic-message) header-name) (aif (assoc header-name (headers message) :test #'string-equal) (setf (cdr it) value) (push (cons header-name value) (headers message))) value) (defmethod add-header ((message basic-message) header-name value) (push (cons header-name value) (headers message)) value) (defmethod delete-header ((message basic-message) header-name) (setf (headers message) (delete-if #'(lambda (item) (string-equal (car item) header-name)) (headers message)))) (defmethod remote-address :around ((message basic-message)) (declare (optimize speed) (inline localhost-ip-address-p ip-address-from-private-network-p)) (let ((physical-remote-address (call-next-method))) (if (and physical-remote-address (or (ip-address-from-private-network-p physical-remote-address) (localhost-ip-address-p physical-remote-address))) ;; check if we are in a proxy setup and extract the real remote address if provided. ;; but do so only if the physical remote address is coming from a machine from the local net. ;; please note that this is not a realiable source for ip addresses! (let ((ip-as-string (get-header message "X-Forwarded-For"))) (when ip-as-string (let* ((real-remote-address (first (cl-ppcre:split "," ip-as-string :sharedp t))) (pieces (cl-ppcre:split "\\." real-remote-address :sharedp t))) (declare (type list pieces)) (if (= (length pieces) 4) (iter (with result = (make-array 4 :element-type '(unsigned-byte 8))) (for idx :from 0 :below 4) (for ip-address-part = (parse-integer (pop pieces))) (assert (<= 0 ip-address-part 255)) (setf (aref result idx) ip-address-part) (finally (return result))) (progn (ucw.backend.info "Returning NIL instead of an invalid ip address: ~S" ip-as-string) nil))))) physical-remote-address))) ;;;; Request handling (defun read-line-from-network (stream &optional (eof-error-p t)) "A simple state machine which reads chars from STREAM until it gets a CR-LF sequence or the end of the stream." (declare (optimize (speed 3))) (let ((buffer (make-array 50 :element-type '(unsigned-byte 8) :adjustable t :fill-pointer 0))) (labels ((read-next-char () (let ((byte (read-byte stream eof-error-p stream))) (if (eq stream byte) (return-from read-line-from-network buffer) (return-from read-next-char byte)))) (cr () (let ((next-byte (read-next-char))) (case next-byte (#.+linefeed+ ;; LF (return-from read-line-from-network buffer)) (t ;; add both the cr and this char to the buffer (vector-push-extend #.+carriage-return+ buffer) (vector-push-extend next-byte buffer) (next))))) (next () (let ((next-byte (read-next-char))) (case next-byte (#.+carriage-return+ ;; CR (cr)) (#.+linefeed+ ;; LF (return-from read-line-from-network buffer)) (t (vector-push-extend next-byte buffer) (next)))))) (next)))) (defun accumulate-parameters (assoc-list) "Accumulates same parameters into lists. Otherwise multiple-selection lists won't have a list value and