1225 lines
48 KiB
EmacsLisp
1225 lines
48 KiB
EmacsLisp
|
;;; request.el --- Compatible layer for URL request in Emacs -*- lexical-binding: t; -*-
|
|||
|
|
|||
|
;; Copyright (C) 2012 Takafumi Arakaki
|
|||
|
;; Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2012
|
|||
|
;; Free Software Foundation, Inc.
|
|||
|
|
|||
|
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
|||
|
;; Package-Requires: ((emacs "24.4"))
|
|||
|
;; Package-Version: 20170131.1747
|
|||
|
;; Version: 0.3.0
|
|||
|
|
|||
|
;; This file is NOT part of GNU Emacs.
|
|||
|
|
|||
|
;; request.el is free software: you can redistribute it and/or modify
|
|||
|
;; it under the terms of the GNU General Public License as published by
|
|||
|
;; the Free Software Foundation, either version 3 of the License, or
|
|||
|
;; (at your option) any later version.
|
|||
|
|
|||
|
;; request.el is distributed in the hope that it will be useful,
|
|||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
|
;; GNU General Public License for more details.
|
|||
|
|
|||
|
;; You should have received a copy of the GNU General Public License
|
|||
|
;; along with request.el.
|
|||
|
;; If not, see <http://www.gnu.org/licenses/>.
|
|||
|
|
|||
|
;;; Commentary:
|
|||
|
|
|||
|
;; Request.el is a HTTP request library with multiple backends. It
|
|||
|
;; supports url.el which is shipped with Emacs and curl command line
|
|||
|
;; program. User can use curl when s/he has it, as curl is more reliable
|
|||
|
;; than url.el. Library author can use request.el to avoid imposing
|
|||
|
;; external dependencies such as curl to users while giving richer
|
|||
|
;; experience for users who have curl.
|
|||
|
|
|||
|
;; Following functions are adapted from GNU Emacs source code.
|
|||
|
;; Free Software Foundation holds the copyright of them.
|
|||
|
;; * `request--process-live-p'
|
|||
|
;; * `request--url-default-expander'
|
|||
|
|
|||
|
;;; Code:
|
|||
|
|
|||
|
(eval-when-compile
|
|||
|
(defvar url-http-method)
|
|||
|
(defvar url-http-response-status))
|
|||
|
|
|||
|
(require 'cl-lib)
|
|||
|
(require 'url)
|
|||
|
(require 'mail-utils)
|
|||
|
|
|||
|
(defgroup request nil
|
|||
|
"Compatible layer for URL request in Emacs."
|
|||
|
:group 'comm
|
|||
|
:prefix "request-")
|
|||
|
|
|||
|
(defconst request-version "0.3.0")
|
|||
|
|
|||
|
|
|||
|
;;; Customize variables
|
|||
|
|
|||
|
(defcustom request-storage-directory
|
|||
|
(concat (file-name-as-directory user-emacs-directory) "request")
|
|||
|
"Directory to store data related to request.el."
|
|||
|
:type 'directory)
|
|||
|
|
|||
|
(defcustom request-curl "curl"
|
|||
|
"Executable for curl command."
|
|||
|
:type 'string)
|
|||
|
|
|||
|
(defcustom request-curl-options nil
|
|||
|
"curl command options.
|
|||
|
|
|||
|
List of strings that will be passed to every curl invocation. You can pass
|
|||
|
extra options here, like setting the proxy."
|
|||
|
:type '(repeat string))
|
|||
|
|
|||
|
(defcustom request-backend (if (executable-find request-curl)
|
|||
|
'curl
|
|||
|
'url-retrieve)
|
|||
|
"Backend to be used for HTTP request.
|
|||
|
Automatically set to `curl' if curl command is found."
|
|||
|
:type '(choice (const :tag "cURL backend" curl)
|
|||
|
(const :tag "url-retrieve backend" url-retrieve)))
|
|||
|
|
|||
|
(defcustom request-timeout nil
|
|||
|
"Default request timeout in second.
|
|||
|
`nil' means no timeout."
|
|||
|
:type '(choice (integer :tag "Request timeout seconds")
|
|||
|
(boolean :tag "No timeout" nil)))
|
|||
|
|
|||
|
(defcustom request-temp-prefix "emacs-request"
|
|||
|
"Prefix for temporary files created by Request."
|
|||
|
:type 'string
|
|||
|
:risky t)
|
|||
|
|
|||
|
(defcustom request-log-level -1
|
|||
|
"Logging level for request.
|
|||
|
One of `error'/`warn'/`info'/`verbose'/`debug'.
|
|||
|
-1 means no logging."
|
|||
|
:type '(choice (integer :tag "No logging" -1)
|
|||
|
(const :tag "Level error" error)
|
|||
|
(const :tag "Level warn" warn)
|
|||
|
(const :tag "Level info" info)
|
|||
|
(const :tag "Level Verbose" verbose)
|
|||
|
(const :tag "Level DEBUG" debug)))
|
|||
|
|
|||
|
(defcustom request-message-level 'warn
|
|||
|
"Logging level for request.
|
|||
|
See `request-log-level'."
|
|||
|
:type '(choice (integer :tag "No logging" -1)
|
|||
|
(const :tag "Level error" error)
|
|||
|
(const :tag "Level warn" warn)
|
|||
|
(const :tag "Level info" info)
|
|||
|
(const :tag "Level Verbose" verbose)
|
|||
|
(const :tag "Level DEBUG" debug)))
|
|||
|
|
|||
|
|
|||
|
;;; Utilities
|
|||
|
|
|||
|
(defun request--safe-apply (function &rest arguments)
|
|||
|
(condition-case err
|
|||
|
(apply #'apply function arguments)
|
|||
|
((debug error))))
|
|||
|
|
|||
|
(defun request--safe-call (function &rest arguments)
|
|||
|
(request--safe-apply function arguments))
|
|||
|
|
|||
|
;; (defun request--url-no-cache (url)
|
|||
|
;; "Imitate `cache=false' of `jQuery.ajax'.
|
|||
|
;; See: http://api.jquery.com/jQuery.ajax/"
|
|||
|
;; ;; FIXME: parse URL before adding ?_=TIME.
|
|||
|
;; (concat url (format-time-string "?_=%s")))
|
|||
|
|
|||
|
(defmacro request--document-function (function docstring)
|
|||
|
"Document FUNCTION with DOCSTRING. Use this for defstruct accessor etc."
|
|||
|
(declare (indent defun)
|
|||
|
(doc-string 2))
|
|||
|
`(put ',function 'function-documentation ,docstring))
|
|||
|
|
|||
|
(defun request--process-live-p (process)
|
|||
|
"Copied from `process-live-p' for backward compatibility (Emacs < 24).
|
|||
|
Adapted from lisp/subr.el.
|
|||
|
FSF holds the copyright of this function:
|
|||
|
Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2012
|
|||
|
Free Software Foundation, Inc."
|
|||
|
(memq (process-status process) '(run open listen connect stop)))
|
|||
|
|
|||
|
|
|||
|
;;; Logging
|
|||
|
|
|||
|
(defconst request--log-level-def
|
|||
|
'(;; debugging
|
|||
|
(blather . 60) (trace . 50) (debug . 40)
|
|||
|
;; information
|
|||
|
(verbose . 30) (info . 20)
|
|||
|
;; errors
|
|||
|
(warn . 10) (error . 0))
|
|||
|
"Named logging levels.")
|
|||
|
|
|||
|
(defun request--log-level-as-int (level)
|
|||
|
(if (integerp level)
|
|||
|
level
|
|||
|
(or (cdr (assq level request--log-level-def))
|
|||
|
0)))
|
|||
|
|
|||
|
(defvar request-log-buffer-name " *request-log*")
|
|||
|
|
|||
|
(defun request--log-buffer ()
|
|||
|
(get-buffer-create request-log-buffer-name))
|
|||
|
|
|||
|
(defmacro request-log (level fmt &rest args)
|
|||
|
(declare (indent 1))
|
|||
|
`(let ((level (request--log-level-as-int ,level))
|
|||
|
(log-level (request--log-level-as-int request-log-level))
|
|||
|
(msg-level (request--log-level-as-int request-message-level)))
|
|||
|
(when (<= level (max log-level msg-level))
|
|||
|
(let ((msg (format "[%s] %s" ,level
|
|||
|
(condition-case err
|
|||
|
(format ,fmt ,@args)
|
|||
|
(error (format "
|
|||
|
!!! Logging error while executing:
|
|||
|
%S
|
|||
|
!!! Error:
|
|||
|
%S"
|
|||
|
',args err))))))
|
|||
|
(when (<= level log-level)
|
|||
|
(with-current-buffer (request--log-buffer)
|
|||
|
(setq buffer-read-only t)
|
|||
|
(let ((inhibit-read-only t))
|
|||
|
(goto-char (point-max))
|
|||
|
(insert msg "\n"))))
|
|||
|
(when (<= level msg-level)
|
|||
|
(message "REQUEST %s" msg))))))
|
|||
|
|
|||
|
|
|||
|
;;; HTTP specific utilities
|
|||
|
|
|||
|
(defconst request--url-unreserved-chars
|
|||
|
'(?a ?b ?c ?d ?e ?f ?g ?h ?i ?j ?k ?l ?m ?n ?o ?p ?q ?r ?s ?t ?u ?v ?w ?x ?y ?z
|
|||
|
?A ?B ?C ?D ?E ?F ?G ?H ?I ?J ?K ?L ?M ?N ?O ?P ?Q ?R ?S ?T ?U ?V ?W ?X ?Y ?Z
|
|||
|
?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9
|
|||
|
?- ?_ ?. ?~)
|
|||
|
"`url-unreserved-chars' copied from Emacs 24.3 release candidate.
|
|||
|
This is used for making `request--urlencode-alist' RFC 3986 compliant
|
|||
|
for older Emacs versions.")
|
|||
|
|
|||
|
(defun request--urlencode-alist (alist)
|
|||
|
;; FIXME: make monkey patching `url-unreserved-chars' optional
|
|||
|
(let ((url-unreserved-chars request--url-unreserved-chars))
|
|||
|
(cl-loop for sep = "" then "&"
|
|||
|
for (k . v) in alist
|
|||
|
concat sep
|
|||
|
concat (url-hexify-string (format "%s" k))
|
|||
|
concat "="
|
|||
|
concat (url-hexify-string (format "%s" v)))))
|
|||
|
|
|||
|
|
|||
|
;;; Header parser
|
|||
|
|
|||
|
(defun request--parse-response-at-point ()
|
|||
|
"Parse the first header line such as \"HTTP/1.1 200 OK\"."
|
|||
|
(when (re-search-forward "\\=[ \t\n]*HTTP/\\([0-9\\.]+\\) +\\([0-9]+\\)" nil t)
|
|||
|
(list :version (match-string 1)
|
|||
|
:code (string-to-number (match-string 2)))))
|
|||
|
|
|||
|
(defun request--goto-next-body ()
|
|||
|
(re-search-forward "^\r\n"))
|
|||
|
|
|||
|
|
|||
|
;;; Response object
|
|||
|
|
|||
|
(cl-defstruct request-response
|
|||
|
"A structure holding all relevant information of a request."
|
|||
|
status-code history data error-thrown symbol-status url
|
|||
|
done-p settings
|
|||
|
;; internal variables
|
|||
|
-buffer -raw-header -timer -backend -tempfiles)
|
|||
|
|
|||
|
(defmacro request--document-response (function docstring)
|
|||
|
(declare (indent defun)
|
|||
|
(doc-string 2))
|
|||
|
`(request--document-function ,function ,(concat docstring "
|
|||
|
|
|||
|
.. This is an accessor for `request-response' object.
|
|||
|
|
|||
|
\(fn RESPONSE)")))
|
|||
|
|
|||
|
(request--document-response request-response-status-code
|
|||
|
"Integer HTTP response code (e.g., 200).")
|
|||
|
|
|||
|
(request--document-response request-response-history
|
|||
|
"Redirection history (a list of response object).
|
|||
|
The first element is the oldest redirection.
|
|||
|
|
|||
|
You can use restricted portion of functions for the response
|
|||
|
objects in the history slot. It also depends on backend. Here
|
|||
|
is the table showing what functions you can use for the response
|
|||
|
objects in the history slot.
|
|||
|
|
|||
|
==================================== ============== ==============
|
|||
|
Slots Backends
|
|||
|
------------------------------------ -----------------------------
|
|||
|
\\ curl url-retrieve
|
|||
|
==================================== ============== ==============
|
|||
|
request-response-url yes yes
|
|||
|
request-response-header yes no
|
|||
|
other functions no no
|
|||
|
==================================== ============== ==============
|
|||
|
")
|
|||
|
|
|||
|
(request--document-response request-response-data
|
|||
|
"Response parsed by the given parser.")
|
|||
|
|
|||
|
(request--document-response request-response-error-thrown
|
|||
|
"Error thrown during request.
|
|||
|
It takes the form of ``(ERROR-SYMBOL . DATA)``, which can be
|
|||
|
re-raised (`signal'ed) by ``(signal ERROR-SYMBOL DATA)``.")
|
|||
|
|
|||
|
(request--document-response request-response-symbol-status
|
|||
|
"A symbol representing the status of request (not HTTP response code).
|
|||
|
One of success/error/timeout/abort/parse-error.")
|
|||
|
|
|||
|
(request--document-response request-response-url
|
|||
|
"Final URL location of response.")
|
|||
|
|
|||
|
(request--document-response request-response-done-p
|
|||
|
"Return t when the request is finished or aborted.")
|
|||
|
|
|||
|
(request--document-response request-response-settings
|
|||
|
"Keyword arguments passed to `request' function.
|
|||
|
Some arguments such as HEADERS is changed to the one actually
|
|||
|
passed to the backend. Also, it has additional keywords such
|
|||
|
as URL which is the requested URL.")
|
|||
|
|
|||
|
(defun request-response-header (response field-name)
|
|||
|
"Fetch the values of RESPONSE header field named FIELD-NAME.
|
|||
|
|
|||
|
It returns comma separated values when the header has multiple
|
|||
|
field with the same name, as :RFC:`2616` specifies.
|
|||
|
|
|||
|
Examples::
|
|||
|
|
|||
|
(request-response-header response
|
|||
|
\"content-type\") ; => \"text/html; charset=utf-8\"
|
|||
|
(request-response-header response
|
|||
|
\"unknown-field\") ; => nil
|
|||
|
"
|
|||
|
(let ((raw-header (request-response--raw-header response)))
|
|||
|
(when raw-header
|
|||
|
(with-temp-buffer
|
|||
|
(erase-buffer)
|
|||
|
(insert raw-header)
|
|||
|
;; ALL=t to fetch all fields with the same name to get comma
|
|||
|
;; separated value [#rfc2616-sec4]_.
|
|||
|
(mail-fetch-field field-name nil t)))))
|
|||
|
;; .. [#rfc2616-sec4] RFC2616 says this is the right thing to do
|
|||
|
;; (see http://tools.ietf.org/html/rfc2616.html#section-4.2).
|
|||
|
;; Python's requests module does this too.
|
|||
|
|
|||
|
|
|||
|
;;; Backend dispatcher
|
|||
|
|
|||
|
(defconst request--backend-alist
|
|||
|
'((url-retrieve
|
|||
|
. ((request . request--url-retrieve)
|
|||
|
(request-sync . request--url-retrieve-sync)
|
|||
|
(terminate-process . delete-process)
|
|||
|
(get-cookies . request--url-retrieve-get-cookies)))
|
|||
|
(curl
|
|||
|
. ((request . request--curl)
|
|||
|
(request-sync . request--curl-sync)
|
|||
|
(terminate-process . interrupt-process)
|
|||
|
(get-cookies . request--curl-get-cookies))))
|
|||
|
"Map backend and method name to actual method (symbol).
|
|||
|
|
|||
|
It's alist of alist, of the following form::
|
|||
|
|
|||
|
((BACKEND . ((METHOD . FUNCTION) ...)) ...)
|
|||
|
|
|||
|
It would be nicer if I can use EIEIO. But as CEDET is included
|
|||
|
in Emacs by 23.2, using EIEIO means abandon older Emacs versions.
|
|||
|
It is probably necessary if I need to support more backends. But
|
|||
|
let's stick to manual dispatch for now.")
|
|||
|
;; See: (view-emacs-news "23.2")
|
|||
|
|
|||
|
(defun request--choose-backend (method)
|
|||
|
"Return `fucall'able object for METHOD of current `request-backend'."
|
|||
|
(assoc-default
|
|||
|
method
|
|||
|
(or (assoc-default request-backend request--backend-alist)
|
|||
|
(error "%S is not valid `request-backend'." request-backend))))
|
|||
|
|
|||
|
|
|||
|
;;; Cookie
|
|||
|
|
|||
|
(defun request-cookie-string (host &optional localpart secure)
|
|||
|
"Return cookie string (like `document.cookie').
|
|||
|
|
|||
|
Example::
|
|||
|
|
|||
|
(request-cookie-string \"127.0.0.1\" \"/\") ; => \"key=value; key2=value2\"
|
|||
|
"
|
|||
|
(mapconcat (lambda (nv) (concat (car nv) "=" (cdr nv)))
|
|||
|
(request-cookie-alist host localpart secure)
|
|||
|
"; "))
|
|||
|
|
|||
|
(defun request-cookie-alist (host &optional localpart secure)
|
|||
|
"Return cookies as an alist.
|
|||
|
|
|||
|
Example::
|
|||
|
|
|||
|
(request-cookie-alist \"127.0.0.1\" \"/\") ; => ((\"key\" . \"value\") ...)
|
|||
|
"
|
|||
|
(funcall (request--choose-backend 'get-cookies) host localpart secure))
|
|||
|
|
|||
|
|
|||
|
;;; Main
|
|||
|
|
|||
|
(cl-defun request-default-error-callback (url &key symbol-status
|
|||
|
&allow-other-keys)
|
|||
|
(request-log 'error
|
|||
|
"Error (%s) while connecting to %s." symbol-status url))
|
|||
|
|
|||
|
(cl-defun request (url &rest settings
|
|||
|
&key
|
|||
|
(type "GET")
|
|||
|
(params nil)
|
|||
|
(data nil)
|
|||
|
(files nil)
|
|||
|
(parser nil)
|
|||
|
(headers nil)
|
|||
|
(success nil)
|
|||
|
(error nil)
|
|||
|
(complete nil)
|
|||
|
(timeout request-timeout)
|
|||
|
(status-code nil)
|
|||
|
(sync nil)
|
|||
|
(response (make-request-response))
|
|||
|
(unix-socket nil))
|
|||
|
"Send request to URL.
|
|||
|
|
|||
|
Request.el has a single entry point. It is `request'.
|
|||
|
|
|||
|
==================== ========================================================
|
|||
|
Keyword argument Explanation
|
|||
|
==================== ========================================================
|
|||
|
TYPE (string) type of request to make: POST/GET/PUT/DELETE
|
|||
|
PARAMS (alist) set \"?key=val\" part in URL
|
|||
|
DATA (string/alist) data to be sent to the server
|
|||
|
FILES (alist) files to be sent to the server (see below)
|
|||
|
PARSER (symbol) a function that reads current buffer and return data
|
|||
|
HEADERS (alist) additional headers to send with the request
|
|||
|
SUCCESS (function) called on success
|
|||
|
ERROR (function) called on error
|
|||
|
COMPLETE (function) called on both success and error
|
|||
|
TIMEOUT (number) timeout in second
|
|||
|
STATUS-CODE (alist) map status code (int) to callback
|
|||
|
SYNC (bool) If `t', wait until request is done. Default is `nil'.
|
|||
|
==================== ========================================================
|
|||
|
|
|||
|
|
|||
|
* Callback functions
|
|||
|
|
|||
|
Callback functions STATUS, ERROR, COMPLETE and `cdr's in element of
|
|||
|
the alist STATUS-CODE take same keyword arguments listed below. For
|
|||
|
forward compatibility, these functions must ignore unused keyword
|
|||
|
arguments (i.e., it's better to use `&allow-other-keys' [#]_).::
|
|||
|
|
|||
|
(CALLBACK ; SUCCESS/ERROR/COMPLETE/STATUS-CODE
|
|||
|
:data data ; whatever PARSER function returns, or nil
|
|||
|
:error-thrown error-thrown ; (ERROR-SYMBOL . DATA), or nil
|
|||
|
:symbol-status symbol-status ; success/error/timeout/abort/parse-error
|
|||
|
:response response ; request-response object
|
|||
|
...)
|
|||
|
|
|||
|
.. [#] `&allow-other-keys' is a special \"markers\" available in macros
|
|||
|
in the CL library for function definition such as `cl-defun' and
|
|||
|
`cl-function'. Without this marker, you need to specify all arguments
|
|||
|
to be passed. This becomes problem when request.el adds new arguments
|
|||
|
when calling callback functions. If you use `&allow-other-keys'
|
|||
|
(or manually ignore other arguments), your code is free from this
|
|||
|
problem. See info node `(cl) Argument Lists' for more information.
|
|||
|
|
|||
|
Arguments data, error-thrown, symbol-status can be accessed by
|
|||
|
`request-response-data', `request-response-error-thrown',
|
|||
|
`request-response-symbol-status' accessors, i.e.::
|
|||
|
|
|||
|
(request-response-data RESPONSE) ; same as data
|
|||
|
|
|||
|
Response object holds other information which can be accessed by
|
|||
|
the following accessors:
|
|||
|
`request-response-status-code',
|
|||
|
`request-response-url' and
|
|||
|
`request-response-settings'
|
|||
|
|
|||
|
* STATUS-CODE callback
|
|||
|
|
|||
|
STATUS-CODE is an alist of the following format::
|
|||
|
|
|||
|
((N-1 . CALLBACK-1)
|
|||
|
(N-2 . CALLBACK-2)
|
|||
|
...)
|
|||
|
|
|||
|
Here, N-1, N-2,... are integer status codes such as 200.
|
|||
|
|
|||
|
|
|||
|
* FILES
|
|||
|
|
|||
|
FILES is an alist of the following format::
|
|||
|
|
|||
|
((NAME-1 . FILE-1)
|
|||
|
(NAME-2 . FILE-2)
|
|||
|
...)
|
|||
|
|
|||
|
where FILE-N is a list of the form::
|
|||
|
|
|||
|
(FILENAME &key PATH BUFFER STRING MIME-TYPE)
|
|||
|
|
|||
|
FILE-N can also be a string (path to the file) or a buffer object.
|
|||
|
In that case, FILENAME is set to the file name or buffer name.
|
|||
|
|
|||
|
Example FILES argument::
|
|||
|
|
|||
|
`((\"passwd\" . \"/etc/passwd\") ; filename = passwd
|
|||
|
(\"scratch\" . ,(get-buffer \"*scratch*\")) ; filename = *scratch*
|
|||
|
(\"passwd2\" . (\"password.txt\" :file \"/etc/passwd\"))
|
|||
|
(\"scratch2\" . (\"scratch.txt\" :buffer ,(get-buffer \"*scratch*\")))
|
|||
|
(\"data\" . (\"data.csv\" :data \"1,2,3\\n4,5,6\\n\")))
|
|||
|
|
|||
|
.. note:: FILES is implemented only for curl backend for now.
|
|||
|
As furl.el_ supports multipart POST, it should be possible to
|
|||
|
support FILES in pure elisp by making furl.el_ another backend.
|
|||
|
Contributions are welcome.
|
|||
|
|
|||
|
.. _furl.el: http://code.google.com/p/furl-el/
|
|||
|
|
|||
|
|
|||
|
* PARSER function
|
|||
|
|
|||
|
PARSER function takes no argument and it is executed in the
|
|||
|
buffer with HTTP response body. The current position in the HTTP
|
|||
|
response buffer is at the beginning of the buffer. As the HTTP
|
|||
|
header is stripped off, the cursor is actually at the beginning
|
|||
|
of the response body. So, for example, you can pass `json-read'
|
|||
|
to parse JSON object in the buffer. To fetch whole response as a
|
|||
|
string, pass `buffer-string'.
|
|||
|
|
|||
|
When using `json-read', it is useful to know that the returned
|
|||
|
type can be modified by `json-object-type', `json-array-type',
|
|||
|
`json-key-type', `json-false' and `json-null'. See docstring of
|
|||
|
each function for what it does. For example, to convert JSON
|
|||
|
objects to plist instead of alist, wrap `json-read' by `lambda'
|
|||
|
like this.::
|
|||
|
|
|||
|
(request
|
|||
|
\"http://...\"
|
|||
|
:parser (lambda ()
|
|||
|
(let ((json-object-type 'plist))
|
|||
|
(json-read)))
|
|||
|
...)
|
|||
|
|
|||
|
This is analogous to the `dataType' argument of jQuery.ajax_.
|
|||
|
Only this function can access to the process buffer, which
|
|||
|
is killed immediately after the execution of this function.
|
|||
|
|
|||
|
* SYNC
|
|||
|
|
|||
|
Synchronous request is functional, but *please* don't use it
|
|||
|
other than testing or debugging. Emacs users have better things
|
|||
|
to do rather than waiting for HTTP request. If you want a better
|
|||
|
way to write callback chains, use `request-deferred'.
|
|||
|
|
|||
|
If you can't avoid using it (e.g., you are inside of some hook
|
|||
|
which must return some value), make sure to set TIMEOUT to
|
|||
|
relatively small value.
|
|||
|
|
|||
|
Due to limitation of `url-retrieve-synchronously', response slots
|
|||
|
`request-response-error-thrown', `request-response-history' and
|
|||
|
`request-response-url' are unknown (always `nil') when using
|
|||
|
synchronous request with `url-retrieve' backend.
|
|||
|
|
|||
|
* Note
|
|||
|
|
|||
|
API of `request' is somewhat mixture of jQuery.ajax_ (Javascript)
|
|||
|
and requests.request_ (Python).
|
|||
|
|
|||
|
.. _jQuery.ajax: http://api.jquery.com/jQuery.ajax/
|
|||
|
.. _requests.request: http://docs.python-requests.org
|
|||
|
"
|
|||
|
(request-log 'debug "REQUEST")
|
|||
|
;; FIXME: support CACHE argument (if possible)
|
|||
|
;; (unless cache
|
|||
|
;; (setq url (request--url-no-cache url)))
|
|||
|
(unless error
|
|||
|
(setq error (apply-partially #'request-default-error-callback url))
|
|||
|
(setq settings (plist-put settings :error error)))
|
|||
|
(unless (or (stringp data)
|
|||
|
(null data)
|
|||
|
(assoc-string "Content-Type" headers t))
|
|||
|
(setq data (request--urlencode-alist data))
|
|||
|
(setq settings (plist-put settings :data data)))
|
|||
|
(when params
|
|||
|
(cl-assert (listp params) nil "PARAMS must be an alist. Given: %S" params)
|
|||
|
(setq url (concat url (if (string-match-p "\\?" url) "&" "?")
|
|||
|
(request--urlencode-alist params))))
|
|||
|
(setq settings (plist-put settings :url url))
|
|||
|
(setq settings (plist-put settings :response response))
|
|||
|
(setf (request-response-settings response) settings)
|
|||
|
(setf (request-response-url response) url)
|
|||
|
(setf (request-response--backend response) request-backend)
|
|||
|
;; Call `request--url-retrieve'(`-sync') or `request--curl'(`-sync').
|
|||
|
(apply (if sync
|
|||
|
(request--choose-backend 'request-sync)
|
|||
|
(request--choose-backend 'request))
|
|||
|
url settings)
|
|||
|
(when timeout
|
|||
|
(request-log 'debug "Start timer: timeout=%s sec" timeout)
|
|||
|
(setf (request-response--timer response)
|
|||
|
(run-at-time timeout nil
|
|||
|
#'request-response--timeout-callback response)))
|
|||
|
response)
|
|||
|
|
|||
|
(defun request--clean-header (response)
|
|||
|
"Strip off carriage returns in the header of REQUEST."
|
|||
|
(request-log 'debug "-CLEAN-HEADER")
|
|||
|
(let ((buffer (request-response--buffer response))
|
|||
|
(backend (request-response--backend response))
|
|||
|
sep-regexp)
|
|||
|
(if (eq backend 'url-retrieve)
|
|||
|
;; FIXME: make this workaround optional.
|
|||
|
;; But it looks like sometimes `url-http-clean-headers'
|
|||
|
;; fails to cleanup. So, let's be bit permissive here...
|
|||
|
(setq sep-regexp "^\r?$")
|
|||
|
(setq sep-regexp "^\r$"))
|
|||
|
(when (buffer-live-p buffer)
|
|||
|
(with-current-buffer buffer
|
|||
|
(request-log 'trace
|
|||
|
"(buffer-string) at %S =\n%s" buffer (buffer-string))
|
|||
|
(goto-char (point-min))
|
|||
|
(when (and (re-search-forward sep-regexp nil t)
|
|||
|
;; Are \r characters stripped off already?:
|
|||
|
(not (equal (match-string 0) "")))
|
|||
|
(while (re-search-backward "\r$" (point-min) t)
|
|||
|
(replace-match "")))))))
|
|||
|
|
|||
|
(defun request--cut-header (response)
|
|||
|
"Cut the first header part in the buffer of RESPONSE and move it to
|
|||
|
raw-header slot."
|
|||
|
(request-log 'debug "-CUT-HEADER")
|
|||
|
(let ((buffer (request-response--buffer response)))
|
|||
|
(when (buffer-live-p buffer)
|
|||
|
(with-current-buffer buffer
|
|||
|
(goto-char (point-min))
|
|||
|
(when (re-search-forward "^$" nil t)
|
|||
|
(setf (request-response--raw-header response)
|
|||
|
(buffer-substring (point-min) (point)))
|
|||
|
(delete-region (point-min) (min (1+ (point)) (point-max))))))))
|
|||
|
|
|||
|
(defun request-untrampify-filename (file)
|
|||
|
"Return FILE as the local file name."
|
|||
|
(or (file-remote-p file 'localname) file))
|
|||
|
|
|||
|
(defun request--parse-data (response parser)
|
|||
|
"Run PARSER in current buffer if ERROR-THROWN is nil,
|
|||
|
then kill the current buffer."
|
|||
|
(request-log 'debug "-PARSE-DATA")
|
|||
|
(let ((buffer (request-response--buffer response)))
|
|||
|
(request-log 'debug "parser = %s" parser)
|
|||
|
(when (and (buffer-live-p buffer) parser)
|
|||
|
(with-current-buffer buffer
|
|||
|
(request-log 'trace
|
|||
|
"(buffer-string) at %S =\n%s" buffer (buffer-string))
|
|||
|
(when (/= (request-response-status-code response) 204)
|
|||
|
(goto-char (point-min))
|
|||
|
(setf (request-response-data response) (funcall parser)))))))
|
|||
|
|
|||
|
(cl-defun request--callback (buffer &key parser success error complete
|
|||
|
timeout status-code response
|
|||
|
&allow-other-keys)
|
|||
|
(request-log 'debug "REQUEST--CALLBACK")
|
|||
|
(request-log 'debug "(buffer-string) =\n%s"
|
|||
|
(when (buffer-live-p buffer)
|
|||
|
(with-current-buffer buffer (buffer-string))))
|
|||
|
|
|||
|
;; Sometimes BUFFER given as the argument is different from the
|
|||
|
;; buffer already set in RESPONSE. That's why it is reset here.
|
|||
|
;; FIXME: Refactor how BUFFER is passed around.
|
|||
|
(setf (request-response--buffer response) buffer)
|
|||
|
(request-response--cancel-timer response)
|
|||
|
(cl-symbol-macrolet
|
|||
|
((error-thrown (request-response-error-thrown response))
|
|||
|
(symbol-status (request-response-symbol-status response))
|
|||
|
(data (request-response-data response))
|
|||
|
(done-p (request-response-done-p response)))
|
|||
|
|
|||
|
;; Parse response header
|
|||
|
(request--clean-header response)
|
|||
|
(request--cut-header response)
|
|||
|
;; Note: Try to do this even `error-thrown' is set. For example,
|
|||
|
;; timeout error can occur while downloading response body and
|
|||
|
;; header is there in that case.
|
|||
|
|
|||
|
;; Parse response body
|
|||
|
(request-log 'debug "error-thrown = %S" error-thrown)
|
|||
|
(condition-case err
|
|||
|
(request--parse-data response parser)
|
|||
|
(error
|
|||
|
;; If there was already an error (e.g. server timeout) do not set the
|
|||
|
;; status to `parse-error'.
|
|||
|
(unless error-thrown
|
|||
|
(setq symbol-status 'parse-error)
|
|||
|
(setq error-thrown err)
|
|||
|
(request-log 'error "Error from parser %S: %S" parser err))))
|
|||
|
(kill-buffer buffer)
|
|||
|
(request-log 'debug "data = %s" data)
|
|||
|
|
|||
|
;; Determine `symbol-status'
|
|||
|
(unless symbol-status
|
|||
|
(setq symbol-status (if error-thrown 'error 'success)))
|
|||
|
(request-log 'debug "symbol-status = %s" symbol-status)
|
|||
|
|
|||
|
;; Call callbacks
|
|||
|
(let ((args (list :data data
|
|||
|
:symbol-status symbol-status
|
|||
|
:error-thrown error-thrown
|
|||
|
:response response)))
|
|||
|
(let* ((success-p (eq symbol-status 'success))
|
|||
|
(cb (if success-p success error))
|
|||
|
(name (if success-p "success" "error")))
|
|||
|
(when cb
|
|||
|
(request-log 'debug "Executing %s callback." name)
|
|||
|
(request--safe-apply cb args)))
|
|||
|
|
|||
|
(let ((cb (cdr (assq (request-response-status-code response)
|
|||
|
status-code))))
|
|||
|
(when cb
|
|||
|
(request-log 'debug "Executing status-code callback.")
|
|||
|
(request--safe-apply cb args)))
|
|||
|
|
|||
|
(when complete
|
|||
|
(request-log 'debug "Executing complete callback.")
|
|||
|
(request--safe-apply complete args)))
|
|||
|
|
|||
|
(setq done-p t)
|
|||
|
|
|||
|
;; Remove temporary files
|
|||
|
;; FIXME: Make tempfile cleanup more reliable. It is possible
|
|||
|
;; callback is never called.
|
|||
|
(request--safe-delete-files (request-response--tempfiles response))))
|
|||
|
|
|||
|
(cl-defun request-response--timeout-callback (response)
|
|||
|
(request-log 'debug "-TIMEOUT-CALLBACK")
|
|||
|
(setf (request-response-symbol-status response) 'timeout)
|
|||
|
(setf (request-response-error-thrown response) '(error . ("Timeout")))
|
|||
|
(let* ((buffer (request-response--buffer response))
|
|||
|
(proc (and (buffer-live-p buffer) (get-buffer-process buffer))))
|
|||
|
(when proc
|
|||
|
;; This will call `request--callback':
|
|||
|
(funcall (request--choose-backend 'terminate-process) proc))
|
|||
|
|
|||
|
(cl-symbol-macrolet ((done-p (request-response-done-p response)))
|
|||
|
(unless done-p
|
|||
|
;; This code should never be executed. However, it occurs
|
|||
|
;; sometimes with `url-retrieve' backend.
|
|||
|
;; FIXME: In Emacs 24.3.50 or later, this is always executed in
|
|||
|
;; request-get-timeout test. Find out if it is fine.
|
|||
|
(request-log 'error "Callback is not called when stopping process! \
|
|||
|
Explicitly calling from timer.")
|
|||
|
(when (buffer-live-p buffer)
|
|||
|
(cl-destructuring-bind (&key code &allow-other-keys)
|
|||
|
(with-current-buffer buffer
|
|||
|
(goto-char (point-min))
|
|||
|
(request--parse-response-at-point))
|
|||
|
(setf (request-response-status-code response) code)))
|
|||
|
(apply #'request--callback
|
|||
|
buffer
|
|||
|
(request-response-settings response))
|
|||
|
(setq done-p t)))))
|
|||
|
|
|||
|
(defun request-response--cancel-timer (response)
|
|||
|
(request-log 'debug "REQUEST-RESPONSE--CANCEL-TIMER")
|
|||
|
(cl-symbol-macrolet ((timer (request-response--timer response)))
|
|||
|
(when timer
|
|||
|
(cancel-timer timer)
|
|||
|
(setq timer nil))))
|
|||
|
|
|||
|
|
|||
|
(defun request-abort (response)
|
|||
|
"Abort request for RESPONSE (the object returned by `request').
|
|||
|
Note that this function invoke ERROR and COMPLETE callbacks.
|
|||
|
Callbacks may not be called immediately but called later when
|
|||
|
associated process is exited."
|
|||
|
(cl-symbol-macrolet ((buffer (request-response--buffer response))
|
|||
|
(symbol-status (request-response-symbol-status response))
|
|||
|
(done-p (request-response-done-p response)))
|
|||
|
(let ((process (get-buffer-process buffer)))
|
|||
|
(unless symbol-status ; should I use done-p here?
|
|||
|
(setq symbol-status 'abort)
|
|||
|
(setq done-p t)
|
|||
|
(when (and
|
|||
|
(processp process) ; process can be nil when buffer is killed
|
|||
|
(request--process-live-p process))
|
|||
|
(funcall (request--choose-backend 'terminate-process) process))))))
|
|||
|
|
|||
|
|
|||
|
;;; Backend: `url-retrieve'
|
|||
|
|
|||
|
(cl-defun request--url-retrieve-preprocess-settings
|
|||
|
(&rest settings &key type data files headers &allow-other-keys)
|
|||
|
(when files
|
|||
|
(error "`url-retrieve' backend does not support FILES."))
|
|||
|
(when (and (equal type "POST")
|
|||
|
data
|
|||
|
(not (assoc-string "Content-Type" headers t)))
|
|||
|
(push '("Content-Type" . "application/x-www-form-urlencoded") headers)
|
|||
|
(setq settings (plist-put settings :headers headers)))
|
|||
|
settings)
|
|||
|
|
|||
|
(cl-defun request--url-retrieve (url &rest settings
|
|||
|
&key type data timeout response
|
|||
|
&allow-other-keys
|
|||
|
&aux headers)
|
|||
|
(setq settings (apply #'request--url-retrieve-preprocess-settings settings))
|
|||
|
(setq headers (plist-get settings :headers))
|
|||
|
(let* ((url-request-extra-headers headers)
|
|||
|
(url-request-method type)
|
|||
|
(url-request-data data)
|
|||
|
(buffer (url-retrieve url #'request--url-retrieve-callback
|
|||
|
(nconc (list :response response) settings)))
|
|||
|
(proc (get-buffer-process buffer)))
|
|||
|
(setf (request-response--buffer response) buffer)
|
|||
|
(process-put proc :request-response response)
|
|||
|
(request-log 'debug "Start querying: %s" url)
|
|||
|
(set-process-query-on-exit-flag proc nil)))
|
|||
|
|
|||
|
(cl-defun request--url-retrieve-callback (status &rest settings
|
|||
|
&key response url
|
|||
|
&allow-other-keys)
|
|||
|
(declare (special url-http-method
|
|||
|
url-http-response-status))
|
|||
|
(request-log 'debug "-URL-RETRIEVE-CALLBACK")
|
|||
|
(request-log 'debug "status = %S" status)
|
|||
|
(request-log 'debug "url-http-method = %s" url-http-method)
|
|||
|
(request-log 'debug "url-http-response-status = %s" url-http-response-status)
|
|||
|
|
|||
|
(setf (request-response-status-code response) url-http-response-status)
|
|||
|
(let ((redirect (plist-get status :redirect)))
|
|||
|
(when redirect
|
|||
|
(setf (request-response-url response) redirect)))
|
|||
|
;; Construct history slot
|
|||
|
(cl-loop for v in
|
|||
|
(cl-loop with first = t
|
|||
|
with l = nil
|
|||
|
for (k v) on status by 'cddr
|
|||
|
when (eq k :redirect)
|
|||
|
if first
|
|||
|
do (setq first nil)
|
|||
|
else
|
|||
|
do (push v l)
|
|||
|
finally do (cons url l))
|
|||
|
do (let ((r (make-request-response :-backend 'url-retrieve)))
|
|||
|
(setf (request-response-url r) v)
|
|||
|
(push r (request-response-history response))))
|
|||
|
|
|||
|
(cl-symbol-macrolet ((error-thrown (request-response-error-thrown response))
|
|||
|
(status-error (plist-get status :error)))
|
|||
|
(when (and error-thrown status-error)
|
|||
|
(request-log 'warn
|
|||
|
"Error %S thrown already but got another error %S from \
|
|||
|
`url-retrieve'. Ignoring it..." error-thrown status-error))
|
|||
|
(unless error-thrown
|
|||
|
(setq error-thrown status-error)))
|
|||
|
|
|||
|
(apply #'request--callback (current-buffer) settings))
|
|||
|
|
|||
|
(cl-defun request--url-retrieve-sync (url &rest settings
|
|||
|
&key type data timeout response
|
|||
|
&allow-other-keys
|
|||
|
&aux headers)
|
|||
|
(setq settings (apply #'request--url-retrieve-preprocess-settings settings))
|
|||
|
(setq headers (plist-get settings :headers))
|
|||
|
(let* ((url-request-extra-headers headers)
|
|||
|
(url-request-method type)
|
|||
|
(url-request-data data)
|
|||
|
(buffer (if timeout
|
|||
|
(with-timeout
|
|||
|
(timeout
|
|||
|
(setf (request-response-symbol-status response)
|
|||
|
'timeout)
|
|||
|
(setf (request-response-done-p response) t)
|
|||
|
nil)
|
|||
|
(url-retrieve-synchronously url))
|
|||
|
(url-retrieve-synchronously url))))
|
|||
|
(setf (request-response--buffer response) buffer)
|
|||
|
;; It seems there is no way to get redirects and URL here...
|
|||
|
(when buffer
|
|||
|
;; Fetch HTTP response code
|
|||
|
(with-current-buffer buffer
|
|||
|
(goto-char (point-min))
|
|||
|
(cl-destructuring-bind (&key version code)
|
|||
|
(request--parse-response-at-point)
|
|||
|
(setf (request-response-status-code response) code)))
|
|||
|
;; Parse response body, etc.
|
|||
|
(apply #'request--callback buffer settings)))
|
|||
|
response)
|
|||
|
|
|||
|
(defun request--url-retrieve-get-cookies (host localpart secure)
|
|||
|
(mapcar
|
|||
|
(lambda (c) (cons (url-cookie-name c) (url-cookie-value c)))
|
|||
|
(url-cookie-retrieve host localpart secure)))
|
|||
|
|
|||
|
|
|||
|
;;; Backend: curl
|
|||
|
|
|||
|
(defvar request--curl-cookie-jar nil
|
|||
|
"Override what the function `request--curl-cookie-jar' returns.
|
|||
|
Currently it is used only for testing.")
|
|||
|
|
|||
|
(defun request--curl-cookie-jar ()
|
|||
|
"Cookie storage for curl backend."
|
|||
|
(or request--curl-cookie-jar
|
|||
|
(expand-file-name "curl-cookie-jar" request-storage-directory)))
|
|||
|
|
|||
|
(defconst request--curl-write-out-template
|
|||
|
(if (eq system-type 'windows-nt)
|
|||
|
"\\n(:num-redirects %{num_redirects} :url-effective %{url_effective})"
|
|||
|
"\\n(:num-redirects %{num_redirects} :url-effective \"%{url_effective}\")"))
|
|||
|
|
|||
|
(defun request--curl-mkdir-for-cookie-jar ()
|
|||
|
(ignore-errors
|
|||
|
(make-directory (file-name-directory (request--curl-cookie-jar)) t)))
|
|||
|
|
|||
|
(cl-defun request--curl-command
|
|||
|
(url &key type data headers timeout response files* unix-socket
|
|||
|
&allow-other-keys
|
|||
|
&aux
|
|||
|
(cookie-jar (convert-standard-filename
|
|||
|
(expand-file-name (request--curl-cookie-jar)))))
|
|||
|
(append
|
|||
|
(list request-curl "--silent" "--include"
|
|||
|
"--location"
|
|||
|
;; FIXME: test automatic decompression
|
|||
|
"--compressed"
|
|||
|
;; FIMXE: this way of using cookie might be problem when
|
|||
|
;; running multiple requests.
|
|||
|
"--cookie" cookie-jar "--cookie-jar" cookie-jar
|
|||
|
"--write-out" request--curl-write-out-template)
|
|||
|
request-curl-options
|
|||
|
(when unix-socket (list "--unix-socket" unix-socket))
|
|||
|
(cl-loop for (name filename path mime-type) in files*
|
|||
|
collect "--form"
|
|||
|
collect (format "%s=@%s;filename=%s%s" name
|
|||
|
(request-untrampify-filename path) filename
|
|||
|
(if mime-type
|
|||
|
(format ";type=%s" mime-type)
|
|||
|
"")))
|
|||
|
(when data
|
|||
|
(let ((tempfile (request--make-temp-file)))
|
|||
|
(push tempfile (request-response--tempfiles response))
|
|||
|
(let ((file-coding-system-alist nil)
|
|||
|
(coding-system-for-write 'binary))
|
|||
|
(with-temp-file tempfile
|
|||
|
(setq buffer-file-coding-system 'binary)
|
|||
|
(set-buffer-multibyte nil)
|
|||
|
(insert data)))
|
|||
|
(list "--data-binary" (concat "@" (request-untrampify-filename tempfile)))))
|
|||
|
(when type (list "--request" type))
|
|||
|
(cl-loop for (k . v) in headers
|
|||
|
collect "--header"
|
|||
|
collect (format "%s: %s" k v))
|
|||
|
(list url)))
|
|||
|
|
|||
|
(defun request--curl-normalize-files-1 (files get-temp-file)
|
|||
|
(cl-loop for (name . item) in files
|
|||
|
collect
|
|||
|
(cl-destructuring-bind
|
|||
|
(filename &key file buffer data mime-type)
|
|||
|
(cond
|
|||
|
((stringp item) (list (file-name-nondirectory item) :file item))
|
|||
|
((bufferp item) (list (buffer-name item) :buffer item))
|
|||
|
(t item))
|
|||
|
(unless (= (cl-loop for v in (list file buffer data) if v sum 1) 1)
|
|||
|
(error "Only one of :file/:buffer/:data must be given. Got: %S"
|
|||
|
(cons name item)))
|
|||
|
(cond
|
|||
|
(file
|
|||
|
(list name filename file mime-type))
|
|||
|
(buffer
|
|||
|
(let ((tf (funcall get-temp-file)))
|
|||
|
(with-current-buffer buffer
|
|||
|
(write-region (point-min) (point-max) tf nil 'silent))
|
|||
|
(list name filename tf mime-type)))
|
|||
|
(data
|
|||
|
(let ((tf (funcall get-temp-file)))
|
|||
|
(with-temp-buffer
|
|||
|
(erase-buffer)
|
|||
|
(insert data)
|
|||
|
(write-region (point-min) (point-max) tf nil 'silent))
|
|||
|
(list name filename tf mime-type)))))))
|
|||
|
|
|||
|
(defun request--make-temp-file ()
|
|||
|
"Create a temporary file."
|
|||
|
(if (file-remote-p default-directory)
|
|||
|
(let ((tramp-temp-name-prefix request-temp-prefix)
|
|||
|
(vec (tramp-dissect-file-name default-directory)))
|
|||
|
(tramp-make-tramp-file-name
|
|||
|
(tramp-file-name-method vec)
|
|||
|
(tramp-file-name-user vec)
|
|||
|
(tramp-file-name-host vec)
|
|||
|
(tramp-make-tramp-temp-file vec)
|
|||
|
(tramp-file-name-hop vec)))
|
|||
|
(make-temp-file request-temp-prefix)))
|
|||
|
|
|||
|
(defun request--curl-normalize-files (files)
|
|||
|
"Change FILES into a list of (NAME FILENAME PATH MIME-TYPE).
|
|||
|
This is to make `request--curl-command' cleaner by converting
|
|||
|
FILES to a homogeneous list. It returns a list (FILES* TEMPFILES)
|
|||
|
where FILES* is a converted FILES and TEMPFILES is a list of
|
|||
|
temporary file paths."
|
|||
|
(let (tempfiles noerror)
|
|||
|
(unwind-protect
|
|||
|
(let* ((get-temp-file (lambda ()
|
|||
|
(let ((tf (request--make-temp-file)))
|
|||
|
(push tf tempfiles)
|
|||
|
tf)))
|
|||
|
(files* (request--curl-normalize-files-1 files get-temp-file)))
|
|||
|
(setq noerror t)
|
|||
|
(list files* tempfiles))
|
|||
|
(unless noerror
|
|||
|
;; Remove temporary files only when an error occurs
|
|||
|
(request--safe-delete-files tempfiles)))))
|
|||
|
|
|||
|
(defun request--safe-delete-files (files)
|
|||
|
"Remove FILES but do not raise error when failed to do so."
|
|||
|
(mapc (lambda (f) (condition-case err
|
|||
|
(delete-file f)
|
|||
|
(error (request-log 'error
|
|||
|
"Failed delete file %s. Got: %S" f err))))
|
|||
|
files))
|
|||
|
|
|||
|
(cl-defun request--curl (url &rest settings
|
|||
|
&key type data files headers timeout response
|
|||
|
&allow-other-keys)
|
|||
|
"cURL-based request backend.
|
|||
|
|
|||
|
Redirection handling strategy
|
|||
|
-----------------------------
|
|||
|
|
|||
|
curl follows redirection when --location is given. However,
|
|||
|
all headers are printed when it is used with --include option.
|
|||
|
Number of redirects is printed out sexp-based message using
|
|||
|
--write-out option (see `request--curl-write-out-template').
|
|||
|
This number is used for removing extra headers and parse
|
|||
|
location header from the last redirection header.
|
|||
|
|
|||
|
Sexp at the end of buffer and extra headers for redirects are
|
|||
|
removed from the buffer before it is shown to the parser function.
|
|||
|
"
|
|||
|
(request--curl-mkdir-for-cookie-jar)
|
|||
|
(let* (;; Use pipe instead of pty. Otherwise, curl process hangs.
|
|||
|
(process-connection-type nil)
|
|||
|
;; Avoid starting program in non-existing directory.
|
|||
|
(home-directory (if (file-remote-p default-directory)
|
|||
|
(let ((vec (tramp-dissect-file-name default-directory)))
|
|||
|
(tramp-make-tramp-file-name
|
|||
|
(tramp-file-name-method vec)
|
|||
|
(tramp-file-name-user vec)
|
|||
|
(tramp-file-name-host vec)
|
|||
|
"~/"
|
|||
|
(tramp-file-name-hop vec)))
|
|||
|
"~/"))
|
|||
|
(default-directory (expand-file-name home-directory))
|
|||
|
(buffer (generate-new-buffer " *request curl*"))
|
|||
|
(command (cl-destructuring-bind
|
|||
|
(files* tempfiles)
|
|||
|
(request--curl-normalize-files files)
|
|||
|
(setf (request-response--tempfiles response) tempfiles)
|
|||
|
(apply #'request--curl-command url :files* files*
|
|||
|
:response response settings)))
|
|||
|
(proc (apply #'start-file-process "request curl" buffer command)))
|
|||
|
(request-log 'debug "Run: %s" (mapconcat 'identity command " "))
|
|||
|
(setf (request-response--buffer response) buffer)
|
|||
|
(process-put proc :request-response response)
|
|||
|
(set-process-coding-system proc 'binary 'binary)
|
|||
|
(set-process-query-on-exit-flag proc nil)
|
|||
|
(set-process-sentinel proc #'request--curl-callback)))
|
|||
|
|
|||
|
(defun request--curl-read-and-delete-tail-info ()
|
|||
|
"Read a sexp at the end of buffer and remove it and preceding character.
|
|||
|
This function moves the point at the end of buffer by side effect.
|
|||
|
See also `request--curl-write-out-template'."
|
|||
|
(let (forward-sexp-function)
|
|||
|
(goto-char (point-max))
|
|||
|
(forward-sexp -1)
|
|||
|
(let ((beg (1- (point))))
|
|||
|
(prog1
|
|||
|
(read (current-buffer))
|
|||
|
(delete-region beg (point-max))))))
|
|||
|
|
|||
|
(defconst request--cookie-reserved-re
|
|||
|
(mapconcat
|
|||
|
(lambda (x) (concat "\\(^" x "\\'\\)"))
|
|||
|
'("comment" "commenturl" "discard" "domain" "max-age" "path" "port"
|
|||
|
"secure" "version" "expires")
|
|||
|
"\\|")
|
|||
|
"Uninterested keys in cookie.
|
|||
|
See \"set-cookie-av\" in http://www.ietf.org/rfc/rfc2965.txt")
|
|||
|
|
|||
|
(defun request--consume-100-continue ()
|
|||
|
"Remove \"HTTP/* 100 Continue\" header at the point."
|
|||
|
(cl-destructuring-bind (&key code &allow-other-keys)
|
|||
|
(save-excursion (request--parse-response-at-point))
|
|||
|
(when (equal code 100)
|
|||
|
(delete-region (point) (progn (request--goto-next-body) (point)))
|
|||
|
;; FIXME: Does this make sense? Is it possible to have multiple 100?
|
|||
|
(request--consume-100-continue))))
|
|||
|
|
|||
|
(defun request--consume-200-connection-established ()
|
|||
|
"Remove \"HTTP/* 200 Connection established\" header at the point."
|
|||
|
(when (looking-at-p "HTTP/1\\.[0-1] 200 Connection established")
|
|||
|
(delete-region (point) (progn (request--goto-next-body) (point)))))
|
|||
|
|
|||
|
(defun request--curl-preprocess ()
|
|||
|
"Pre-process current buffer before showing it to user."
|
|||
|
(let (history)
|
|||
|
(cl-destructuring-bind (&key num-redirects url-effective)
|
|||
|
(request--curl-read-and-delete-tail-info)
|
|||
|
(goto-char (point-min))
|
|||
|
(request--consume-100-continue)
|
|||
|
(request--consume-200-connection-established)
|
|||
|
(when (> num-redirects 0)
|
|||
|
(cl-loop with case-fold-search = t
|
|||
|
repeat num-redirects
|
|||
|
;; Do not store code=100 headers:
|
|||
|
do (request--consume-100-continue)
|
|||
|
do (let ((response (make-request-response
|
|||
|
:-buffer (current-buffer)
|
|||
|
:-backend 'curl)))
|
|||
|
(request--clean-header response)
|
|||
|
(request--cut-header response)
|
|||
|
(push response history))))
|
|||
|
|
|||
|
(goto-char (point-min))
|
|||
|
(nconc (list :num-redirects num-redirects :url-effective url-effective
|
|||
|
:history (nreverse history))
|
|||
|
(request--parse-response-at-point)))))
|
|||
|
|
|||
|
(defun request--curl-absolutify-redirects (start-url redirects)
|
|||
|
"Convert relative paths in REDIRECTS to absolute URLs.
|
|||
|
START-URL is the URL requested."
|
|||
|
(cl-loop for prev-url = start-url then url
|
|||
|
for url in redirects
|
|||
|
unless (string-match url-nonrelative-link url)
|
|||
|
do (setq url (url-expand-file-name url prev-url))
|
|||
|
collect url))
|
|||
|
|
|||
|
(defun request--curl-absolutify-location-history (start-url history)
|
|||
|
"Convert relative paths in HISTORY to absolute URLs.
|
|||
|
START-URL is the URL requested."
|
|||
|
(when history
|
|||
|
(setf (request-response-url (car history)) start-url))
|
|||
|
(cl-loop for url in (request--curl-absolutify-redirects
|
|||
|
start-url
|
|||
|
(mapcar (lambda (response)
|
|||
|
(request-response-header response "location"))
|
|||
|
history))
|
|||
|
for response in (cdr history)
|
|||
|
do (setf (request-response-url response) url)))
|
|||
|
|
|||
|
(defun request--curl-callback (proc event)
|
|||
|
(let* ((buffer (process-buffer proc))
|
|||
|
(response (process-get proc :request-response))
|
|||
|
(symbol-status (request-response-symbol-status response))
|
|||
|
(settings (request-response-settings response)))
|
|||
|
(request-log 'debug "REQUEST--CURL-CALLBACK event = %s" event)
|
|||
|
(request-log 'debug "REQUEST--CURL-CALLBACK proc = %S" proc)
|
|||
|
(request-log 'debug "REQUEST--CURL-CALLBACK buffer = %S" buffer)
|
|||
|
(request-log 'debug "REQUEST--CURL-CALLBACK symbol-status = %S"
|
|||
|
symbol-status)
|
|||
|
(cond
|
|||
|
((and (memq (process-status proc) '(exit signal))
|
|||
|
(/= (process-exit-status proc) 0))
|
|||
|
(setf (request-response-error-thrown response) (cons 'error event))
|
|||
|
(apply #'request--callback buffer settings))
|
|||
|
((equal event "finished\n")
|
|||
|
(cl-destructuring-bind (&key version code num-redirects history error
|
|||
|
url-effective)
|
|||
|
(condition-case err
|
|||
|
(with-current-buffer buffer
|
|||
|
(request--curl-preprocess))
|
|||
|
((debug error)
|
|||
|
(list :error err)))
|
|||
|
(request--curl-absolutify-location-history (plist-get settings :url)
|
|||
|
history)
|
|||
|
(setf (request-response-status-code response) code)
|
|||
|
(setf (request-response-url response) url-effective)
|
|||
|
(setf (request-response-history response) history)
|
|||
|
(setf (request-response-error-thrown response)
|
|||
|
(or error (when (>= code 400) `(error . (http ,code)))))
|
|||
|
(apply #'request--callback buffer settings))))))
|
|||
|
|
|||
|
(cl-defun request--curl-sync (url &rest settings &key response &allow-other-keys)
|
|||
|
;; To make timeout work, use polling approach rather than using
|
|||
|
;; `call-process'.
|
|||
|
(let (finished)
|
|||
|
(prog1 (apply #'request--curl url
|
|||
|
:complete (lambda (&rest _) (setq finished t))
|
|||
|
settings)
|
|||
|
(let ((proc (get-buffer-process (request-response--buffer response))))
|
|||
|
(while (and (not finished) (request--process-live-p proc))
|
|||
|
(accept-process-output proc))))))
|
|||
|
|
|||
|
(defun request--curl-get-cookies (host localpart secure)
|
|||
|
(request--netscape-get-cookies (request--curl-cookie-jar)
|
|||
|
host localpart secure))
|
|||
|
|
|||
|
|
|||
|
;;; Netscape cookie.txt parser
|
|||
|
|
|||
|
(defun request--netscape-cookie-parse ()
|
|||
|
"Parse Netscape/Mozilla cookie format."
|
|||
|
(goto-char (point-min))
|
|||
|
(let ((tsv-re (concat "^\\(#HttpOnly_\\)?"
|
|||
|
(cl-loop repeat 6 concat "\\([^\t\n]+\\)\t")
|
|||
|
"\\(.*\\)"))
|
|||
|
cookies)
|
|||
|
(while (not (eobp))
|
|||
|
;; HttpOnly cookie starts with '#' but its line is not comment line(#60)
|
|||
|
(cond ((and (looking-at-p "^#") (not (looking-at-p "^#HttpOnly_"))) t)
|
|||
|
((looking-at-p "^$") t)
|
|||
|
((looking-at tsv-re)
|
|||
|
(let ((cookie (cl-loop for i from 1 to 8 collect (match-string i))))
|
|||
|
(push cookie cookies))))
|
|||
|
(forward-line 1))
|
|||
|
(setq cookies (nreverse cookies))
|
|||
|
(cl-loop for (http-only domain flag path secure expiration name value) in cookies
|
|||
|
collect (list domain
|
|||
|
(equal flag "TRUE")
|
|||
|
path
|
|||
|
(equal secure "TRUE")
|
|||
|
(null (not http-only))
|
|||
|
(string-to-number expiration)
|
|||
|
name
|
|||
|
value))))
|
|||
|
|
|||
|
(defun request--netscape-filter-cookies (cookies host localpart secure)
|
|||
|
(cl-loop for (domain flag path secure-1 http-only expiration name value) in cookies
|
|||
|
when (and (equal domain host)
|
|||
|
(equal path localpart)
|
|||
|
(or secure (not secure-1)))
|
|||
|
collect (cons name value)))
|
|||
|
|
|||
|
(defun request--netscape-get-cookies (filename host localpart secure)
|
|||
|
(when (file-readable-p filename)
|
|||
|
(with-temp-buffer
|
|||
|
(erase-buffer)
|
|||
|
(insert-file-contents filename)
|
|||
|
(request--netscape-filter-cookies (request--netscape-cookie-parse)
|
|||
|
host localpart secure))))
|
|||
|
|
|||
|
(provide 'request)
|
|||
|
|
|||
|
;;; request.el ends here
|