webfakes/0000755000176200001440000000000014535506762012056 5ustar liggesuserswebfakes/NAMESPACE0000644000176200001440000000164214515705422013270 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(format,webfakes_app) S3method(format,webfakes_app_process) S3method(format,webfakes_regexp) S3method(format,webfakes_request) S3method(format,webfakes_response) S3method(print,webfakes_app) S3method(print,webfakes_app_process) S3method(print,webfakes_regexp) S3method(print,webfakes_request) S3method(print,webfakes_response) export(git_app) export(http_time_stamp) export(httpbin_app) export(local_app_process) export(mw_cgi) export(mw_cookie_parser) export(mw_etag) export(mw_json) export(mw_log) export(mw_multipart) export(mw_range_parser) export(mw_raw) export(mw_static) export(mw_text) export(mw_urlencoded) export(new_app) export(new_app_process) export(new_regexp) export(oauth2_httr_login) export(oauth2_login) export(oauth2_resource_app) export(oauth2_third_party_app) export(server_opts) export(tmpl_glue) useDynLib(webfakes, .registration = TRUE, .fixes = "c_") webfakes/LICENSE0000644000176200001440000000005614521205417013050 0ustar liggesusersYEAR: 2023 COPYRIGHT HOLDER: webfakes authors webfakes/README.md0000644000176200001440000001112114535456505013330 0ustar liggesusers # webfakes > Your own web server for happy HTTP testing [![R build status](https://github.com/r-lib/webfakes/workflows/R-CMD-check/badge.svg)](https://github.com/r-lib/webfakes/actions) [![CRAN status](https://www.r-pkg.org/badges/version/webfakes)](https://CRAN.R-project.org/package=webfakes) [![Codecov test coverage](https://codecov.io/gh/r-lib/webfakes/branch/main/graph/badge.svg)](https://app.codecov.io/gh/r-lib/webfakes?branch=main) [![R-CMD-check](https://github.com/r-lib/webfakes/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-lib/webfakes/actions/workflows/R-CMD-check.yaml) Lightweight fake web apps for testing. Built using the [civetweb](https://github.com/civetweb/civetweb) embedded web server. ## Features - Complete web app framework, define handlers for HTTP requests in R. - Write your own app for your custom test cases; our use app similar to the `https://httpbin.org` API, so often you don’t need to write your own web app (e.g. if you are writing an HTTP client (httr, curl, crul). - Run one web app per test suite, per test file or per test case. - Flexible path matching, with parameters and regular expressions. - Built in templating system using glue or bring your own template engine. - Middleware to parse JSON, multipart and URL encoded request bodies. - A web app is just an R object. It can be saved to disk, copied to another R process, etc. - A web app is extensible, by adding new routes and middleware to it. - Helper functions for sending JSON, files from disk, etc. - App-specific environment to store any data including data from requests to the fake app. - After a web app is launched from R, you can interact with it from R but also from the command line, your browser, etc. Nice for debugging. - The web server runs in the R process, so it has no problems with local firewalls. - Multi-threaded web server supports concurrent HTTP requests. - Limit download speed to simulate low bandwidth. ## Optional dependencies - The jsonlite package is needed for the `mw_json()` middleware, the `response$send_json()` method and the `httpbin_app()` app. - The glue package is needed for the `tmpl_glue()` template engine. - The callr package is needed for `new_app_process()` and `local_app_process` to work. - The `/brotli` endpoint of `httpbin_app()` needs the brotli package. - The `/deflate` endpoint of `httpbin_app()` needs the zip package. - The `/digest-auth` endpoint of `httpbin_app()` needs the digest package. - `git_app()` requires the processx package. ## Installation Install the release version from CRAN: ``` r install.packages("webfakes") ``` If you need the development version of the package, install it from GitHub: ``` r pak::pak("r-lib/webfakes") ``` ## Usage Start a web app at the beginning of your tests or test file, and stop it after. Here is an example with the testthat package. Suppose you want to test that your `get_hello()` function can query an API: `local_app_process()` helps you clean up the web server process after the test block, or test file. It is similar to the `withr::local_*` functions. ``` r app <- webfakes::new_app() app$get("/hello/:user", function(req, res) { res$send(paste0("Hello ", req$params$user, "!")) }) web <- webfakes::local_app_process(app) test_that("can use hello API", { url <- web$url("/hello/Gabor") expect_equal(get_hello(url), "Hello Gabor!") }) ``` When testing HTTP clients you can often use the built in `httpbin_app()`: ``` r httpbin <- webfakes::local_app_process(webfakes::httpbin_app()) ``` ``` r test_that("HTTP errors are caught", { url <- httpbin$url("/status/404") resp <- httr::GET(url) expect_error(httr::stop_for_status(resp), class = "http_404") }) ``` #> Test passed 😸 ## Documentation See ## Links ### Other solutions for HTTP testing in R: - [vcr](https://github.com/ropensci/vcr) - [httptest](https://github.com/nealrichardson/httptest) ### R web application frameworks webfakes focuses on testing, these packages are for writing real web apps: - [shiny](https://github.com/rstudio/shiny) - [opencpu](https://www.opencpu.org/) - [plumber](https://github.com/rstudio/plumber) - [fiery](https://github.com/thomasp85/fiery) - [RestRserve](https://github.com/rexyai/RestRserve) ## Code of Conduct Please note that the fs project is released with a [Contributor Code of Conduct](https://webfakes.r-lib.org/dev/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. ## License MIT © RStudio webfakes/man/0000755000176200001440000000000014515731450012621 5ustar liggesuserswebfakes/man/git_app.Rd0000644000176200001440000000224314515734123014534 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/git-app.R \name{git_app} \alias{git_app} \title{Web app that acts as a git http server} \usage{ git_app( git_root, git_cmd = "git", git_timeout = as.difftime(1, units = "mins"), filter = TRUE, cleanup = TRUE ) } \arguments{ \item{git_root}{Path to the root of the directory tree to be served.} \item{git_cmd}{Command to call, by default it is \code{"git"}. It may also be a full path to git.} \item{git_timeout}{A \code{difftime} object, time limit for the git command.} \item{filter}{Whether to support the \code{filter} capability in the server.} \item{cleanup}{Whether to clean up \code{git_root} when the app is garbage collected.} } \description{ It is useful for tests that need an HTTP git server. } \examples{ \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} dir.create(tmp <- tempfile()) setwd(tmp) system("git clone --bare https://github.com/cran/crayon") system("git clone --bare https://github.com/cran/glue") app <- git_app(tmp) git <- new_app_process(app) system(paste("git ls-remote", git$url("/crayon"))) \dontshow{\}) # examplesIf} } webfakes/man/httpbin_app.Rd0000644000176200001440000000155214430757546015435 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/httpbin.R \name{httpbin_app} \alias{httpbin_app} \title{Generic web app for testing HTTP clients} \usage{ httpbin_app(log = interactive()) } \arguments{ \item{log}{Whether to log requests to the standard output.} } \value{ A \code{webfakes_app}. } \description{ A web app similar to \verb{https://httpbin.org}. See \href{https://webfakes.r-lib.org/httpbin.html}{its specific docs}. You can also see these docs locally, by starting the app: \if{html}{\out{
}}\preformatted{httpbin <- new_app_process(httpbin_app()) browseURL(httpbin$url()) }\if{html}{\out{
}} } \examples{ app <- httpbin_app() proc <- new_app_process(app) url <- proc$url("/get") resp <- curl::curl_fetch_memory(url) curl::parse_headers_list(resp$headers) cat(rawToChar(resp$content)) proc$stop() } webfakes/man/mw_urlencoded.Rd0000644000176200001440000000163514515726753015756 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-urlencoded.R \name{mw_urlencoded} \alias{mw_urlencoded} \title{Middleware to parse an url-encoded request body} \usage{ mw_urlencoded(type = "application/x-www-form-urlencoded") } \arguments{ \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ This is typically data from a form. The parsed data is added as the \code{form} element of the request object. } \examples{ app <- new_app() app$use(mw_urlencoded()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()} } \concept{middleware} webfakes/man/webfakes_response.Rd0000644000176200001440000001251014427146706016622 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/response.R \name{webfakes_response} \alias{webfakes_response} \title{A webfakes response object} \description{ webfakes creates a \code{webfakes_response} object for every incoming HTTP request. This object is passed to every matched route and middleware, until the HTTP response is sent. It has reference semantics, so handlers can modify it. } \details{ Fields and methods: \itemize{ \item \code{app}: The \code{webfakes_app} object itself. \item \code{req}: The request object. \item \code{headers_sent}: Whether the response headers were already sent out. \item \code{locals}: Local variables, the are shared between the handler functions. This is for the end user, and not for the middlewares. \item \code{delay(secs)}: delay the response for a number of seconds. If a handler calls \code{delay()}, the same handler will be called again, after the specified number of seconds have passed. Use the \code{locals} environment to distinguish between the calls. If you are using \code{delay()}, and want to serve requests in parallel, then you probably need a multi-threaded server, see \code{\link[=server_opts]{server_opts()}}. \item \code{add_header(field, value)}: Add a response header. Note that \code{add_header()} may create duplicate headers. You usually want \code{set_header()}. \item \code{get_header(field)}: Query the currently set response headers. If \code{field} is not present it return \code{NULL}. \item \code{on_response(fun)}: Run the \code{fun} handler function just before the response is sent out. At this point the headers and the body are already properly set. \item \code{redirect(path, status = 302)}: Send a redirect response. It sets the \code{Location} header, and also sends a \code{text/plain} body. \item \code{render(view, locals = list())}: Render a template page. Searches for the \code{view} template page, using all registered engine extensions, and calls the first matching template engine. Returns the filled template. \item \code{send(body)}. Send the specified body. \code{body} can be a raw vector, or HTML or other text. For raw vectors it sets the content type to \code{application/octet-stream}. \item \code{send_json(object = NULL, text = NULL, ...)}: Send a JSON response. Either \code{object} or \code{text} must be given. \code{object} will be converted to JSON using \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}. \code{...} are passed to \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}. It sets the content type appropriately. \item \code{send_file(path, root = ".")}: Send a file. Set \code{root = "/"} for absolute file names. It sets the content type automatically, based on the extension of the file, if it is not set already. \item \code{send_status(status)}: Send the specified HTTP status code, without a response body. \item \code{send_chunk(data)}: Send a chunk of a response in chunked encoding. The first chunk will automatically send the HTTP response headers. Webfakes will automatically send a final zero-lengh chunk, unless \verb{$delay()} is called. \item \code{set_header(field, value)}: Set a response header. If the headers have been sent out already, then it throws a warning, and does nothing. \item \code{set_status(status)}: Set the response status code. If the headers have been sent out already, then it throws a warning, and does nothing. \item \code{set_type(type)}: Set the response content type. If it contains a \code{/} character then it is set as is, otherwise it is assumed to be a file extension, and the corresponding MIME type is set. If the headers have been sent out already, then it throws a warning, and does nothing. \item \code{add_cookie(name, value, options)}: Adds a cookie to the response. \code{options} is a named list, and may contain: \itemize{ \item \code{domain}: Domain name for the cookie, not set by default. \item \code{expires}: Expiry date in GMT. It must be a POSIXct object, and will be formatted correctly. \item 'http_only': if TRUE, then it sets the 'HttpOnly' attribute, so Javasctipt cannot access the cookie. \item \code{max_age}: Maximum age, in number of seconds. \item \code{path}: Path for the cookie, defaults to "/". \item \code{same_site}: The 'SameSite' cookie attribute. Possible values are "strict", "lax" and "none". \item \code{secure}: if TRUE, then it sets the 'Secure' attribute. } \item \code{clear_cookie(name, options = list())}: clears a cookie. Typically, web browsers will only clear a cookie if the options also match. \item \code{write(data)}: writes (part of) the body of the response. It also sends out the response headers, if they haven't been sent out before. } Usually you need one of the \code{send()} methods, to send out the HTTP response in one go, first the headers, then the body. Alternatively, you can use \verb{$write()} to send the response in parts. } \examples{ # This is how you can see the request and response objects: app <- new_app() app$get("/", function(req, res) { browser() res$send("done") }) app # Now start this app on a port: # app$listen(3000) # and connect to it from a web browser: http://127.0.0.1:3000 # You can also use another R session to connect: # httr::GET("http://127.0.0.1:3000") # or the command line curl tool: # curl -v http://127.0.0.1:3000 # The app will stop while processing the request. } \seealso{ \link{webfakes_request} for the webfakes request object. } webfakes/man/oauth2_resource_app.Rd0000644000176200001440000001321514306332713017060 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_resource_app} \alias{oauth2_resource_app} \title{Fake OAuth 2.0 resource and authorization app} \usage{ oauth2_resource_app( access_duration = 3600L, refresh_duration = 7200L, refresh = TRUE, seed = NULL, authorize_endpoint = "/authorize", token_endpoint = "/token" ) } \arguments{ \item{access_duration}{After how many seconds should access tokens expire.} \item{refresh_duration}{After how many seconds should refresh tokens expire (ignored if \code{refresh} is \code{FALSE}).} \item{refresh}{Should a refresh token be returned (logical).} \item{seed}{Random seed used when creating tokens. If \code{NULL}, we rely on R to provide a seed. The app uses its own RNG stream, so it does not affect reproducibility of the tests.} \item{authorize_endpoint}{The authorization endpoint of the resource server. Change this from the default if the real app that you are faking does not use \verb{/authorize}.} \item{token_endpoint}{The endpoint to request tokens. Change this if the real app that you are faking does not use \verb{/token}.} } \value{ a \code{webfakes} app webfakes app } \description{ The webfakes package comes with two fake apps that allow to imitate the OAuth2.0 flow in your test cases. (See \href{https://aaronparecki.com/oauth-2-simplified/}{Aaron Parecki’s tutorial} for a good introduction to OAuth2.0.) One app (\code{oauth2_resource_app()}) is the API server that serves both as the resource and provides authorization. \code{oauth2_third_party_app()} plays the role of the third-party app. They are useful when testing or demonstrating code handling OAuth2.0 authorization, token caching, etc. in a package. The apps can be used in your tests directly, or you could adapt one or both of them to better mimic a particular OAuth2.0 flow. } \details{ The app has the following endpoints: \itemize{ \item \code{GET /register} is the endpoint that you can use to register your third party app. It needs to receive the \code{name} of the third party app, and its \code{redirect_uri} as query parameters, otherwise returns an HTTP 400 error. On success it returns a JSON dictionary with entries \code{name} (the name of the third party app), \code{client_id}, \code{client_secret} and \code{redirect_uri}. \item \code{GET /authorize} is the endpoint where the user of the third party app is sent. You can change the URL of this endpoint with the \code{authorize_endpoint} argument. It needs to receive the \code{client_id} of the third party app, and its correct \code{redirect_uri} as query parameters. It may receive a \code{state} string as well, which can be used by a client to identify the request. Otherwise it generates a random \code{state} string. On error it fails with a HTTP 400 error. On success it returns a simple HTML login page. \item \code{POST /authorize/decision} is the endpoint where the HTML login page generated at \verb{/authorize} connects back to, either with a positive or negative result. The form on the login page will send the \code{state} string and the user's choice in the \code{action} variable. If the user authorized the third party app, then they are redirected to the \code{redirect_uri} of the app, with a temporary \code{code} and the \code{state} string supplied as query parameters. Otherwise a simple HTML page is returned. \item \code{POST /token} is the endpoint where the third party app requests a temporary access token. It is also uses for refreshing an access token with a refresh token. You can change the URL of this endpoint with the \code{token_endpoint} argument. To request a new token or refresh an existing one, the following data must be included in either a JSON or an URL encoded request body: \itemize{ \item \code{grant_type}, this must be \code{authorization_code} for new tokens, and \code{refresh_token} for refreshing. \item \code{code}, this must be the temporary code obtained from the \verb{/authorize/decision} redirection, for new tokens. It is not needed when refreshing. \item \code{client_id} must be the client id of the third party app. \item \code{client_secret} must be the client secret of the third party app. \item \code{redirect_uri} must be the correct redirection URI of the third party app. It is not needed when refreshing tokens. \item \code{refresh_token} must be the refresh token obtained previously, when refreshing a token. It is not needed for new tokens. On success a JSON dictionary is returned with entries: \code{access_token}, \code{expiry} and \code{refresh_token}. (The latter is omitted if the \code{refresh} argument is \code{FALSE}). } \item \code{GET /locals} returns a list of current apps, access tokens and refresh tokens. \item \code{GET /data} is an endpoint that returns a simple JSON response, and needs authorization. } \subsection{Notes}{ \itemize{ \item Using this app in your tests requires the glue package, so you need to put it in \code{Suggests}. \item You can add custom endpoints to the app, as needed. \item If you need authorization in your custom endpoint, call \code{app$is_authorized()} in your handler: \if{html}{\out{
}}\preformatted{if (!app$is_authorized(req, res)) return() }\if{html}{\out{
}} \code{app$is_authorized()} returns an HTTP 401 response if the client is not authorized, so you can simply return from your handler. } For more details see \code{vignette("oauth", package = "webfakes")}. } } \section{\code{oauth2_resource_app()}}{ App representing the API server (resource/authorization) } \seealso{ Other OAuth2.0 functions: \code{\link{oauth2_httr_login}()}, \code{\link{oauth2_login}()}, \code{\link{oauth2_third_party_app}()} } \concept{OAuth2.0 functions} webfakes/man/mw_json.Rd0000644000176200001440000000212314515726753014574 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-json.R \name{mw_json} \alias{mw_json} \title{Middleware to parse a JSON body} \usage{ mw_json(type = "application/json", simplifyVector = FALSE, ...) } \arguments{ \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} \item{simplifyVector}{Whether to simplify lists to vectors, passed to \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}}.} \item{...}{Arguments to pass to \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}}, that performs the JSON parsing.} } \value{ Handler function. } \description{ Adds the parsed object as the \code{json} element of the request object. } \examples{ app <- new_app() app$use(mw_json()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/local_app_process.Rd0000644000176200001440000000144314172041777016607 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/local-app-process.R \name{local_app_process} \alias{local_app_process} \title{App process that is cleaned up automatically} \usage{ local_app_process(app, ..., .local_envir = parent.frame()) } \arguments{ \item{app}{\code{webfakes_app} object, the web app to run.} \item{...}{Passed to \code{\link[=new_app_process]{new_app_process()}}.} \item{.local_envir}{The environment to attach the process cleanup to. Typically a frame. When this frame finishes, the process is stopped.} } \description{ You can start the process with an explicit \verb{$start()} call. Alternatively it starts up at the first \verb{$url()} or \verb{$get_port()} call. } \seealso{ \code{\link[=new_app_process]{new_app_process()}} for more details. } webfakes/man/mw_cgi.Rd0000644000176200001440000000532614515726753014375 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-cgi.R \name{mw_cgi} \alias{mw_cgi} \title{Middleware that calls a CGI script} \usage{ mw_cgi(command, args = character(), timeout = as.difftime(Inf, units = "secs")) } \arguments{ \item{command}{External command to run.} \item{args}{Arguments to pass to the external command.} \item{timeout}{Timeout for the external command. If the command does not terminate in time, the web server kills it and returns an 500 response.} } \value{ A function with signature \if{html}{\out{
}}\preformatted{function(req, res, env = character()) }\if{html}{\out{
}} See \href{https://www.ietf.org/rfc/rfc3875}{RFC 3875} for details on the CGI protocol. The request body (if any) is passed to the external command as standard intput. \code{mw_cgi()} sets \code{CONTENT_LENGTH}, \code{CONTENT_TYPE}, \code{GATEWAY_INTERFACE}, \code{PATH_INFO}, \code{QUERY_STRING}, \code{REMOTE_ADDR}, \code{REMOTE_HOST}, \code{REMOTE_USER}, \code{REQUEST_METHOD}, \code{SERVER_NAME}, \code{SERVER_PORT}, \code{SERVER_PROTOCOL}, \code{SERVER_SOFTEWARE}. It does not currently set the \code{AUTH_TYPE}, \code{PATH_TRANSLATED}, \code{REMOTE_IDENT}, \code{SCRIPT_NAME} environment variables. The standard output of the external command is used to set the response status code, the response headers and the response body. Example output from git's CGI: \if{html}{\out{
}}\preformatted{Status: 200 OK Expires: Fri, 01 Jan 1980 00:00:00 GMT Pragma: no-cache Cache-Control: no-cache, max-age=0, must-revalidate Content-Type: application/x-git-upload-pack-advertisement 000eversion 2 0015agent=git/2.42.0 0013ls-refs=unborn 0020fetch=shallow wait-for-done 0012server-option 0017object-format=sha1 0010object-info 0000 }\if{html}{\out{
}} } \description{ You can use it as an unconditional middleware in \code{app$use()}, as a handler on \code{app$get()}, \code{app$post()}, etc., or you can call it from a handler. See examples below. } \examples{ app <- new_app() app$use(mw_cgi("echo", "Status: 200\n\nHello")) app app2 <- new_app() app2$get("/greet", mw_cgi("echo", "Status: 200\n\nHello")) app2 # Using `mw_cgi()` in a handler, you can pass extra environment variables app3 <- new_app() cgi <- mw_cgi("echo", "Status: 200\n\nHello") app2$get("/greet", function(req, res) { cgi(req, res, env = c("EXTRA_VAR" = "EXTRA_VALUE")) }) app3 } \seealso{ Other middleware: \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/glossary.Rd0000644000176200001440000001057614430757546014776 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/docs.R \name{glossary} \alias{glossary} \title{webfakes glossary} \description{ webfakes glossary } \section{Webfakes glossary}{ The webfakes package uses vocabulary that is standard for web apps, especially those developed with Express.js, but not necessarily well known to all R package developers. \subsection{app}{ (Also: fake web app, webfakes app.) A web application that can be served by webfakes's web server, typically in another process, an \emph{app process}. Sometimes we call it a \emph{fake} web app, to emphasize that we use it for testing real web apps and APIs. You can create a webfakes app with the \code{new_app()} function. A webfakes app is an R object that you can save to disk with \code{saveRDS()} , and you can also include it in your package. You can start an with its \verb{$listen()} method. Since the main R process runs that test suite code, you usually run them in a subprocess, see \code{new_app_process()} or \code{local_app_process()}. } \subsection{app process}{ (Also: web server process, webfakes subprocess.) An app process is an R subprocess, started from the main R process, to serve a webfakes \emph{app}. You can create an app process object with \code{new_app_process()} or \code{local_app_process()}. By default the actual process does not start yet, when you create it. You can start it explicitly with the \verb{$start} method of the app process object, or by querying its URL with \verb{$url()} or its port with \verb{$get_port()}. For test cases, you typically start app processes at these places: \itemize{ \item In a \code{setup*.R} file, to start an app that the whole test suite can use. \item Alternatively, in a \code{helper*.R} file, to start an app that the whole test suite can use, and it works better for interactive development. \item At the beginning of a test file, to create an app for a single test file. \item Inside \code{test_that()}, to create an app for a single test block. } See the How-to for details about each. } \subsection{handler}{ (Or handler function.) A handler is a \emph{route} or a \emph{middleware}. } \subsection{handler stack}{ This is a stack of handler functions, which are called by the app one after the other, passing the request and response objects to them. Handlers typically manipulate the request and/or response objects. A terminal handler instructs the app to return the response to the HTTP client. A non-terminal handler tells the app to keep calling handlers, by returning the string \code{"next"}. } \subsection{httpbin app}{ This is an example app, which implements the excellent \verb{https://httpbin.org/} web service. You can use it to simulate certain HTTP responses. It is most handy for HTTP clients, but potentially useful for other tools as well. Use \code{httpbin_app()} to create an instance of this app. } \subsection{middleware}{ A middleware is a handler function that is not bound to a path. It is called by the router, like other handler functions. It may manipulate the request or the response, or can have a side effect. Some example built-in middleware functions in webfakes: \itemize{ \item \code{mw_json()} parses a request's JSON body into an R object. \item \code{mw_log()} logs requests and responses to the screen or to a file. \item \code{mw_static()} serves static files from the directory. } You can also write your own middleware functions. } \subsection{path matching}{ The router performs path matching when it goes over the handler stack. If the HTTP method and path of a \emph{route} match the HTTP method and URL of the request, then the handler is called, otherwise it is not. Paths can have parameters and be regular expressions. See \code{?new_regexp()} for regular expressions and "Path parameters" in \code{?new_app()} for parameters. } \subsection{route}{ A route is a handler function that is bound to certain paths of you web app. If the request URL matches the path of the route, then the handler function is called, to give it a chance to send the appropriate response. Route paths may have parameters or they can be regular expressions in webfakes. } \subsection{routing}{ Routing is the process of going over the handlers stack, and calling handler functions, one after the other, until one handles the request. If a handler function is a \emph{route}, then the router only calls it if its path matches the request URL. } } webfakes/man/mw_log.Rd0000644000176200001440000000256514515726753014416 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-log.R \name{mw_log} \alias{mw_log} \title{Log requests to the standard output or other connection} \usage{ mw_log(format = "dev", stream = "stdout") } \arguments{ \item{format}{Log format. Not implemented currently.} \item{stream}{R connection to log to. \code{"stdout"} means the standard output, \code{"stderr"} is the standard error. You can also supply a connection object, but then you need to be sure that it will be valid when the app is actually running.} } \value{ Handler function. } \description{ A one line log entry for every request. The output looks like this: \if{html}{\out{
}}\preformatted{GET http://127.0.0.1:3000/image 200 3 ms - 4742 }\if{html}{\out{
}} and contains \itemize{ \item the HTTP method, \item the full request URL, \item the HTTP status code of the response, \item how long it took to process the response, in ms, \item and the size of the response body, in bytes. } } \examples{ app <- new_app() app$use(mw_log()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/mw_etag.Rd0000644000176200001440000000202014515726753014537 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-etag.R \name{mw_etag} \alias{mw_etag} \title{Middleware that add an \code{ETag} header to the response} \usage{ mw_etag(algorithm = "crc32") } \arguments{ \item{algorithm}{Checksum algorithm to use. Only \code{"crc32"} is implemented currently.} } \value{ Handler function. } \description{ If the response already has an \code{ETag} header, then it is kept. } \details{ This middleware handles the \code{If-None-Match} headers, and it sets the status code of the response to 304 if \code{If-None-Match} matches the \code{ETag}. It also removes the response body in this case. } \examples{ app <- new_app() app$use(mw_etag()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/mw_raw.Rd0000644000176200001440000000150214515726753014414 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-raw.R \name{mw_raw} \alias{mw_raw} \title{Middleware to read the raw body of a request} \usage{ mw_raw(type = "application/octet-stream") } \arguments{ \item{type}{Content type to match. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ Adds the raw body, as a raw object to the \code{raw} field of the request. } \examples{ app <- new_app() app$use(mw_raw()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/webfakes_request.Rd0000644000176200001440000000376614172041777016470 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/request.R \name{webfakes_request} \alias{webfakes_request} \title{A webfakes request object} \description{ webfakes creates a \code{webfakes_request} object for every incoming HTTP request. This object is passed to every matched route and middleware, until the response is sent. It has reference semantics, so handlers can modify it. } \details{ Fields and methods: \itemize{ \item \code{app}: The \code{webfakes_app} object itself. \item \code{headers}: Named list of HTTP request headers. \item \code{hostname}: The Host header, the server hostname and maybe port. \item \code{method}: HTTP method. \item \code{path}: Server path. \item \code{protocol}: \code{"http"} or \code{"https"}. \item \code{query_string}: The raw query string, without the starting \verb{?}. \item \code{query}: Parsed query parameters in a named list. \item \code{remote_addr}: String, the domain name or IP address of the client. webfakes runs on the localhost, so this is \verb{127.0.0.1}. \item \code{url}: The full URL of the request. \item \code{get_header(field)}: Function to query a request header. Returns \code{NULL} if the header is not present. } Body parsing middleware adds additional fields to the request object. See \code{\link[=mw_raw]{mw_raw()}}, \code{\link[=mw_text]{mw_text()}}, \code{\link[=mw_json]{mw_json()}}, \code{\link[=mw_multipart]{mw_multipart()}} and \code{\link[=mw_urlencoded]{mw_urlencoded()}}. } \examples{ # This is how you can see the request and response objects: app <- new_app() app$get("/", function(req, res) { browser() res$send("done") }) app # Now start this app on a port: # app$listen(3000) # and connect to it from a web browser: http://127.0.0.1:3000 # You can also use another R session to connect: # httr::GET("http://127.0.0.1:3000") # or the command line curl tool: # curl -v http://127.0.0.1:3000 # The app will stop while processing the request. } \seealso{ \link{webfakes_response} for the webfakes response object. } webfakes/man/oauth2_httr_login.Rd0000644000176200001440000000250314172041777016550 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_httr_login} \alias{oauth2_httr_login} \title{Helper function to use httr's OAuth2.0 functions non-interactively, e.g. in test cases} \usage{ oauth2_httr_login(expr) } \arguments{ \item{expr}{Expression that calls \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, either directly, or indirectly.} } \value{ The return value of \code{expr}. } \description{ To perform an automatic acknowledgement and log in for a local OAuth2.0 app, run by httr, wrap the expression that obtains the OAuth2.0 token in \code{oauth2_httr_login()}. } \details{ In interactive sessions, \code{oauth2_httr_login()} overrides the \code{browser} option, and when httr opens a browser page, it calls \code{\link[=oauth2_login]{oauth2_login()}} in a subprocess. In non-interactive sessions, httr does not open a browser page, only messages the user to do it manually. \code{oauth2_httr_login()} listens for these messages, and calls \code{\link[=oauth2_login]{oauth2_login()}} in a subprocess. } \seealso{ See \code{?vignette("oauth", package = "webfakes")} for a case study that uses this function. Other OAuth2.0 functions: \code{\link{oauth2_login}()}, \code{\link{oauth2_resource_app}()}, \code{\link{oauth2_third_party_app}()} } \concept{OAuth2.0 functions} webfakes/man/mw_text.Rd0000644000176200001440000000161714515726753014616 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-text.R \name{mw_text} \alias{mw_text} \title{Middleware to parse a plain text body} \usage{ mw_text(default_charset = "utf-8", type = "text/plain") } \arguments{ \item{default_charset}{Encoding to set on the text.} \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ Adds the parsed object as the \code{text} element of the request object. } \examples{ app <- new_app() app$use(mw_text()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/mw_static.Rd0000644000176200001440000000235714515726753015123 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-static.R \name{mw_static} \alias{mw_static} \title{Middleware function to serve static files} \usage{ mw_static(root, set_headers = NULL) } \arguments{ \item{root}{Root path of the served files. Everything under this directory is served automatically. Directory lists are not currently supports.} \item{set_headers}{Callback function to call before a file is served.} } \value{ Handler function. } \description{ The content type of the response is set automatically from the extension of the file. Note that this is a terminal middleware handler function. If a file is served, then the rest of the handler functions will not be called. If a file was not found, however, the rest of the handlers are still called. } \examples{ root <- system.file(package = "webfakes", "examples", "static", "public") app <- new_app() app$use(mw_static(root = root)) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/oauth2_login.Rd0000644000176200001440000000170114172041777015506 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_login} \alias{oauth2_login} \title{Helper function to log in to a third party OAuth2.0 app without a browser} \usage{ oauth2_login(login_url) } \arguments{ \item{login_url}{The login URL of the third party app.} } \value{ A named list with \itemize{ \item \code{login_response} The curl HTTP response object for the login page. \item \code{token_response} The curl HTTP response object for submitting the login page. } } \description{ It works with \code{\link[=oauth2_resource_app]{oauth2_resource_app()}}, and any third party app, including the fake \code{\link[=oauth2_third_party_app]{oauth2_third_party_app()}}. } \details{ See \code{test-oauth.R} in webfakes for an example. } \seealso{ Other OAuth2.0 functions: \code{\link{oauth2_httr_login}()}, \code{\link{oauth2_resource_app}()}, \code{\link{oauth2_third_party_app}()} } \concept{OAuth2.0 functions} webfakes/man/mw_multipart.Rd0000644000176200001440000000161014515726753015644 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-multipart.R \name{mw_multipart} \alias{mw_multipart} \title{Parse a multipart HTTP request body} \usage{ mw_multipart(type = "multipart/form-data") } \arguments{ \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ Adds the parsed form fields in the \code{form} element of the request and the parsed files to the \code{files} element. } \examples{ app <- new_app() app$use(mw_multipart()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/tmpl_glue.Rd0000644000176200001440000000306114172041777015105 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/tmpl-glue.R \name{tmpl_glue} \alias{tmpl_glue} \title{glue based template engine} \usage{ tmpl_glue( sep = "", open = "{", close = "}", na = "NA", transformer = NULL, trim = TRUE ) } \arguments{ \item{sep}{Separator used to separate elements.} \item{open}{The opening delimiter. Doubling the full delimiter escapes it.} \item{close}{The closing delimiter. Doubling the full delimiter escapes it.} \item{na}{Value to replace NA values with. If \code{NULL} missing values are propagated, that is an \code{NA} result will cause \code{NA} output. Otherwise the value is replaced by the value of \code{na}.} \item{transformer}{A function taking three parameters \code{code}, \code{envir} and \code{data} used to transform the output of each block before during or after evaluation.} \item{trim}{Whether to trim the input template with \code{\link[glue:trim]{glue::trim()}} or not.} } \value{ Template function. } \description{ Use this template engine to create pages with glue templates. See \code{\link[glue:glue]{glue::glue()}} for the syntax. } \examples{ # See th 'hello' app at hello_root <- system.file(package = "webfakes", "examples", "hello") hello_root app <- new_app() app$engine("txt", tmpl_glue()) app$use(mw_log()) app$get("/view", function(req, res) { txt <- res$render("test") res$ set_type("text/plain")$ send(txt) }) # Switch to the app's root: setwd(hello_root) # Now start the app with: app$listen(3000L) # Or start it in another process: new_process(app) } webfakes/man/server_opts.Rd0000644000176200001440000000460614172041777015476 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/server.R \name{server_opts} \alias{server_opts} \title{Webfakes web server options} \usage{ server_opts( remote = FALSE, port = NULL, num_threads = 1, interfaces = "127.0.0.1", enable_keep_alive = FALSE, access_log_file = remote, error_log_file = TRUE, tcp_nodelay = FALSE, throttle = Inf ) } \arguments{ \item{remote}{Meta-option. If set to \code{TRUE}, webfakes uses slightly different defaults, that are more appropriate for a background server process.} \item{port}{Port to start the web server on. Defaults to a randomly chosen port.} \item{num_threads}{Number of request handler threads to use. Typically you don't need more than one thread, unless you run test cases in parallel or you make concurrent HTTP requests.} \item{interfaces}{The network interfaces to listen on. Being a test web server, it defaults to the localhost. Only bind to a public interface if you know what you are doing. webfakes was not designed to serve public web pages.} \item{enable_keep_alive}{Whether the server keeps connections alive.} \item{access_log_file}{\code{TRUE}, \code{FALSE}, or a path. See 'Logging' below.} \item{error_log_file}{\code{TRUE}, \code{FALSE}, or a path. See 'Logging' below.} \item{tcp_nodelay}{if \code{TRUE} then packages will be sent as soon as possible, instead of waiting for a full buffer or timeout to occur.} \item{throttle}{Limit download speed for clients. If not \code{Inf}, then it is the maximum number of bytes per second, that is sent to as connection.} } \value{ List of options that can be passed to \code{webfakes_app$listen()} (see \code{\link[=new_app]{new_app()}}), and \code{\link[=new_app_process]{new_app_process()}}. } \description{ Webfakes web server options } \section{Logging}{ \itemize{ \item For \code{access_log_file}, \code{TRUE} means \verb{/access.log}. \item For \code{error_log_file}, \code{TRUE} means \verb{/error.log}. } \verb{} is set to the contents of the \code{WEBFAKES_LOG_DIR} environment variable, if it is set. Otherwise it is set to \verb{/webfakes} for local apps and \verb{//webfakes} for remote apps (started with \code{new_app_procss()}). \verb{} is the session temporary directory of the \emph{main process}. \verb{} is the process id of the subprocess. } \examples{ # See the defaults server_opts() } webfakes/man/oauth2_third_party_app.Rd0000644000176200001440000000615214172041777017574 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_third_party_app} \alias{oauth2_third_party_app} \title{App representing the third-party app} \usage{ oauth2_third_party_app(name = "Third-Party app") } \arguments{ \item{name}{Name of the third-party app} } \value{ webfakes app } \description{ The webfakes package comes with two fake apps that allow to imitate the OAuth2.0 flow in your test cases. (See \href{https://aaronparecki.com/oauth-2-simplified/}{Aaron Parecki’s tutorial} for a good introduction to OAuth2.0.) One app (\code{oauth2_resource_app()}) is the API server that serves both as the resource and provides authorization. \code{oauth2_third_party_app()} plays the role of the third-party app. They are useful when testing or demonstrating code handling OAuth2.0 authorization, token caching, etc. in a package. The apps can be used in your tests directly, or you could adapt one or both of them to better mimic a particular OAuth2.0 flow. } \details{ Endpoints: \itemize{ \item \code{POST /login/config} Use this endpoint to configure the client ID and the client secret of the app, received from \code{\link[=oauth2_resource_app]{oauth2_resource_app()}} (or another resource app). You need to send in a JSON or URL encoded body: \itemize{ \item \code{auth_url}, the authorization URL of the resource app. \item \code{token_url}, the token URL of the resource app. \item \code{client_id}, the client ID, received from the resource app. \item \code{client_secret} the client secret, received from the resource app. } \item \code{GET /login} Use this endpoint to start the login process. It will redirect to the resource app for authorization and after the OAuth2.0 dance to \verb{/login/redirect}. \item \code{GET /login/redirect}, \code{POST /login/redirect} This is the redirect URI of the third party app. (Some HTTP clients redirect a \code{POST} to a \code{GET}, others don't, so it has both.) This endpoint is used by the resource app, and it received the \code{code} that can be exchanged to an access token and the \code{state} which was generated in \verb{/login}. It contacts the resource app to get an access token, and then stores the token in its \code{app$locals} local variables. It fails with HTTP code 500 if it cannot obtain an access token. On success it returns a JSON dictionary with \code{access_token}, \code{expiry} and \code{refresh_token} (optionally) by default. This behavior can be changed by redefining the \code{app$redirect_hook()} function. \item \code{GET /locals} returns the tokens that were obtained from the resource app. \item \code{GET /data} is an endpoint that uses the obtained token(s) to connect to the \verb{/data} endpoint of the resource app. The \verb{/data} endpoint of the resource app needs authorization. It responds with the response of the resource app. It tries to refresh the access token of the app if needed. } For more details see \code{vignette("oauth", package = "webfakes")}. } \seealso{ Other OAuth2.0 functions: \code{\link{oauth2_httr_login}()}, \code{\link{oauth2_login}()}, \code{\link{oauth2_resource_app}()} } \concept{OAuth2.0 functions} webfakes/man/http_time_stamp.Rd0000644000176200001440000000065114427135612016313 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{http_time_stamp} \alias{http_time_stamp} \title{Format a time stamp for HTTP} \usage{ http_time_stamp(t = Sys.time()) } \arguments{ \item{t}{Date-time value to format, defaults to the current date and time. It must be a POSIXct object.} } \value{ Character vector, formatted date-time. } \description{ Format a time stamp for HTTP } webfakes/man/mw_cookie_parser.Rd0000644000176200001440000000143214515726753016452 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-cookie-parser.R \name{mw_cookie_parser} \alias{mw_cookie_parser} \title{Middleware to parse Cookies} \usage{ mw_cookie_parser() } \value{ Handler function. } \description{ Adds the cookies as the \code{cookies} element of the request object. } \details{ It ignores cookies in an invalid format. It ignores duplicate cookies: if two cookies have the same name, only the first one is included. } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/new_app_process.Rd0000644000176200001440000000606414312576030016301 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/app-process.R \name{new_app_process} \alias{new_app_process} \alias{webfakes_app_process} \title{Run a webfakes app in another process} \usage{ new_app_process( app, port = NULL, opts = server_opts(remote = TRUE), start = FALSE, auto_start = TRUE, process_timeout = NULL, callr_opts = NULL ) } \arguments{ \item{app}{\code{webfakes_app} object, the web app to run.} \item{port}{Port to use. By default the OS assigns a port.} \item{opts}{Server options. See \code{\link[=server_opts]{server_opts()}} for the defaults.} \item{start}{Whether to start the web server immediately. If this is \code{FALSE}, and \code{auto_start} is \code{TRUE}, then it is started as neeed.} \item{auto_start}{Whether to start the web server process automatically. If \code{TRUE} and the process is not running, then \verb{$start()}, \verb{$get_port()} and \verb{$url()} start the process.} \item{process_timeout}{How long to wait for the subprocess to start, in milliseconds.} \item{callr_opts}{Options to pass to \code{\link[callr:r_session_options]{callr::r_session_options()}} when setting up the subprocess.} } \value{ A \code{webfakes_app_process} object. \subsection{Methods}{ The \code{webfakes_app_process} class has the following methods: \if{html}{\out{
}}\preformatted{get_app() get_port() stop() get_state() local_env(envvars) url(path = "/", query = NULL) }\if{html}{\out{
}} \itemize{ \item \code{envvars}: Named list of environment variables. The \code{{url}} substring is replaced by the URL of the app. \item \code{path}: Path to return the URL for. \item \code{query}: Additional query parameters, a named list, to add to the URL. } \code{get_app()} returns the app object. \code{get_port()} returns the port the web server is running on. \code{stop()} stops the web server, and also the subprocess. If the error log file is not empty, then it dumps its contents to the screen. \code{get_state()} returns a string, the state of the web server: \itemize{ \item \code{"not running"} the server is not running (because it was stopped already). \item \code{"live"} means that the server is running. \item \code{"dead"} means that the subprocess has quit or crashed. } \code{local_env()} sets the given environment variables for the duration of the app process. It resets them in \verb{$stop()}. Webfakes replaces \code{{url}} in the value of the environment variables with the app URL, so you can set environment variables that point to the app. \code{url()} returns the URL of the web app. You can use the \code{path} parameter to return a specific path. } } \description{ Runs an app in a subprocess, using \link[callr:r_session]{callr::r_session}. } \examples{ app <- new_app() app$get("/foo", function(req, res) { res$send("Hello world!") }) proc <- new_app_process(app) url <- proc$url("/foo") resp <- curl::curl_fetch_memory(url) cat(rawToChar(resp$content)) proc$stop() } \seealso{ \code{\link[=local_app_process]{local_app_process()}} for automatically cleaning up the subprocess. } webfakes/man/rmd-fragments/0000755000176200001440000000000014172041777015374 5ustar liggesuserswebfakes/man/rmd-fragments/oauth2.Rmd0000644000176200001440000000120714172041777017242 0ustar liggesusersThe webfakes package comes with two fake apps that allow to imitate the OAuth2.0 flow in your test cases. (See [Aaron Parecki's tutorial](https://aaronparecki.com/oauth-2-simplified/) for a good introduction to OAuth2.0.) One app (`oauth2_resource_app()`) is the API server that serves both as the resource and provides authorization. `oauth2_third_party_app()` plays the role of the third-party app. They are useful when testing or demonstrating code handling OAuth2.0 authorization, token caching, etc. in a package. The apps can be used in your tests directly, or you could adapt one or both of them to better mimic a particular OAuth2.0 flow. webfakes/man/new_regexp.Rd0000644000176200001440000000140114172041777015254 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/path.R \name{new_regexp} \alias{new_regexp} \alias{webfakes_regexp} \title{Create a new regular expression to use in webfakes routes} \usage{ new_regexp(x) } \arguments{ \item{x}{String scalar containing a regular expression.} } \value{ String with class \code{webfakes_regexp}. } \description{ Note that webfakes uses PERL regular expressions. } \details{ As R does not have data type or class for regular expressions, you can use \code{new_regexp()} to mark a string as a regular expression, when adding routes. } \examples{ new_regexp("^/api/match/(?.*)$") } \seealso{ The 'Path specification' and 'Path parameters' chapters of the manual of \code{\link[=new_app]{new_app()}}. } webfakes/man/mw_range_parser.Rd0000644000176200001440000000225014515726753016274 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-range-parser.R \name{mw_range_parser} \alias{mw_range_parser} \title{Middleware to parse a Range header} \usage{ mw_range_parser() } \value{ Handler function. } \description{ Adds the requested ranges to the \code{ranges} element of the request object. \code{request$ranges} is a data frame with two columns, \code{from} and \code{to}. Each row corresponds one requested interval. } \details{ When the last \code{n} bytes of the file are requested, the matrix row is set to \code{c(0, -n)}. When all bytes after a \code{p} position are requested, the matrix row is set to \code{c(p, Inf)}. If the intervals overlap, then \code{ranges} is not set, i.e. the \code{Range} header is ignored. If its syntax is invalid or the unit is not \code{bytes}, then the \code{Range} header is ignored. } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/new_app.Rd0000644000176200001440000003335314506043724014550 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/app.R \name{new_app} \alias{new_app} \alias{webfakes_app} \title{Create a new web application} \usage{ new_app() } \value{ A new \code{webfakes_app}. } \description{ Create a new web application } \details{ The typical workflow of creating a web application is: \enumerate{ \item Create a \code{webfakes_app} object with \code{new_app()}. \item Add middleware and/or routes to it. \item Start is with the \code{webfakes_app$listen()} method, or start it in another process with \code{\link[=new_app_process]{new_app_process()}}. \item Make queries to the web app. \item Stop it via \code{CTRL+C} / \code{ESC}, or, if it is running in another process, with the \verb{$stop()} method of \code{\link[=new_app_process]{new_app_process()}}. } A web application can be \itemize{ \item restarted, \item saved to disk, \item copied to another process using the callr package, or a similar way, \item embedded into a package, \item extended by simply adding new routes and/or middleware. } The webfakes API is very much influenced by the \href{http://expressjs.com/}{express.js} project. \subsection{Create web app objects}{ \if{html}{\out{
}}\preformatted{new_app() }\if{html}{\out{
}} \code{new_app()} returns a \code{webfakes_app} object the has the methods listed on this page. An app is an environment with S3 class \code{webfakes_app}. } \subsection{The handler stack}{ An app has a stack of handlers. Each handler can be a route or middleware. The differences between the two are: \itemize{ \item A route is bound to one or more paths on the web server. Middleware is not (currently) bound to paths, but run for all paths. \item A route is usually (but not always) the end of the handler stack for a request. I.e. a route takes care of sending out the response to the request. Middleware typically performs some action on the request or the response, and then the next handler in the stack is invoked. } } \subsection{Routes}{ The following methods define routes. Each method corresponds to the HTTP verb with the same name, except for \code{app$all()}, which creates a route for all HTTP methods. \if{html}{\out{
}}\preformatted{app$all(path, ...) app$delete(path, ...) app$get(path, ...) app$head(path, ...) app$patch(path, ...) app$post(path, ...) app$put(path, ...) ... (see list below) }\if{html}{\out{
}} \itemize{ \item \code{path} is a path specification, see 'Path specification' below. \item \code{...} is one or more handler functions. These will be placed in the handler stack, and called if they match an incoming HTTP request. See 'Handler functions' below. } webfakes also has methods for the less frequently used HTTP verbs: \code{CONNECT}, \code{MKCOL}, \code{OPTIONS}, \code{PROPFIND}, \code{REPORT}. (The method names are always in lowercase.) If a request is not handled by any routes (or handler functions in general), then webfakes will send a simple HTTP 404 response. } \subsection{Middleware}{ \code{app$use()} adds a middleware to the handler stack. A middleware is a handler function, see 'Handler functions' below. webfakes comes with middleware to perform common tasks: \itemize{ \item \code{\link[=mw_cookie_parser]{mw_cookie_parser()}} parses \code{Cookie} headers. \item \code{\link[=mw_etag]{mw_etag()}} adds an \code{ETag} header to the response. \item \code{\link[=mw_json]{mw_json()}} parses JSON request bodies. \item \code{\link[=mw_log]{mw_log()}} logs each requests to standard output, or another connection. \item \code{\link[=mw_multipart]{mw_multipart()}} parses multipart request bodies. \item \code{\link[=mw_range_parser]{mw_range_parser()}} parses \code{Range} headers. \item \code{\link[=mw_raw]{mw_raw()}} parses raw request bodies. \item \code{\link[=mw_static]{mw_static()}} serves static files from a directory. \item \code{\link[=mw_text]{mw_text()}} parses plain text request bodies. \item \code{\link[=mw_urlencoded]{mw_urlencoded()}} parses URL encoded request bodies. } \if{html}{\out{
}}\preformatted{app$use(..., .first = FALSE) }\if{html}{\out{
}} \itemize{ \item \code{...} is a set of (middleware) handler functions. They are added to the handler stack, and called for every HTTP request. (Unless an HTTP response is created before reaching this point in the handler stack.) \item \code{.first} set to \code{TRUE} is you want to add the handler function to the bottom of the stack. } } \subsection{Handler functions}{ A handler function is a route or middleware. A handler function is called by webfakes with the incoming HTTP request and the outgoing HTTP response objects (being built) as arguments. The handler function may query and modify the members of the request and/or the response object. If it returns the string \code{"next"}, then it is \emph{not} a terminal handler, and once it returns, webfakes will move on to call the next handler in the stack. A typical route: \if{html}{\out{
}}\preformatted{app$get("/user/:id", function(req, res) \{ id <- req$params$id ... res$ set_status(200L)$ set_header("X-Custom-Header", "foobar")$ send_json(response, auto_unbox = TRUE) \}) }\if{html}{\out{
}} \itemize{ \item The handler belongs to an API path, which is a wildcard path in this case. It matches \verb{/user/alice}, \verb{/user/bob}, etc. The handler will be only called for GET methods and matching API paths. \item The handler receives the request (\code{req}) and the response (\code{res}). \item It sets the HTTP status, additional headers, and sends the data. (In this case the \code{webfakes_response$send_json()} method automatically converts \code{response} to JSON and sets the \code{Content-Type} and \code{Content-Length} headers. \item This is a terminal handler, because it does \emph{not} return \code{"next"}. Once this handler function returns, webfakes will send out the HTTP response. } A typical middleware: \if{html}{\out{
}}\preformatted{app$use(function(req, res) \{ ... "next" \}) }\if{html}{\out{
}} \itemize{ \item There is no HTTP method and API path here, webfakes will call the handler for each HTTP request. \item This is not a terminal handler, it does return \code{"next"}, so after it returns webfakes will look for the next handler in the stack. } } \subsection{Errors}{ If a handler function throws an error, then the web server will return a HTTP 500 \code{text/plain} response, with the error message as the response body. } \subsection{Request and response objects}{ See \link{webfakes_request} and \link{webfakes_response} for the methods of the request and response objects. } \subsection{Path specification}{ Routes are associated with one or more API paths. A path specification can be \itemize{ \item A "plain" (i.e. without parameters) string. (E.g. \code{"/list"}.) \item A parameterized string. (E.g. \code{"/user/:id"}.) \item A regular expression created via \code{\link[=new_regexp]{new_regexp()}} function. \item A list or character vector of the previous ones. (Regular expressions must be in a list.) } } \subsection{Path parameters}{ Paths that are specified as parameterized strings or regular expressions can have parameters. For parameterized strings the keys may contain letters, numbers and underscores. When webfakes matches an API path to a handler with a parameterized string path, the parameters will be added to the request, as \code{params}. I.e. in the handler function (and subsequent handler functions, if the current one is not terminal), they are available in the \code{req$params} list. For regular expressions, capture groups are also added as parameters. It is best to use named capture groups, so that the parameters are in a named list. If the path of the handler is a list of parameterized strings or regular expressions, the parameters are set according to the first matching one. } \subsection{Templates}{ webfakes supports templates, using any template engine. It comes with a template engine that uses the glue package, see \code{\link[=tmpl_glue]{tmpl_glue()}}. \code{app$engine()} registers a template engine, for a certain file extension. The \verb{$render()} method of \link{webfakes_response} can be called from the handler function to evaluate a template from a file. \if{html}{\out{
}}\preformatted{app$engine(ext, engine) }\if{html}{\out{
}} \itemize{ \item \code{ext}: the file extension for which the template engine is added. It should not contain the dot. E.g. \verb{"html"', }"brew"`. \item \code{engine}: the template engine, a function that takes the file path (\code{path}) of the template, and a list of local variables (\code{locals}) that can be used in the template. It should return the result. } An example template engine that uses glue might look like this: \if{html}{\out{
}}\preformatted{app$engine("txt", function(path, locals) \{ txt <- readChar(path, nchars = file.size(path)) glue::glue_data(locals, txt) \}) }\if{html}{\out{
}} (The built-in \code{\link[=tmpl_glue]{tmpl_glue()}} engine has more features.) This template engine can be used in a handler: \if{html}{\out{
}}\preformatted{app$get("/view", function(req, res) \{ txt <- res$render("test") res$ set_type("text/plain")$ send(txt) \}) }\if{html}{\out{
}} The location of the templates can be set using the \code{views} configuration parameter, see the \verb{$set_config()} method below. In the template, the variables passed in as \code{locals}, and also the response local variables (see \code{locals} in \link{webfakes_response}), are available. } \subsection{Starting and stopping}{ \if{html}{\out{
}}\preformatted{app$listen(port = NULL, opts = server_opts(), cleanup = TRUE) }\if{html}{\out{
}} \itemize{ \item \code{port}: port to listen on. When \code{NULL}, the operating system will automatically select a free port. \item \code{opts}: options to the web server. See \code{\link[=server_opts]{server_opts()}} for the list of options and their default values. \item \code{cleanup}: stop the server (with an error) if the standard input of the process is closed. This is handy when the app runs in a \code{callr::r_session} subprocess, because it stops the app (and the subprocess) if the main process has terminated. } This method does not return, and can be interrupted with \code{CTRL+C} / \code{ESC} or a SIGINT signal. See \code{\link[=new_app_process]{new_app_process()}} for interrupting an app that is running in another process. When \code{port} is \code{NULL}, the operating system chooses a port where the app will listen. To be able to get the port number programmatically, before the listen method blocks, it advertises the selected port in a \code{webfakes_port} condition, so one can catch it: webfakes by default binds only to the loopback interface at 127.0.0.1, so the webfakes web app is never reachable from the network. \if{html}{\out{
}}\preformatted{withCallingHandlers( app$listen(), "webfakes_port" = function(msg) print(msg$port) ) }\if{html}{\out{
}} } \subsection{Logging}{ webfakes can write an access log that contains an entry for all incoming requests, and also an error log for the errors that happen while the server is running. This is the default behavior for local app (the ones started by \code{app$listen()} and for remote apps (the ones started via \code{new_app_process()}: \itemize{ \item Local apps do not write an access log by default. \item Remote apps write an access log into the \verb{/webfakes//access.log} file, where \verb{} is the session temporary directory of the \emph{main process}, and \verb{} is the process id of the \emph{subprocess}. \item Local apps write an error log to \verb{/webfakes/error.log}, where \verb{} is the session temporary directory of the current process. \item Remote app write an error log to the \verb{/webfakes//error.log}, where \verb{} is the session temporary directory of the \emph{main process} and \verb{} is the process id of the \emph{subprocess}`. } See \code{\link[=server_opts]{server_opts()}} for changing the default logging behavior. } \subsection{Shared app data}{ \if{html}{\out{
}}\preformatted{app$locals }\if{html}{\out{
}} It is often useful to share data between handlers and requests in an app. \code{app$locals} is an environment that supports this. E.g. a middleware that counts the number of requests can be implemented as: \if{html}{\out{
}}\preformatted{app$use(function(req, res) \{ locals <- req$app$locals if (is.null(locals$num)) locals$num <- 0L locals$num <- locals$num + 1L "next" \}) }\if{html}{\out{
}} \link{webfakes_response} objects also have a \code{locals} environment, that is initially populated as a copy of \code{app$locals}. } \subsection{Configuration}{ \if{html}{\out{
}}\preformatted{app$get_config(key) app$set_config(key, value) }\if{html}{\out{
}} \itemize{ \item \code{key}: configuration key. \item \code{value}: configuration value. } Currently used configuration values: \itemize{ \item \code{views}: path where webfakes searches for templates. } } } \examples{ # see example web apps in the `/examples` directory in system.file(package = "webfakes", "examples") app <- new_app() app$use(mw_log()) app$get("/hello", function(req, res) { res$send("Hello there!") }) app$get(new_regexp("^/hi(/.*)?$"), function(req, res) { res$send("Hi indeed!") }) app$post("/hello", function(req, res) { res$send("Got it, thanks!") }) app # Start the app with: app$listen() # Or start it in another R session: new_app_process(app) } \seealso{ \link{webfakes_request} for request objects, \link{webfakes_response} for response objects. } webfakes/man/webfakes-package.Rd0000644000176200001440000000254014535456123016274 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/webfakes-package.R \docType{package} \name{webfakes-package} \alias{webfakes} \alias{webfakes-package} \title{webfakes: Fake Web Apps for HTTP Testing} \description{ Create a web app that makes it easier to test web clients without using the internet. It includes a web app framework with path matching, parameters and templates. Can parse various 'HTTP' request bodies. Can send 'JSON' data or files from the disk. Includes a web app that implements the 'httpbin.org' web service. } \seealso{ Useful links: \itemize{ \item \url{https://webfakes.r-lib.org/} \item \url{https://github.com/r-lib/webfakes} \item Report bugs at \url{https://github.com/r-lib/webfakes/issues} } } \author{ \strong{Maintainer}: Gábor Csárdi \email{csardi.gabor@gmail.com} Other contributors: \itemize{ \item Posit Software, PBC [copyright holder, funder] \item Civetweb contributors (see inst/credits/ciwetweb.md) [contributor] \item Redoc contributors (see inst/credits/redoc.md) [contributor] \item L. Peter Deutsch (src/md5.h) [contributor] \item Martin Purschke (src/md5.h) [contributor] \item Aladdin Enterprises (src/md5.h) [copyright holder] \item Maëlle Salmon \email{maelle.salmon@yahoo.se} (\href{https://orcid.org/0000-0002-2815-0399}{ORCID}) [contributor] } } \keyword{internal} webfakes/DESCRIPTION0000644000176200001440000000416614535506762013573 0ustar liggesusersPackage: webfakes Title: Fake Web Apps for HTTP Testing Version: 1.3.0 Authors@R: c( person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", role = c("aut", "cre")), person("Posit Software, PBC", role = c("cph", "fnd")), person(, "Civetweb contributors", role = "ctb", comment = "see inst/credits/ciwetweb.md"), person(, "Redoc contributors", role = "ctb", comment = "see inst/credits/redoc.md"), person("L. Peter", "Deutsch", role = "ctb", comment = "src/md5.h"), person("Martin", "Purschke", role = "ctb", comment = "src/md5.h"), person(, "Aladdin Enterprises", role = "cph", comment = "src/md5.h"), person("Maëlle", "Salmon", , "maelle.salmon@yahoo.se", role = "ctb", comment = c(ORCID = "0000-0002-2815-0399")) ) Description: Create a web app that makes it easier to test web clients without using the internet. It includes a web app framework with path matching, parameters and templates. Can parse various 'HTTP' request bodies. Can send 'JSON' data or files from the disk. Includes a web app that implements the 'httpbin.org' web service. License: MIT + file LICENSE URL: https://webfakes.r-lib.org/, https://github.com/r-lib/webfakes BugReports: https://github.com/r-lib/webfakes/issues Depends: R (>= 3.6) Imports: stats, tools, utils Suggests: brotli, callr, covr, curl, digest, glue, httpuv, httr, jsonlite, processx, testthat (>= 3.0.0), withr, xml2, zip (>= 2.3.0) Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 RoxygenNote: 7.2.3 NeedsCompilation: yes Packaged: 2023-12-11 01:08:07 UTC; gaborcsardi Author: Gábor Csárdi [aut, cre], Posit Software, PBC [cph, fnd], Civetweb contributors [ctb] (see inst/credits/ciwetweb.md), Redoc contributors [ctb] (see inst/credits/redoc.md), L. Peter Deutsch [ctb] (src/md5.h), Martin Purschke [ctb] (src/md5.h), Aladdin Enterprises [cph] (src/md5.h), Maëlle Salmon [ctb] () Maintainer: Gábor Csárdi Repository: CRAN Date/Publication: 2023-12-11 04:20:02 UTC webfakes/tests/0000755000176200001440000000000014172041777013215 5ustar liggesuserswebfakes/tests/testthat/0000755000176200001440000000000014535506762015060 5ustar liggesuserswebfakes/tests/testthat/test-app.R0000644000176200001440000000031714172041777016736 0ustar liggesusers test_that("invalid handler", { app <- new_app() expect_error( app$use("foobar"), "Invalid webfakes handler" ) expect_error( app$get("/foo", 1:100), "Invalid webfakes handler" ) }) webfakes/tests/testthat/fixtures/0000755000176200001440000000000014515731756016732 5ustar liggesuserswebfakes/tests/testthat/fixtures/views/0000755000176200001440000000000014172041777020063 5ustar liggesuserswebfakes/tests/testthat/fixtures/views/test-view.html0000644000176200001440000000011414172041777022674 0ustar liggesusers { greeting } { user } webfakes/tests/testthat/fixtures/git-repo.tar.gz0000644000176200001440000006217314515731756021620 0ustar liggesusers5eX8**P D#EP"! )"|E+Vb⦆`;k{ow. ~=Arb{YYY9c\⋵#femkgoxK.%$vH‘%\~'pj7 Op. 1;)å2V_0lemhoU9"!+pzGk;k5W"xpD| M/I2y"I*[6H"!Y4y|*L"il ?I`pR#1.JqQ)#d8FII|!,2Xbi 80Y-Uz|p'2< 񏦙ʗHD5iQ #B=W=oec_G;:C3gsls|!^;8U#eEu|O[[[+?w\B!;bJù|&KK1hc&*aD,~U 7?Y$J?rd :wb6'2!:mmm/C 7m,/KvĠ L&E*?-6*ǝV<nílG)ތ+Opy8/љd`o:SH-bMYf ef`֖bPiRg@15:9ɀT3*V[8Tqb;>Pat:M dBVLDf/h\OØE8fbi4-& gMm<4ZӲv?`0)HdKZ[[ԓvjo8SOJap c@yc}ةlr@lЕlBW \ d%RK@r@ Q\0P F9O"JPPA?AiRQq݄8"(! PBJYcL&zG.$8;}>.@;JPP%l!'%ZLE O21>B1!A l)QA %TuP{ Hep)5a0[f$&Bs xbw$%d!64@!M5ǝ24p~K#LM  PHV+@,8b S"˔Hsqq%i<>F:(BSAGK3MՕ} hs6.es^ NV@1*}WQ@1T(!/4qP"@>и b1Ӕ`!*Fk* '%@ "A ㉯DoVeNV&t W(%(9<N_ # *a`H,5drc#f?+W 3 yyD`ITj tXHN7q%ACI$duE?FYʆ=T|jrԩAX?*)JWwڔH&49x v%AbHJ.4:]u&!2 #I!B"]T!A5-R ґb\"@'5.Fabρ )lF'5]lPTpI0*3T_MM8+3h,@s{` -Sfj+ ѳ~ 8<3U #f A6  S$ YBR6*zҊ fh@G CT$Д @)dMǨ $DD)(J+;'I('Ɂ(mP'J5/%zrY0`~`P wēBafX$OhX@yE2@(8kIlMq>@/92 FK7`|^4SA T^FPR q|0:O8D,%>)Xpz=8`< Qb@!pr,Ey|D!΁hI&1d Ra:@9#h2|0)2A( Pm+$39la|IF,BSIK@2ᛱ Xpa5a"PՒ4^JT5B@ ?ҌL_Q —{E= -BQ:`oSkb+A$] $HN!b>W>*XHؐQaMDTHRz: 6Vgt!C6moQDsalp:9A.0/"8QHB"@ ͷZzR 8T`]Ɛ?,0;,@L:*$6TADo!A] ؤIiIOf``ф+e!*'!gARʡP(dX4J;03"'Tg5JRq@}&é x_S1$ӣGAIx'b dYY<7@SܘW7-Un[E޵$߳ľ QG$$i4O 8½<!D'/ug!OI Ga2 Bex|!Pɾ2Ȳ҆ ~4DYMiRT7 %@]Y Pu.aC{0IOBB`pKI8ͫ^PA@ IAMh Bo@u(ȶFJ_W6OȐsh[J1(QC^_Jq`u`i?;;+:_;+o.QLl 1S -,QRiG8> ڌ:ٯ:s6@F՚Tf_3. tW&<\ C%/`бX.ekJBe7*Z)hJ3i#`BIq f-% MR9No WԼHى"{X`B0l6RK'٠QF'INHr/($} \JHeHL%!0E #u`ĆS7_+K@o1Ul|DH }M 4UטQ9`>FLD UoBZB8dSi}c0(APȿ,er(4P}3z l &C4u`Тh*:,S@p BХUMc ֤ϖ6:<9LjL$A5g`ݻC@A;0!P2JM[ | b XESPn&"??+#K-?h` &0hCU |ҞZ&AU[ .7`#RXLd@ Zl0F3@dv! seBe iawƣt[5hS+I2 }T$$lĚ b!..4M[K=qJ6\ mdh(T/`˅xRy"V9 1ԳxDIXʶp+&]<TD@Vn.IQ"pL&}YKڐ TIB'`Pδ>9-0+ ̚iC'VDހBh6TuZZ . @$˸cnntM^f ǡx3F-zmT'`1tHh@ X'̈́"}@A;'!TB &U* wR*'AxʷwǮԈȴt(qmehFR뫍^J7 vQ%"}a BUAtUU*%T.KJae\l Y #@q2u<)ГlrffDGr\tJE2#w"ps˒&HN.׹u.QWت/?97]шw4@% =O #|RPF˱L)9HT"! 3OBfKE<*(*LmKdt'`2+"Ԥ*X 0Mp(4<-_o [=ϕwL]SdTTQ} xh^GHW 5#'=$=V L2R|w+fz2C?S z9M,tG{yIK(/ Cf#!S*_])LbC3l2T(DD0q&1c`ˀ( 7p/GlG?2@6VGEE HQPP`"ʽ`&R0K8#Mz <%8ޯ i02B R;|&X UADJdHkD6H>-Xh& w*AXkXt,g;T+)7,凂` CpRZJ9Q)-3Y;z2; p3uCLP3\%g0|Vʊ0 m6+rTF vwSD$p5I$"rl :>(mwp7Dy6>F4ZC|V";"8fBx ie{}jb ,0M,PLȨf1FQ Ć9a0!Zk_,a NI%5,`k CY#cUCc1TµyH>8)Lwp@71|!`ƤRI !V+EV-z ف 2 (\ v!6WH*cz z?烨D#*FD MHZ*9AE1)%&<_`ƿC=ݟ{xbߴKF䀇L$VTD(]*IZݔhg. ?oe4`_o{_9aj_wlIC8@H~吮ɯɯ"~XO_hCj~ ~S×pmD08%24HB2JG2ɢJ"|VfzR~O{t*H#6mUGQ~tm3&DI0:&砬&r|hk%?nT6rg`&V& X1x֊wGW Uo T FX@~ `Ny e3HJ1x@V~'kZ.ߨ~T? IRWpP$z,C T 0huY{ _ 06YZ!-~m "` >`#LT\*s\V)VBjfD 9$x % (AѸG|! vvK^ł`^`l@Z#L6T; SMQ0󕃀)9] FRN(QaF@ BTTTmfA/2k)R$0XlFY'CC%ldWdRNiQT_Y[fTV jN-r+X TBT@ߡ.R )@_IJP=>Jv4Tg ($SnDt#*)cEiNByD%źb{V'SV'^ѵ&T)"ĉSz,4nWqxPPRLXnTW$ޔV9u!}Vtۯ—J[2Ni~5\Tܯhkf1Km{0h!BZN)2A=N\כ<ǟW{[[Ǻ~pljPZxh _8OUB8Ylp\;B (<EB4\@G(ї)i+YʄPi MwB;O&ԅ %D~B 'wي$&.Jj'`j/=3%P-A/ =Vj6FPE'Njc ށOCk]@2&_l yam!Vc@`|ϭuJ%pH/85E31ʢ0Ŗ ΕEf=Щ~/@T5";|*QD I"D\hZ5J݊BU"%/x ̿Bp)5`=j`~1-d".uFJ3X4י8lCT_:E^$/Qqf#//ʝ(!3jR`a k#CS;UaE 7Aɚ}!VG?#4l.5)ǘDVp䵃-T'rYp,NCJ 'F@/2-V M."jEEqA*@7Q2DC@"0u]xGD 0T* K1ٌZ}q\TEr]Ef%t\sG j:+" Syc!aP2<do$ K5%DRex..2a'CLd>ԁJNQ$׏ [d4@4*7E463 ںRKPכ:n2j/UwVXh߯}=֡n/g;.b@|d&.8ȴ@""nFCd8QhCc=Ζ@uk.ΤوdP}-S"h:Ne񋙱bDð?UBj M_Rjہ`/\_R?)mm︾3m#;ʖml8v3džslq'. Z oo_w;{0^:y{v{nGp7 ;`7u$&n[pܾ[5mnpk-ۃ;nWpہ[܍ܚn p7ܝ ~Op;knnnnۀ ܦ7KmNC4`E`Q5p޾tHc nTL30gVi$Y=`NvvdcKj{U H 3Jؾ䀷5nC-W]^Q=w|wQMhÍ.0j6>rnV74hN S7.叴<7ޠToa5SrrIґEWUk.8D-u߃JtKmh~zf:Mcwm7*cٝuqߗIM!'<KM-~; }}1)sJcBoL<-YwA+2<8a]fҠ.y7>{}YޅiOJ=2^ȪN^M{X 33" Sǿk޿]Ō|۴i+gOjyL1}'_>O.n jLif9~I9asL/9`ne7ZXW\NJK6ٷg+&/d(JrJ@' /dtwзzĦU\u8LީŦ͡##{_!B~ H/e + Bs]ẍ˃s.K_~Kwm faIN>kt+0X>O/KNϸxyzrιg$oXˑsǗܥ_ hu;ngؽE]^@>몓 }քF+Ch++8IYkr|'u[y`0k `I|:zudzCLn=ak1/63/@#,kw!e;Uʳ:#f̲ܻúʏw鴫CLq3=I(zkeO5uz{EIFZe ܝ ˦]G3LTtf@}fGk78aڶfw~sgx^:jQ,mD)[Tv\ErvvȥI$fjev}YG< vˁYyyz¤e:;DT T dY^J:e][< ޗGީ [H!6c6a>[_};>쫭z%V:j[̵VSqJq#'Y =l(;{F}4ɹ.1˚o;vr3 w#Z Ѵ<{lGkO6=/t\+?:*!{/ =\xql{֖OXrnMwe[΢_xPrE/AJh֔A=,ʯ|-ZTHJ vsHcϵY{w=NOnR|rek#5"^oU9d#~8uĶ)65 H|woљ{wIԼǮ^MG,Ne  9oxc|XЛO̪jƵ\kŴo = NX~I@3ƞK-[FLRM&n*]ci4uS] 嫚Qx3F׌c'Slȫy_?q}6gk^u0wW]fU+5/8dqz3sO1Orjicg?zk4ںI*hCnEf~8g#=.;c]/8{gk=QW]jsJc'/h%f89(HsVzZI;+wqja3}\Wvr·m'&:w+&|T3YgM UjIQg:!t820~/ca-hI8 0ٜ!6/ch6bZRD\0-ݒCekzmu#dTzzq¹m԰17As羽`6] /js޾].&,@NlYX%+~z?tm54f#iw krH_ Χunἤ<F9@Z}8HZsgll?!O%UWf {`NF_MO[Fú=VL=s٬+{6}77e2PGGȭy֊#b[uzLky.bVм-DΫvm~܋m."1ׄ5*gJ:]}Nc^u.2^6׹_i۰kq~ێK9o:\gFe7Dk<{ <ѻ<6fj;6G$&}ݦ7>0fo[pr_%=-B7]SZ2ͧ=,Z}(ש!#:aXv7Qb[Eŭ`uf9&* kCGT9xG/4Eů.xWdt7{{y&p)k6蜨Vۃh7\Sm&O~E /+'fMD";sox; 6}ƇOO՝2R'oc>{3[%L{|ӊUFz8`p6vgs~#!waۋrg+ZIۏJ?7>tWͽv H}D ]L*q,򾍧Uj2ƌENK0.\.ZȟBqL?հforlw8XtuҼޚirO1rEЙlFg98t<>0kP\WNnPu4 ,G{RxlADA#s^ըM7F42zՏfv<0Scqrcږ-S;H{5md͍,<۳eFۖw݌fl|њ7qv^:<7Mtӭo~p+wNbކų<2'cƭ|'YX1ƅYx;]Fo1Yz\ƒA< g?i“q6nq'ge>4{-˸ 6Yf#LJXաWx&eɖO]f7}24;ͣkUG.kqM#gڜԄf{.|婳NWug/˝9˽<yz ۭf:[nXge ^/٦ɺeqJ'tʃSo֌Nh2ߟ`sl/Q.kk.dz4oGbځ[}8ճyEfm;w+`.F"CzatZ3f.U)m[]iՈc !~[\[~ŮGHy4=ŧ>~3̻.V|dOQzèOo_[&BӠvTw'/n9v |`"N"M}Y]?qR`;nj6 }&];8NMўC6ʺu4;m6X .^?y["O}7)pqݻ3)Vף/(Yi$f}h:QEUjt;$7ieF˞/yXe5m@Zw֜oq̢]hGk^/{tn"-M58iq4(~CFYKN#f:u(зTJ85)~1FA}9ڢiNVǼݰt+KMՌp@svksu o,htY|^i]ud{!mC[V8`ԱZwXx7RbmВwWr&oM?FًnG{2;w0q+}y wԜ}[T uۀNswlX[3ߡ[Uu+mLJ:YY1-673ePv-JWw=a:Ž ?$|n~X+Ŧo\¢ne-r. tcL1F;էx{ՀkmO7eK>5([4-~MgCN0-Yazt:|̲o#~\;KiKҟ| ݚYtD]Ϧ7[jCV?_\ɦO=lЃc ]hGܙ$7JC w-,w]4M u9On2G6=&9سL7su)R69]kukFf܄C>Y}*}>kgy齚ի@KeD.;$>mVĠ.f,K5i6Lk)}F/)Ó,L*V?;W.K_&|8)-#NGWO?env|✼sQ 6;p;k]U٩\[Npl+ٖ2nXc;y ZU!}@;}dߧv<],Pص-RGfϬY).8r&ʸZm& #M1-/gY.th@inr5]v6\nf gjjqmuypȼc@fΌx+n;|mT1{1lp1gc^Iw:qW}9w$øjuBc¦Y?LZ;,ęYeMh3y9°]Ԗk5EjX؍~<[r -"H?UL^]6:'%CXF;iO4w{їouh;meo\ Y`hF4nd#UkO.pQt]\][dx{?e[lΥ^3m72Qճׇz8u쒫^^A=jqݻ[-޽3bnէݚsG74d)J)nO3L쪑䈋I7֝9 *?21MoZ93 +>ezB,F:Nm&.}f _G];FI6}yݑ]=s-XqÈ#?Ӽqm{w[l e0o\tKxWxвV!v5^d%IjFoםch#iN]9g a{tf,?Xڀ->AfݓL4J\aڑww1hMuҳ:{&=̽ث-_95_iLׁ1 &7XgHzIlـ%WN4ǛhΣ bu2wۭ@ާoPlⷎyU9+ّg;@N+^;]]K:!fQ=߫\9&k{[!|l_рM7t`vna ! 6j8׉S=&VU!2L:_*=4Bm}ۜ4}_Z?n-w*Ϙ{Cjlbo;KenenTc {s+6woAc͞a;fiq̗7_=rwpI9:uOj׼3=5d^:d] U #CrNgei>bAk󂬋onthS 1kd:;n񨳣9]^=ɀf_Z֎kٛ 1fT&v1TV#=|thnGC%)0KmΚGk>3CCr'wwS>`CoaD]Fzϫ~zjߞ!hGқ/KoSV+u`Ω+NmgTm1yN^<ී?Kosmh' 6kW4h^GeynyjZ6jt۸~-\}ƦLj ܴ)n2Xb^,dnΓ*xܣʼn3;iF ߢ|ґa![[7x ]}KN2q'(fu;i26 ,(g'2<;o+&0hgIQBIKeeZ<{E/]#8Q3ɡWjZ;W8mGigծR?xǸ֙ڷMQF V3Wv7ixvܸ݊;V~#ǭiU.oP~c7%OO^? +//>k̰R:o=xS3VX{grqcwQu@/(uVض3U%-Uu06>{e##Lv˟$w~2^`Reߋza-k-Yse==woߧtY~m[n&_+00tV.`NxԹr 2kf֮CX'ћhHk΁Zܴ5GiR{_'-dlSv;-=u4dw鷵ˌn]Lm= K[,55?:ܗ Gq0mTp$%>jרR5_02U\8et%r=wV%w南_L9G=1I^MF}t94g&Jx%z:ҕL㸌_a^yVs6TD{72;Wu\7}S?yM{#oI-w誴+Fݖ3C ׮Kyl So&<{UőS%cYso=ҞXvcE=7J{D;渞?[dߍc>?{ջHlw&$=iyuMձw|svgJ7n;i}eFZS?󝋻F5 nzr@|k,cfBIͺZ;H3vd9o^NBU]+E/ڏ(2!u#%4ۮYldyM'O 7$onGUFܩUuޥcG!Σާ _%ޤcMw vzKe/^sc.{LsG)z6'ǚ#`λ4"k߼ڲ5$ A%I IR˕ IIM JP@@ 6 H5LswܪyoU~?NwzɥKF[xHN'Gs53WLSܩ5xʷ:W'\WZYb.1&E k1] d"-͝O0lB^') pX7';7|v<;6@̛ʅlE"IG+7fRW5`DK.'N.]ȋjįQB2>y <5̆{݄Ìe\r*Ye>dA*3w :5ZX;LȲ|۟ RT 1e  ۷ 1+PyHBj]7 }ԖQݏ#rָ-O[*bNr>9v#MHC|!Xh>G؛@yr~?TPL\0̑aj&eP֚*>'M\YN1JjaDTTGCծӼ[Uzt6U>d È,:+ IwH- q6).6tOfm1dR9NFaf@WZ0W 1E t$*K o`a:=_Ycl{g<>9q#[ XfbIQ,s* [5oק& z[7HT}CV9晷q7팞M !ttȠVaӧ\yf3Pey8?зڊg8ުD{t$'mc"3{X(+qd9Y>eQl}ZBrR-l}cQإn3R˲`=B`gU(~(5=PkEf+Мܮly0/(?6Ojl>lZ^%>_|^wpu~Pb'ACDS9ok~j5'[e:~x-Z"h`%MB]+ KdΕq/nawƻh_#CD)uqQ+/aeP+Fk55x^Vw7onoK)CJθ.<Dmx<( `ߩvҦFX~H&Ze8fwۨ"{YiAQWyIBEՏd$EUC%Ә5E24Fk>c֥dM`/!wʾM8\;t b%1%-E}Ij<_ &VHzB#I{=eb0DY} L czS33=Fm쇨HhٔX4t.}_\g%kmu_pi1 &5wLM 2:]]٬5Rk'KG/e_(XVu]S8W'Kw"GCEKEЫc$#Brs arfMֽX ##,l5cDD)i@? LQ͈1Q[/6-:BU|\>nGd=:_lix54+BJ),j2S&A;=LFZ% [<nGyrE07SU!$<'^`Dx#-SgJq0Wo—q*r#_bක!U+v,ssblNw<}GG[\)+OoĮ 4İ\4m2+[h?eָNJis  #,sK HegY2iIP _)vg!@aX c 6]_?l4އO kXc 8Dgxw߲eodaO0x,z6:[H0 3aYEc} 6 4L>EE_(A z [ُ ςtbpv4I<ݗEl9_eĀ -9x4"߱lK(ƄUwAY7;YҢw.Ѹ5u^g\"=7u: *)*}|v[ol23-FflZ+kert-1Pچ{*XB`eTZ t&i&k1ߓnTt&<䘫#.z055B/ku_5,Q hTSڭڡ4\5'O߬{1/7]gEVpc^.f(\8s! 3|ZzW9måyIffW3%k=8*]/`p"a+}t L٦/ULp+`&N>l(Iݲҁiޮ%o^|[L/h\_׼ȭ$iU4$%NfӇӭKgUP`N2ޫ El [~vػxq^NcwF?耛!$QM&+7~Tx'eSY)I{fT!FrZjE)s/0ݒ7=ոQrkr|dE.s"@ĝzxg#kP%`-A"@5\mx?Ƒ15U uk@ɇKˁ$}OփB+qo=16t6E?xe%zkjHT]tZbOkHgݤ,}'s rno'|0̢A'Y&k=FˉxaAc"Y1%c73XDt$.j7Q}L)5Sb*4T6}f܌D1U%_,pr3i4hzq!g,6b9s?4Ufh yS0Uf`%g`42-H4cd% 20@<9@E@Z` -@q:\2@_MÁϦ@60 Y@}t@/7@4Z7^@ 6@_p;'4c#~@ ;Z \rhM)khmC jbY9@OQF.zr3{2/V+zWǘ+~ů+g92webfakes/tests/testthat/fixtures/output/0000755000176200001440000000000014172041777020266 5ustar liggesuserswebfakes/tests/testthat/fixtures/output/webfakes_request.txt0000644000176200001440000000151714506044225024362 0ustar liggesusers> req method: get url: http://127.0.0.1:3000/ client: 127.0.0.1 query: headers: Host: 127.0.0.1:3000 User-Agent: It is me, libcurl Accept: */* Accept-Encoding: deflate, gzip fields and methods: app # the webfakes_app the request belongs to headers # HTTP request headers hostname # server hostname, the Host header method # HTTP method of request (lowercase) path # server path protocol # http or https query_string # raw query string without '?' query # named list of query parameters remote_addr # IP address of the client url # full URL of the request get_header(field) # get a request header # see ?webfakes_request for details webfakes/tests/testthat/fixtures/output/webfakes_app.txt0000644000176200001440000000114514506044225023447 0ustar liggesusers> app routes: use * # add etag get /api fields and methods: all(path, ...) # add route for *all* HTTP methods delete(path, ...) # add route for DELETE engine(ext, engine) # add template engine for file extension head(path, ...) # add route for HEAD listen(port) # start web app on port patch(path, ...) # add route for PATCH post(path, ...) # add route for POST put(path, ...) # add route for PUT use(...) # add middleware locals # app-wide shared data # see ?webfakes_app for all methods webfakes/tests/testthat/fixtures/output/webfakes_response.txt0000644000176200001440000000151114506044225024522 0ustar liggesusers> res fields and methods: app # the webfakes_app the response belongs to locals # response-wide shared data get_header(field) # query response header on_response(fun) # call handler function for complete response redirect(path, status) # send redirect response render(view, locals) # render template send(body) # send text or raw data send_file(path, root) # send a file (automatic Content-Type) send_json(object, text, ...) # send JSON data send_status(status) # send HTTP status and empty body set_header(field, value) # set a response header set_status(status) # set response status code set_type(type) # set Content-Type # see ?webfakes_response for details webfakes/tests/testthat/fixtures/output/webfakes_regexp.txt0000644000176200001440000000007514506044225024162 0ustar liggesusers> new_regexp("^(foo|bar)$") "^(foo|bar)$" webfakes/tests/testthat/fixtures/output/webfakes_app_process.txt0000644000176200001440000000101614506044226025203 0ustar liggesusers> proc state: not running auto_start: TRUE process id: none http url: NA fields and methods: get_app() # get the app object get_port() # query port of the app get_state() # query web server process state local_env(envvars) # set temporary environment variables start() # start the app url(path, query) # query url for an api path stop() # stop web server process # see ?webfakes_app_process for details webfakes/tests/testthat/fixtures/static/0000755000176200001440000000000014172041777020215 5ustar liggesuserswebfakes/tests/testthat/fixtures/static/subdir/0000755000176200001440000000000014172041777021505 5ustar liggesuserswebfakes/tests/testthat/fixtures/static/subdir/static.json0000644000176200001440000000002114172041777023660 0ustar liggesusers{ "foo": "bar" } webfakes/tests/testthat/fixtures/static/static.html0000644000176200001440000000006414172041777022372 0ustar liggesusersHello world! webfakes/tests/testthat/fixtures/static2/0000755000176200001440000000000014172041777020277 5ustar liggesuserswebfakes/tests/testthat/fixtures/static2/static.tar.gz0000644000176200001440000000026614172041777022721 0ustar liggesusers0^K 0]E܀Mx;)eH⬈CK8!cpRpj+VϙqIXbcI2TJf65zs*gWC5|=K+D>|wHello world! webfakes/tests/testthat/test-base64.R0000644000176200001440000000103614172041777017241 0ustar liggesusers test_that("base64_decode", { decode_tests <- c( 'YWE=' = 'aa', ' YWE=' = 'aa', 'Y WE=' = 'aa', 'YWE= ' = 'aa', "Y\nW\r\nE=" = 'aa', 'YWE=====' = 'aa', # extra padding 'YWE' = 'aa', # missing padding 'YWFh====' = 'aaa', 'YQ' = 'a', 'Y' = '', 'x==' = '' ) for (i in seq_along(decode_tests)) { encoded <- names(decode_tests)[[i]] expected <- decode_tests[[i]] decoded <- base64_decode(encoded) expect_equal(decoded, expected) } }) webfakes/tests/testthat/test-app-process.R0000644000176200001440000000374414172041777020421 0ustar liggesusers test_that("error if cannot start", { # does not start before the timeout app <- new_app() app$listen <- function(...) Sys.sleep(1) expect_error( new_app_process(app, process_timeout = 100, start = TRUE), "webfakes app subprocess did not start" ) # errors before/while starting app <- new_app() app$listen <- function(...) stop("oops") expect_error( new_app_process(app, start = TRUE), class = "callr_status_error", "failed to start webfakes app process.*oops" ) # sends a different message first app <- new_app() app$listen <- function(...) "foobar" expect_error( new_app_process(app, start = TRUE), "Unexpected message from webfakes app subprocess" ) }) test_that("get_state", { app <- new_app() on.exit(proc$stop(), add = TRUE) proc <- new_app_process(app, start = TRUE) expect_equal(proc$get_state(), "live") proc$.process$kill() expect_equal(proc$get_state(), "dead") expect_output(proc$stop(), "webfakes process dead") expect_equal(proc$get_state(), "not running") }) test_that("env vars", { app <- new_app() on.exit(proc$stop(), add = TRUE) withr::local_envvar(list(FOO = "foo")) withr::local_envvar(list(BAR = NA_character_)) proc <- new_app_process(app, start = TRUE) proc$local_env(list(FOO = "bar")) proc$local_env(list(BAR = "{url}")) expect_equal(Sys.getenv("FOO", ""), "bar") expect_equal(Sys.getenv("BAR"), proc$url()) proc$stop() expect_equal(Sys.getenv("FOO", ""), "foo") expect_equal(Sys.getenv("BAR", ""), "") }) test_that("env vars 2", { app <- new_app() on.exit(proc$stop(), add = TRUE) withr::local_envvar(list(FOO = "foo")) withr::local_envvar(list(BAR = NA_character_)) proc <- new_app_process(app) proc$local_env(list(FOO = "bar")) proc$local_env(list(BAR = "{url}")) proc$start() expect_equal(Sys.getenv("FOO", ""), "bar") expect_equal(Sys.getenv("BAR"), proc$url()) proc$stop() expect_equal(Sys.getenv("FOO", ""), "foo") expect_equal(Sys.getenv("BAR", ""), "") }) webfakes/tests/testthat/test-print.R0000644000176200001440000000373014172041777017314 0ustar liggesusers app <- new_app() app$use(function(req, res) { tmp <- tempfile() saveRDS(list(req = req, res = res), file = tmp) res$ set_status(200)$ send(normalizePath(tmp)) }) proc <- local_app_process(app) withr::local_options(list(HTTPUserAgent = "It is me, libcurl")) resp <- curl::curl_fetch_memory(proc$url()) tmp <- rawToChar(resp$content) withr::defer(unlink(tmp)) # Verify_output uses a png() graphics device, and fails if there is # no png() support. So we skip theses tests then. capabilities() # is very slow on macOS, because it starts up X11, so we'll just assume # that macOS has a png device. skip_without_png_device <- function() { if (.Platform$OS.type == "windows") return() if (! capabilities("png") || ! capabilities("X11")) { skip("Needs a PNG device") } } test_that("webfakes_app", { skip_without_png_device() app <- new_app() app$use("add etag" = mw_etag()) app$get("/api", function(req, res) res$send("foobar")) verify_output( test_path("fixtures", "output", "webfakes_app.txt"), app ) }) test_that("webfakes_request", { skip_without_png_device() req <- readRDS(tmp)$req req$url <- "http://127.0.0.1:3000/" req$headers$Host <- "127.0.0.1:3000" req$headers$`Accept-Encoding` <- "deflate, gzip" verify_output( test_path("fixtures", "output", "webfakes_request.txt"), req ) }) test_that("webfakes_response", { skip_without_png_device() res <- readRDS(tmp)$res verify_output( test_path("fixtures", "output", "webfakes_response.txt"), res ) }) test_that("webfakes_regexp", { skip_without_png_device() verify_output( test_path("fixtures", "output", "webfakes_regexp.txt"), new_regexp("^(foo|bar)$") ) }) test_that("webfakes_app_process", { skip_without_png_device() app <- new_app() proc <- new_app_process(app, start = TRUE) proc$stop() # make the output deterministic proc$.port <- 3000 verify_output( test_path("fixtures", "output", "webfakes_app_process.txt"), proc ) }) webfakes/tests/testthat/test-git-app.R0000644000176200001440000000061714515732651017521 0ustar liggesuserstest_that("git_app", { skip_on_cran() dir.create(tmp <- tempfile()) on.exit(unlink(tmp, recursive = TRUE), add = TRUE) untar(testthat::test_path("fixtures/git-repo.tar.gz"), exdir = tmp) app <- git_app(file.path(tmp, "repo")) git <- webfakes::local_app_process(app) expect_snapshot( system( paste("git ls-remote", git$url("/pak-test.git")), intern = TRUE ) ) }) webfakes/tests/testthat/test-mw-urlencoded.R0000644000176200001440000000141714172041777020725 0ustar liggesusers app <- new_app() app$use(mw_urlencoded()) app$post("/form", function(req, res) { ret <- list( form = req$form ) res$send_json(ret, pretty = TRUE, auto_unbox = TRUE) }) web <- local_app_process(app) test_that("mw-urlencoded", { url <- web$url("/form") handle <- curl::new_handle() data <- charToRaw("foo=bar&foobar=100") curl::handle_setheaders( handle, "content-type" = "application/x-www-form-urlencoded" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON( rawToChar(resp$content), simplifyVector = FALSE ) expect_equal(echo, list(form = list("foo" = "bar", "foobar" = "100"))) }) webfakes/tests/testthat/test-request.R0000644000176200001440000000024314427150365017641 0ustar liggesusers test_that("parse_query", { expect_snapshot({ parse_query("foo") parse_query("?foo") parse_query("?foo&bar") parse_query("?foo&bar=baz") }) }) webfakes/tests/testthat/setup.R0000644000176200001440000000013514172041777016337 0ustar liggesusers httpbin2 <- local_app_process( httpbin_app(), .local_envir = testthat::teardown_env() ) webfakes/tests/testthat/test-mw-raw.R0000644000176200001440000000215314172041777017370 0ustar liggesusers app <- new_app() app$use(mw_raw()) app$post("/raw", function(req, res) { res$ set_type("application/octet-stream")$ send(req$raw) }) web <- local_app_process(app) test_that("raw body parser", { url <- web$url("/raw") data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/octet-stream") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(data, resp$content) }) test_that("non-matching content-type", { url <- web$url("/raw") data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/json") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 404L) }) webfakes/tests/testthat/test-tmpl-glue.R0000644000176200001440000000076514172041777020073 0ustar liggesusers app <- new_app() app$engine("html", tmpl_glue()) app$set_config("views", test_path("fixtures", "views")) app$get("/hello/:user", function(req, res) { locals <- c(as.list(req$params), greeting = "hello") html <- res$render("test-view", locals = locals) res$ set_type("text/html")$ send(html) }) web <- local_app_process(app) test_that("glue templating", { url <- web$url("/hello/gabor") resp <- curl::curl_fetch_memory(url) expect_match(rawToChar(resp$content), "hello gabor") }) webfakes/tests/testthat/teardown.R0000644000176200001440000000002614172041777017021 0ustar liggesusers try(httpbin2$stop()) webfakes/tests/testthat/test-mw-multipart.R0000644000176200001440000000155214172041777020622 0ustar liggesusers app <- new_app() app$use(mw_multipart()) app$put("/form", function(req, res) { ret = list(form = req$form, files = req$files) res$send_json(ret, pretty = TRUE, auto_unbox = TRUE) }) web <- local_app_process(app) test_that("mw_multipart", { on.exit(rm(tmp), add = TRUE) tmp <- tempfile() writeBin(charToRaw("foobar\n"), con = tmp) url <- web$url("/form") handle <- curl::new_handle() curl::handle_setopt(handle, customrequest = "PUT") curl::handle_setform( handle, a = "1", b = "2", c = curl::form_file(tmp, type = "text/plain") ) resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo$form, list(a = "1", b = "2")) expect_equal( echo$files$c, list( filename = basename(tmp), value = base64_encode("foobar\n") ) ) }) webfakes/tests/testthat/test-mw-static.R0000644000176200001440000000474214310055034020055 0ustar liggesusers app <- new_app() app$use(mw_etag()) app$use(mw_static(root = test_path("fixtures", "static"))) set_headers <- function(req, res) { res$set_header("foo", "bar") } app$use(mw_static(root = test_path("fixtures", "static2"), set_headers = set_headers)) app$get("/static.html", function(req, res) { res$send("this is never reached") }) app$get("/fallback", function(req, res) { res$send("this is the fallback") }) web <- local_app_process(app) test_that("static file", { url <- web$url("/static.html") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) # type is set from the file extension expect_equal(resp$type, "text/html") expect_equal( rawToChar(resp$content), read_char(test_path("fixtures", "static", "static.html")) ) }) test_that("static file in subdirectory", { url <- web$url("/subdir/static.json") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) # type is set from the file extension expect_equal(resp$type, "application/json") expect_equal( rawToChar(resp$content), read_char(test_path("fixtures", "static", "subdir", "static.json")) ) }) test_that("file not found", { url <- web$url("/notfound") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("file not found falls back", { url <- web$url("/fallback") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "text/plain") expect_equal( rawToChar(resp$content), "this is the fallback" ) }) test_that("directory is 404", { url <- web$url("/subdir") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("set_headers callback", { url <- web$url("/static.tar.gz") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/gzip") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$foo, "bar") expect_equal( resp$content, read_bin(test_path("fixtures", "static2", "static.tar.gz")) ) }) test_that("if-none-match is respected", { url <- web$url("/static.html") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) etag <- curl::parse_headers_list(resp$headers)$etag h <- curl::new_handle() curl::handle_setheaders(h, "If-None-Match" = etag) resp2 <- curl::curl_fetch_memory(url, handle = h) expect_equal(resp2$status_code, 304L) expect_equal(resp2$content, raw(0)) }) webfakes/tests/testthat/test-http-methods.R0000644000176200001440000000336614172041777020605 0ustar liggesusers test_that("get", { url <- httpbin$url("/get") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(data$path, "/get") }) test_that("post", { url <- httpbin$url("/post") data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "Content-Type" = "application/json") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal( data$json, list(foo = "bar", foobar = 1:3) ) expect_equal(data$path, "/post") }) test_methods <- c("connect", "delete", "head", "mkcol", "options", "patch", "propfind", "put", "report") app <- new_app() handler <- function(req, res) res$send_json(list(method = req$method)) for (method in test_methods) app[[method]](paste0("/", method), handler) web2 <- local_app_process(app, port = NA) test_that("the rest", { for (method in test_methods) { url <- web2$url(paste0("/", method)) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/json") curl::handle_setopt(handle, customrequest = toupper(method)) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status, 200) if (method == "head") { expect_equal(length(resp$content), 0) } else { echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(echo$method, method) } } }) webfakes/tests/testthat/test-response.R0000644000176200001440000000462414172041777020021 0ustar liggesusers web <- local_app_process(test_response_app()) test_that("response locals", { url <- web$url("/local") resp <- curl::curl_fetch_memory(url) expect_equal(rawToChar(resp$content), "foo bar") }) # on_response is tested via mw_log() test_that("render", { # if the template or engine does not exist url <- web$url("/badengine") resp <- curl::curl_fetch_memory(url) expect_match( rawToChar(resp$content), "Cannot find template engine for view" ) }) test_that("send_json", { url <- web$url("/badjson") resp <- curl::curl_fetch_memory(url) expect_match( rawToChar(resp$content), "Specify only one of" ) }) test_that("send_file", { url <- web$url("/file") resp <- curl::curl_fetch_memory(url) path <- system.file( package = "webfakes", "examples", "static", "public", "foo", "bar.json" ) expect_equal(resp$content, read_bin(path)) }) test_that("set_type", { url <- web$url("/type") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`content-type`, "application/json") }) test_that("write", { url <- web$url("/write") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal(rawToChar(resp$content), "hello world!") # header can be set url <- web$url("/write-header") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal(rawToChar(resp$content), "hello world!") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`foo`, "bar") }) test_that("write-wait", { url <- web$url("/write-wait") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal(rawToChar(resp$content), "hello world!") }) test_that("send_chunk", { url <- web$url("/send-chunk") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal( rawToChar(resp$content), "first chunk\nsecond chunk\nthird and final chunk\n" ) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers[["content-type"]], "text/plain") expect_equal(headers[["transfer-encoding"]], "chunked") }) test_that("add_header", { url <- web$url("/add-header") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal( headers[names(headers) == "foo"], list(foo = "bar", foo = "bar2") ) expect_equal(headers[["foobar"]], "baz") }) webfakes/tests/testthat/helper.R0000644000176200001440000000434414427165752016467 0ustar liggesusers test_response_app <- function() { app <- new_app() `%||%` <- function(l, r) if (is.null(l)) r else l app$locals$applocal <- "foo" app$engine("txt", tmpl_glue()) app$get("/local", function(req, res) { res$locals$reslocal <- "bar" "next" }) app$get("/local", function(req, res) { res$send(paste(res$locals$applocal, res$locals$reslocal)) }) app$get("/badengine", function(req, res) { txt <- res$render("foobar") res$send(txt) }) app$get("/badjson", function(req, res) { res$send_json(1:3, text = "foo") }) app$get("/file", function(req, res) { res$send_file( root = system.file(package = "webfakes"), file.path("examples", "static", "public", "foo", "bar.json") ) }) app$get("/type", function(req, res) { res$set_type("json") res$send("{ \"foo\": 1 }") }) app$get("/write", function(req, res) { res$ write("hello ")$ write("world!") }) app$get("/write-header", function(req, res) { res$ set_header("foo", "bar")$ write("hello ")$ write("world!") }) app$get("/write-wait", function(req, res) { res$locals$turn <- (res$locals$turn %||% 0) + 1L if (res$locals$turn == 1) { res$ set_header("content-length", nchar("hello world!"))$ write("hell")$ delay(0.01) } else if (res$locals$turn == 2) { res$ write("o world")$ delay(0.01) } else { res$send("!") } }) app$get("/send-chunk", function(req, res) { res$locals$turn <- (res$locals$turn %||% 0) + 1L if (res$locals$turn == 1) { res$ set_header("Content-Type", "text/plain")$ send_chunk("first chunk\n")$ delay(0.01) } else if (res$locals$turn == 2) { res$ send_chunk("second chunk\n")$ delay(0.01) } else { res$send_chunk("third and final chunk\n") } }) app$get("/add-header", function(req, res) { res$add_header("foo", "bar") res$add_header("foo", "bar2") res$add_header("foobar", "baz") res$send("ready") }) app } httpbin <- local_app_process(httpbin_app()) httpbin$local_env(c(FOO = "{url}xxx")) r_variant <- function() { if (getRversion() < "4.2.0") { "old-r" } else { "new-r" } } webfakes/tests/testthat/test-mw-etag.R0000644000176200001440000000252014172041777017515 0ustar liggesusers app <- new_app()$ use(mw_etag())$ get("/txt", function(req, res) { res$ set_type("text/plain")$ send("textual") })$ get("/txt-empty", function(req, res) { res$ set_type("text/plain")$ send("") })$ get("/raw", function(req, res) { res$ set_type("applicartion/octet-stream")$ send(charToRaw("textual")) })$ get("/raw-empty", function(req, res) { res$ set_type("applicartion/octet-stream")$ send(raw(0)) }) web <- local_app_process(app) test_that("text/plain response", { url <- web$url("/txt") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_true("etag" %in% names(headers)) expect_match(headers$etag, "\"[-a-zA-Z0-9]+\"") }) test_that("raw response", { txt <- curl::curl_fetch_memory(web$url("/txt")) raw <- curl::curl_fetch_memory(web$url("/raw")) htxt <- curl::parse_headers_list(txt$headers) hraw <- curl::parse_headers_list(raw$headers) expect_equal(htxt$etag, hraw$etag) }) test_that("etag for empty response", { txt <- curl::curl_fetch_memory(web$url("/txt-empty")) raw <- curl::curl_fetch_memory(web$url("/raw-empty")) htxt <- curl::parse_headers_list(txt$headers) hraw <- curl::parse_headers_list(raw$headers) expect_equal(htxt$etag, "\"00000000\"") expect_equal(htxt$etag, hraw$etag) }) webfakes/tests/testthat/_snaps/0000755000176200001440000000000014515732560016336 5ustar liggesuserswebfakes/tests/testthat/_snaps/new-r/0000755000176200001440000000000014427165776017401 5ustar liggesuserswebfakes/tests/testthat/_snaps/new-r/httpbin.md0000644000176200001440000000117614506044176021364 0ustar liggesusers# /cookies/set/:name/:value Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE Inf foo bar # /cookies/set Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE Inf foo bar 2 127.0.0.1 FALSE / FALSE Inf bar baz # /cookies/delete Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE Inf bar baz webfakes/tests/testthat/_snaps/request.md0000644000176200001440000000062014506044227020342 0ustar liggesusers# parse_query Code parse_query("foo") Output $foo [1] "" Code parse_query("?foo") Output $foo [1] "" Code parse_query("?foo&bar") Output $foo [1] "" $bar [1] "" Code parse_query("?foo&bar=baz") Output $foo [1] "" $bar [1] "baz" webfakes/tests/testthat/_snaps/mw-range-parser.md0000644000176200001440000000145314506044203021660 0ustar liggesusers# parse_range Code parse_range("foobar=1-100") Output NULL Code parse_range("bytes=0-100, 50-150") Output NULL Code parse_range("bytes=200-100") Output NULL Code parse_range("bytes=x-100") Output NULL Code parse_range("bytes=1-100") Output [,1] [,2] [1,] 1 100 Code parse_range("bytes=1-") Output [,1] [,2] [1,] 1 Inf Code parse_range("bytes=-100") Output [,1] [,2] [1,] 0 -100 Code parse_range("bytes=0-100, 200-") Output [,1] [,2] [1,] 0 100 [2,] 200 Inf Code parse_range("bytes=200-300, 0-100") Output [,1] [,2] [1,] 0 100 [2,] 200 300 webfakes/tests/testthat/_snaps/git-app.md0000644000176200001440000000061614515732660020225 0ustar liggesusers# git_app Code system(paste("git ls-remote", git$url("/pak-test.git")), intern = TRUE) Output [1] "3f3b0b4ee8a0ff4563073924e5fe069da67a6d8b\tHEAD" [2] "3f3b0b4ee8a0ff4563073924e5fe069da67a6d8b\trefs/heads/main" [3] "cefdc0eebcd7f757efb9a80652fd8aaf1a87508e\trefs/heads/subdir" [4] "cefdc0eebcd7f757efb9a80652fd8aaf1a87508e\trefs/tags/v1" webfakes/tests/testthat/_snaps/old-r/0000755000176200001440000000000014427165754017362 5ustar liggesuserswebfakes/tests/testthat/_snaps/old-r/httpbin.md0000644000176200001440000000117614427165754021361 0ustar liggesusers# /cookies/set/:name/:value Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE foo bar # /cookies/set Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE foo bar 2 127.0.0.1 FALSE / FALSE bar baz # /cookies/delete Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE bar baz webfakes/tests/testthat/test-httpbin.R0000644000176200001440000006510614506043703017625 0ustar liggesusers # HTTP methods ========================================================= test_that("/get", { url <- httpbin$url("/get", query = c(q1 = "one", q2 = "two")) handle <- curl::new_handle() curl::handle_setheaders(handle, "foo" = "bar") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(data$path, "/get") expect_equal(data$headers$foo, "bar") expect_equal(data$args, list(q1 = "one", q2 = "two")) }) test_that("/post", { url <- httpbin$url("/post", query = c(q1 = "one", q2 = "two")) data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders( handle, "content-type" = "application/json", foo = "bar" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data$path, "/post") expect_equal(data$headers$foo, "bar") expect_equal(data$args, list(q1 = "one", q2 = "two")) expect_equal( data$json, list(foo = "bar", foobar = 1:3) ) expect_equal( jsonlite::fromJSON(data$data, simplifyVector = TRUE), data$json ) }) test_that("/post and multipart data", { on.exit(rm(tmp), add = TRUE) tmp <- tempfile() writeBin(charToRaw("foobar\n"), con = tmp) url <- httpbin$url("/post") handle <- curl::new_handle() curl::handle_setopt(handle, customrequest = "POST") curl::handle_setform( handle, a = "1", b = "2", c = curl::form_file(tmp, type = "application/octet-stream") ) resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(resp$status_code, 200L) expect_equal(echo$form, list(a = "1", b = "2")) expect_equal( echo$files$c, list( filename = basename(tmp), value = paste0( "data:application/octet-stream;base64,", base64_encode("foobar\n") ) ) ) }) # Auth ================================================================= test_that("/basic-auth", { # no auth supplied url <- httpbin$url("/basic-auth/Aladdin/OpenSesame") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 401L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "Basic realm=\"Fake Realm\"") # correct auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic QWxhZGRpbjpPcGVuU2VzYW1l" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") expect_equal( jsonlite::fromJSON(rawToChar(resp$content)), list(authenticated = TRUE, user = "Aladdin") ) # wrong auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic NOLUCK" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 401L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "Basic realm=\"Fake Realm\"") }) test_that("/hidden-basic-auth", { # no auth supplied url <- httpbin$url("/hidden-basic-auth/Aladdin/OpenSesame") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, NULL) # correct auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic QWxhZGRpbjpPcGVuU2VzYW1l" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") expect_equal( jsonlite::fromJSON(rawToChar(resp$content)), list(authenticated = TRUE, user = "Aladdin") ) # wrong auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic NOLUCK" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 404L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, NULL) }) test_that("/digest-auth", { # url <- "http://localhost:3000/digest-auth/auth/user/secret" url <- httpbin$url("/digest-auth/auth/user/secret") handle <- curl::new_handle() resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl::parse_headers_list(resp$headers) expect_true("www-authenticate" %in% names(headers)) creds <- parse_authorization_header(headers$`www-authenticate`) expect_equal(creds$scheme, "digest") expect_equal(creds$realm, "webfakes.r-lib.org") expect_equal(creds$qop, "auth") expect_equal(creds$algorithm, "MD5") expect_equal(creds$stale, "FALSE") auth <- list( username = "user", realm = "webfakes.r-lib.org", nonce = creds$nonce, uri = "/digest-auth/auth/user/secret", qop = "auth", nc = "00000001", cnonce = "0a4f113b", opaque = creds$opaque ) hash <- function(x) digest::digest(x, algo = "md5", serialize = FALSE) HA1 <- hash(paste(c(auth$username, auth$realm, "secret"), collapse = ":")) HA2 <- hash("GET:/digest-auth/auth/user/secret") auth$response <- hash(paste0( collapse = ":", c(HA1, auth$nonce, auth$nc, auth$cnonce, auth$qop, HA2) )) authorize <- paste0( "Digest ", paste0(names(auth), "=", auth, collapse = ", ") ) curl::handle_setheaders(handle, authorization = authorize) resp2 <- curl::curl_fetch_memory(handle = handle, url) expect_equal(resp2$status_code, 200L) cnt <- jsonlite::fromJSON(rawToChar(resp2$content), simplifyVector = TRUE) expect_equal(cnt, list(authentication = TRUE, user = "user")) }) test_that("/bearer", { # no auth url <- httpbin$url("/bearer") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 401L) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "bearer") # bad auth format handle <- curl::new_handle() curl::handle_setheaders(handle, authorization = "foobar") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 401L) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "bearer") # correct format handle <- curl::new_handle() curl::handle_setheaders(handle, authorization = "Bearer secret") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo, list(authenticated = TRUE, token = "secret")) }) # Status codes ========================================================= test_that("/status", { codes <- c(200, 301, 401, 404) # get for (code in codes) { url <- httpbin$url(paste0("/status/", code)) resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, code) } # post data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/json") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) for (code in codes) { url <- httpbin$url(paste0("/status/", code)) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, code) } }) # Request inspection =================================================== test_that("/headers", { url <- httpbin$url("/headers") handle <- curl::new_handle() curl::handle_setheaders(handle, "header1" = "this", "header2" = "that") resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo$headers$header1, "this") expect_equal(echo$headers$header2, "that") }) test_that("/ip", { url <- httpbin$url("/ip") resp <- curl::curl_fetch_memory(url) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo, list(origin = "127.0.0.1")) }) test_that("/user-agent", { url <- httpbin$url("/user-agent") ua <- "i am libcurl, that is my name" withr::local_options(list(HTTPUserAgent = ua)) resp <- curl::curl_fetch_memory(url) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo, list("user-agent" = ua)) }) # Response inspection ================================================== test_that("/etag", { url <- httpbin$url("/etag/foobar") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers$etag, "foobar") handle <- curl::new_handle() curl::handle_setheaders(handle, "If-None-Match" = "\"foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 304) expect_true(length(resp$content) == 0) handle <- curl::new_handle() curl::handle_setheaders(handle, "If-None-Match" = "\"not-foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200) expect_true(length(resp$content) > 0) handle <- curl::new_handle() curl::handle_setheaders(handle, "If-Match" = "\"foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200) expect_true(length(resp$content) > 0) handle <- curl::new_handle() curl::handle_setheaders(handle, "If-Match" = "\"not-foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 412) }) test_that("/response-headers", { url <- httpbin$url("/response-headers") resp <- curl::curl_fetch_memory(url) expect_equal(rawToChar(resp$content), "{}") url2 <- httpbin$url("/response-headers", c(foo = "bar")) resp2 <- curl::curl_fetch_memory(url2) expect_equal( jsonlite::fromJSON(rawToChar(resp2$content)), list(foo = "bar") ) headers <- curl::parse_headers_list(resp2$headers) expect_equal(headers[["foo"]], "bar") url3 <- httpbin$url("/response-headers", c(foo = "bar", foobar = "baz")) resp3 <- curl::curl_fetch_memory(url3) expect_equal( jsonlite::fromJSON(rawToChar(resp3$content)), list(foo = "bar", foobar = "baz") ) headers <- curl::parse_headers_list(resp3$headers) expect_equal(headers[["foo"]], "bar") expect_equal(headers[["foobar"]], "baz") handle <- curl::new_handle() data <- charToRaw("{}") curl::handle_setheaders( handle, "content-type" = "application/json" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp31 <- curl::curl_fetch_memory(url3, handle = handle) expect_equal( jsonlite::fromJSON(rawToChar(resp31$content)), list(foo = "bar", foobar = "baz") ) headers <- curl::parse_headers_list(resp3$headers) expect_equal(headers[["foo"]], "bar") expect_equal(headers[["foobar"]], "baz") url4 <- httpbin$url("/response-headers", c(foo = "bar", foo = "bar2")) resp4 <- curl::curl_fetch_memory(url4) expect_equal( jsonlite::fromJSON(rawToChar(resp4$content)), list(foo = c("bar", "bar2")) ) headers <- curl::parse_headers_list(resp4$headers) expect_equal( headers[names(headers) == "foo"], list(foo = "bar", foo = "bar2") ) }) test_that("/cache", { url <- httpbin$url("/cache") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_true("last-modified" %in% tolower(names(headers))) handle <- curl::new_handle() curl::handle_setheaders( handle, "If-Modified-Since" = http_time_stamp(Sys.time() - as.difftime(5, units = "mins")) ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 304L) handle <- curl::new_handle() curl::handle_setheaders( handle, "If-None-Match" = "some-etag" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 304L) }) test_that("/cache/:value", { url <- httpbin$url("/cache/10") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal( headers[["cache-control"]], "public, max-age=10" ) }) # Response formats ===================================================== test_that("/deny", { url <- httpbin$url("/deny") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) path <- system.file( package = "webfakes", "examples", "httpbin", "data", "deny.txt" ) expect_equal(resp$content, read_bin(path)) }) test_that("/gzip", { # curl seems to ungzip automatically, so we use url() url <- httpbin$url("/gzip") con <- url(url, open = "rb") on.exit(close(con), add = TRUE) echo <- readBin(con, "raw", 10000) expect_equal(echo[1:2], charToRaw("\x1f\x8b")) json <- readChar(gzcon(rawConnection(echo)), 10000) obj <- jsonlite::fromJSON(json, simplifyVector = FALSE) expect_equal(obj$path, "/gzip") }) test_that("/deflate", { url <- httpbin$url("/deflate") con <- url(url, open = "rb") on.exit(close(con), add = TRUE) echo <- readBin(con, "raw", 10000) data <- jsonlite::fromJSON(rawToChar(zip::inflate(echo)$output)) expect_true(data$deflated) }) test_that("/brotli", { url <- httpbin$url("/brotli") con <- url(url, open = "rb") on.exit(close(con), add = TRUE) echo <- readBin(con, "raw", 10000) data <- jsonlite::fromJSON(rawToChar(brotli::brotli_decompress(echo))) expect_true(data$brotli) }) test_that("/encoding/utf8", { url <- httpbin$url("/encoding/utf8") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "text/html; charset=utf-8") ptrn <- as.raw(c( 0xe1, 0x8c, 0x8c, 0xe1, 0x8c, 0xa5, 0x20, 0xe1, 0x8b, 0xab, 0xe1, 0x88, 0x88, 0xe1, 0x89, 0xa4, 0xe1, 0x89, 0xb1 )) # On windows end of line is converted to \r\n expect_true(grepRaw(ptrn, resp$content, fixed = TRUE) %in% c(9085, 9233)) }) test_that("/html", { url <- httpbin$url("/html") resp <- curl::curl_fetch_memory(url) expect_match(resp$type, "^text/html") expect_match(rawToChar(resp$content), "", fixed = TRUE) }) test_that("/json", { url <- httpbin$url("/json") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/json") path <- system.file( package = "webfakes", "examples", "httpbin", "data", "example.json" ) expect_equal(resp$content, read_bin(path)) }) test_that("/robots.txt", { url <- httpbin$url("/robots.txt") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "text/plain") path <- system.file( package = "webfakes", "examples", "httpbin", "data", "robots.txt" ) expect_equal(resp$content, read_bin(path)) }) test_that("/xml", { url <- httpbin$url("/xml") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/xml") path <- system.file( package = "webfakes", "examples", "httpbin", "data", "example.xml" ) expect_equal(resp$content, read_bin(path)) }) # Dynamic data ========================================================= test_that("/base64/", { url <- httpbin$url("/base64") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) expect_equal(rawToChar(resp$content), "Everything is Rsome") value <- base64_encode("hello there!") url <- httpbin$url(paste0("/base64/", value)) resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/octet-stream") expect_equal(rawToChar(resp$content), "hello there!") }) test_that("/bytes", { url <- httpbin$url("/bytes/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) url <- httpbin$url("/bytes/1000") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/octet-stream") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers[["content-length"]], "1000") expect_equal(length(resp$content), 1000) }) test_that("/delay", { url <- httpbin$url("/delay/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) url <- httpbin$url("/delay/0.2") st <- system.time(resp <- curl::curl_fetch_memory(url)) expect_true(st[["elapsed"]] >= 0.1) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo$path, "/delay/0.2") }) test_that("/stream-bytes", { url <- httpbin$url("/stream-bytes/100") resp <- curl::curl_fetch_memory(url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers[["transfer-encoding"]], "chunked") expect_equal(length(resp$content), 100) ## seed works url2 <- httpbin$url("/stream-bytes/10", c(seed = 100)) resp2 <- curl::curl_fetch_memory(url2) expect_false(identical(resp$content, resp2$content)) resp3 <- curl::curl_fetch_memory(url2) expect_identical(resp2$content, resp3$content) ## chunk_size works url <- httpbin$url("/stream-bytes/100", c(chunk_size = 30)) resp <- curl::curl_fetch_memory(url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers[["transfer-encoding"]], "chunked") expect_equal(length(resp$content), 100) }) test_that("/range", { url <- httpbin$url("/range/100") # HEAD handle <- curl::new_handle() curl::handle_setopt(handle, customrequest = "HEAD") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") abc <- function(n) { l <- paste(letters, collapse = "") substr(strrep(l, n / nchar(l) + 1), 1, n) } # No range header handle <- curl::new_handle() resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(abc(100))) # Illegal range headers handle <- curl::new_handle() curl::handle_setheaders(handle, Range = "bytes=20-30, 25-35") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(abc(100))) # Legal header handle <- curl::new_handle() curl::handle_setheaders(handle, Range = "bytes=20-29") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 206L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(substr(abc(30), 21, 30))) # In pieces, duration is for the full response url <- httpbin$url("/range/100?chunk_size=1&duration=10") handle <- curl::new_handle() curl::handle_setheaders(handle, Range = "bytes=20-29") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 206L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(substr(abc(30), 21, 30))) expect_true(resp$times[["total"]] > 0.5) }) test_that("/uuid", { url <- httpbin$url("/uuid") resp <- curl::curl_fetch_memory(url) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_match(echo$uuid, "^[-0-9a-z]+$") expect_equal(nchar(echo$uuid), 36) }) test_that("/stream", { url <- httpbin$url("/stream/10") resp <- curl::curl_fetch_memory(url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers[["transfer-encoding"]], "chunked") lines <- strsplit(rawToChar(resp$content), "\n", fixed = TRUE)[[1]] json <- lapply(lines, jsonlite::fromJSON, simplifyVector = TRUE) expect_equal(vapply(json, "[[", integer(1), "id"), 0:9) }) # Cookies ============================================================== test_that("/cookies", { url <- httpbin$url("/cookies") handle <- curl::new_handle() curl::handle_setheaders(handle, Cookie = "foo=bar; bar=baz") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(foo = "bar", bar = "baz"))) }) test_that("/cookies/set/:name/:value", { url <- httpbin$url("/cookies/set/foo/bar") handle <- curl::new_handle() resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers(resp$headers, multiple = TRUE) expect_true("HTTP/1.1 302 Found" %in% headers[[1]]) expect_true("Set-Cookie: foo=bar; Path=/" %in% headers[[1]]) expect_true("HTTP/1.1 200 OK" %in% headers[[2]]) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(foo = "bar"))) expect_snapshot(curl::handle_cookies(handle), variant = r_variant()) }) test_that("/cookies/set", { url <- httpbin$url("/cookies/set?foo=bar&bar=baz") handle <- curl::new_handle() resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers(resp$headers, multiple = TRUE) expect_true("HTTP/1.1 302 Found" %in% headers[[1]]) expect_true("Set-Cookie: foo=bar; Path=/" %in% headers[[1]]) expect_true("Set-Cookie: bar=baz; Path=/" %in% headers[[1]]) expect_true("HTTP/1.1 200 OK" %in% headers[[2]]) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(bar="baz", foo = "bar"))) expect_snapshot(curl::handle_cookies(handle), variant = r_variant()) }) test_that("/cookies/delete", { handle <- curl::new_handle() url1 <- httpbin$url("/cookies/set?foo=bar&bar=baz") resp1 <- curl::curl_fetch_memory(url1, handle = handle) url <- httpbin$url("/cookies/delete?foo") resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers(resp$headers, multiple = TRUE) expect_true("HTTP/1.1 302 Found" %in% headers[[1]]) expect_true("Set-Cookie: foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/" %in% headers[[1]]) expect_true("HTTP/1.1 200 OK" %in% headers[[2]]) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(bar = "baz"))) expect_snapshot(curl::handle_cookies(handle), variant = r_variant()) }) # Images =============================================================== test_that("/image", { url <- httpbin$url("/image") types <- c("image/webp", "image/svg+xml", "image/jpeg", "image/png") for (type in types) { handle <- curl::new_handle() curl::handle_setheaders(handle, accept = type) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, type) } handle <- curl::new_handle() curl::handle_setheaders(handle, accept = "image/*") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "image/png") handle <- curl::new_handle() curl::handle_setheaders(handle, accept = "application/json") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 406L) exts <- c("jpeg", "png", "svg", "webp") for (ext in exts) { handle <- curl::new_handle() url <- httpbin$url(paste0("/image/", ext)) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, unname(mime_find(ext))) } }) # Redirects ============================================================ test_that("absolute-redirect", { url <- httpbin$url("/absolute-redirect/4") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers_list(resp$headers) expect_equal(resp$status_code, 302L) expect_equal(headers$location, httpbin$url("/absolute-redirect/3")) url <- httpbin$url("/absolute-redirect/2") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = TRUE) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$url, httpbin$url("/get")) url <- httpbin$url("/absolute-redirect/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("redirect", { url <- httpbin$url("/redirect/4") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers_list(resp$headers) expect_equal(resp$status_code, 302L) expect_equal(headers$location, "/redirect/3") url <- httpbin$url("/redirect/2") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = TRUE) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$url, httpbin$url("/get")) url <- httpbin$url("/redirect/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("/redirect-to", { # default status is 302 handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) url <- httpbin$url("/redirect-to", query = list(url = "/get")) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 302L) expect_equal(resp$type, "text/plain") expect_equal(curl::parse_headers_list(resp$headers)$location, "/get") expect_equal(rawToChar(resp$content), "302 Found. Redirecting to /get") # can specify status handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) url <- httpbin$url( "/redirect-to", query = list(url = "/status/200", status_code = 301) ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 301L) expect_equal(resp$type, "text/plain") expect_equal(curl::parse_headers_list(resp$headers)$location, "/status/200") expect_equal( rawToChar(resp$content), "301 Moved Permanently. Redirecting to /status/200" ) }) # Anything ============================================================= webfakes/tests/testthat/test-mime.R0000644000176200001440000000022714172041777017105 0ustar liggesusers test_that("mime_find", { expect_equal(mime_find("json"), c(json = "application/json")) expect_equal(mime_find("blahxml"), c(xml = "text/xml")) }) webfakes/tests/testthat/test-oauth.R0000644000176200001440000000777014172041777017310 0ustar liggesusers test_that("oauth2", { skip_on_cran() # Create the resource server rsapp <- local_app_process( oauth2_resource_app(), opts = server_opts(num_threads = 3) ) regi_url <- rsapp$url("/register") auth_url <- rsapp$url("/authorize") toke_url <- rsapp$url("/token") # Create the third party app server tpapp <- local_app_process( oauth2_third_party_app("3P app"), opts = server_opts(num_threads = 3) ) redi_url <- tpapp$url("/login/redirect") conf_url <- tpapp$url("/login/config") # Register the third party app at the resource server # In real life this is done by the admin of the third party app url <- paste0( regi_url, "?name=3P%20app", "&redirect_uri=", redi_url ) resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) regdata <- jsonlite::fromJSON(rawToChar(resp$content)) # Now set this data on the third party app # In real life this is included in the config of the third party app # by its admin auth_data <- jsonlite::toJSON(list( auth_url = auth_url, token_url = toke_url, client_id = regdata$client_id, client_secret = regdata$client_secret ), auto_unbox = TRUE) handle <- curl::new_handle() curl::handle_setheaders( handle, "content-type" = "application/json" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = nchar(auth_data), postfields = auth_data ) resp2 <- curl::curl_fetch_memory(conf_url, handle = handle) expect_equal(resp2$status_code, 200L) # Now everything is set up, a user can go to the login page of the # third party app: # browseURL(tpapp$url("/login")) # Scripting it is a bit tedious, because we need to parse the HTML and # submit a form. The `oauth2_login()` helper function does this. resp3 <- oauth2_login(tpapp$url("/login")) token <- jsonlite::fromJSON(rawToChar(resp3$token_response$content)) expect_equal(resp3$login_response$status_code, 200L) expect_equal(resp3$login_response$type, "text/html") expect_equal(resp3$token_response$status_code, 200L) expect_match(token$access_token, "^token-[0-9a-f]+$") }) test_that("oauth + httr", { skip_on_cran() # Not great, the OS should allocate a port, really... withr::local_envvar(c( HTTP_SERVER = "127.0.0.1", HTTP_SERVER_PORT = httpuv::randomPort() )) # Create the resource server rsappex <- oauth2_resource_app(access_duration = 0.1) log <- tempfile("webfakes-log-", fileext = ".log") on.exit(unlink(log, recursive = TRUE), add = TRUE) rsappex$use(logger = mw_log(stream = log), .first = TRUE) rsapp <- local_app_process( rsappex, opts = server_opts(num_threads = 3) ) regi_url <- rsapp$url("/register") auth_url <- rsapp$url("/authorize") toke_url <- rsapp$url("/token") # Register httr url <- paste0( regi_url, "?name=httr%20local%20app", "&redirect_uri=", httr::oauth_callback() ) reg_resp <- httr::GET(url) httr::stop_for_status(reg_resp) regdata <- httr::content(reg_resp) app <- httr::oauth_app( regdata$name[[1]], key = regdata$client_id[[1]], secret = regdata$client_secret[[1]], redirect_uri = httr::oauth_callback() ) endpoint <- httr::oauth_endpoint( authorize = auth_url, access = toke_url ) token <- suppressMessages(oauth2_httr_login( httr::oauth2.0_token(endpoint, app, cache = FALSE) )) # This will refresh the token automatically Sys.sleep(0.1) expect_message( cnt <- httr::content( httr::GET(rsapp$url("/data"), config = token), as = "parsed", type = "application/json" ), "Auto-refreshing stale OAuth" ) expect_equal(cnt, list(data = list("top secret!"))) loglines <- readLines(log) endpoints <- parse_url(map_chr(strsplit(loglines, " "), "[[", 2))$path expect_equal( endpoints, c("/register", "/authorize", "/authorize/decision", "/token", "/data", "/token", "/data") ) status <- map_chr(strsplit(loglines, " "), "[[", 3) expect_equal( as.numeric(status), c(200, 200, 302, 200, 401, 200, 200) ) }) webfakes/tests/testthat/test-delay.R0000644000176200001440000000204414172041777017253 0ustar liggesusers app <- new_app() app$get("/delay", function(req, res) { if (is.null(res$locals$seen)) { res$locals$seen <- TRUE res$delay(.5) } else { res$send_json( list(message = "Sorry, running late..."), auto_unbox = TRUE ) } }) app$get("/nodelay", function(req, res) { res$send_json( list(message = "I am fast, aren't I?"), auto_unbox = TRUE ) }) web <- local_app_process(app, opts = server_opts(num_threads = 2)) test_that("delay", { p <- curl::new_pool(multiplex = FALSE) h1 <- curl::new_handle(url = web$url("/delay")) h2 <- curl::new_handle(url = web$url("/nodelay")) resp1 <- resp2 <- NULL curl::multi_add(h1, done = function(x) resp1 <<- x, fail = stop, pool = p) curl::multi_add(h2, done = function(x) resp2 <<- x, fail = stop, pool = p) curl::multi_run(timeout = 2, pool = p) curl::multi_cancel(h1) curl::multi_cancel(h2) expect_false(is.null(resp1)) expect_false(is.null(resp2)) expect_true(resp1$times[["total"]] > 0.5) expect_true(resp2$times[["total"]] < resp1$times[["total"]]) }) webfakes/tests/testthat/test-uuid.R0000644000176200001440000000110014172041777017113 0ustar liggesusers test_that("uuid_random() format", { uu <- replicate(1000, uuid_random()) # format is right regex <- paste0( "^[0-9a-f]{8}-", "[0-9a-f]{4}-", "4[0-9a-f]{3}-", "[89ab][0-9a-f]{3}-", "[0-9a-f]{12}$" ) expect_true(all(grepl(regex, uu, perl = TRUE))) # all bits are used, except for the dashes and M and N pos <- setdiff(1:36, c(9, 14, 19, 24, 15, 20)) xd <- format(as.hexmode(0:15)) for (p in pos) expect_true(all(xd %in% substr(uu, p, p))) # all bits are used for N expect_true(all(c("8", "9", "a", "b") %in% substr(uu, 20, 20))) }) webfakes/tests/testthat/test-mw-log.R0000644000176200001440000000255014172041777017361 0ustar liggesusers app <- new_app()$ use(mw_log())$ get("/txt", function(req, res) { res$ set_type("text/plain")$ send("textual") })$ get("/html", function(req, res) { res$ set_type("text/html")$ send("hello") })$ get("/raw", function(req, res) { res$ set_type("applicartion/octet-stream")$ send(charToRaw("raw")) }) web <- local_app_process(app, callr_opts = list(stdout = "|")) test_that("text/plain response", { url <- web$url("/txt") resp <- curl::curl_fetch_memory(url) plr <- web$.process$poll_io(1000) expect_equal(plr[["output"]], "ready") log <- web$.process$read_output_lines() expect_match(log, "GET http://127\\.0\\.0\\.1:[0-9]*/txt 200 [0-9]+ ms - 7") }) test_that("text/html response", { url <- web$url("/html") resp <- curl::curl_fetch_memory(url) plr <- web$.process$poll_io(1000) expect_equal(plr[["output"]], "ready") log <- web$.process$read_output_lines() expect_match(log, "GET http://127\\.0\\.0\\.1:[0-9]+/html 200 [0-9]+ ms - 44") }) test_that("application/octet-stream response", { url <- web$url("/raw") resp <- curl::curl_fetch_memory(url) plr <- web$.process$poll_io(1000) expect_equal(plr[["output"]], "ready") log <- web$.process$read_output_lines() expect_match(log, "GET http://127\\.0\\.0\\.1:[0-9]+/raw 200 [0-9]+ ms - 3") }) webfakes/tests/testthat/test-path-matching.R0000644000176200001440000000560614172041777020710 0ustar liggesusers test_that("middleware", { expect_true(path_match("foobar", "foobar2", list(method = "use"))) }) test_that("string", { good <- list( list("/foo", "/foo"), list("/", "/"), list("/***", "/***") ) for (x in good) { expect_true(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } bad <- list( list("/foo", "/bar"), list("/foo", "foo") ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("character vector or list", { good <- list( list("/foo", c("/foo2", "/foo")), list("/", list("notthis", "/")) ) for (x in good) { expect_true(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } bad <- list( list("/foo", list()) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) named_list <- function() structure(list(), names = character()) test_that("regexp", { good <- list( list("/foo", new_regexp("^/fo+$"), list(params = named_list())), list("/", list(new_regexp("/foobar"), new_regexp("^/$")), list(params = named_list())) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } bad <- list( list("/foo", list("/foobar")) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("list of things", { good <- list( list("/foo", list("/foo2", new_regexp("/foo")), list(params = named_list())), list("/", list(new_regexp("notthis"), "/"), TRUE) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } bad <- list( list("/foo", list()) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("regexp with capture groups", { good <- list( list( "/foo/bar", new_regexp("^/foo/([a-z]+)/?$"), list(params = structure(list("bar"), names = "")) ), list( "/foo/bar", new_regexp("^/(?f.*)/(?[a-z]+)/?$"), list(params = structure(list("foo", "bar"), names = c("x", "y"))) ) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } bad <- list( list("/foo/bar", new_regexp("/foox/([a-z]+)/?$")) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("tokens", { good <- list( list( "/foo", "/:x", list(params = list(x = "foo")) ), list( "/foo/bar", "/:x/:y", list(params = list(x = "foo", y = "bar")) ) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } }) webfakes/tests/testthat/test-mw-range-parser.R0000644000176200001440000000131414430110410021135 0ustar liggesusers test_that("parse_range", { expect_snapshot({ parse_range("foobar=1-100") parse_range("bytes=0-100, 50-150") parse_range("bytes=200-100") parse_range("bytes=x-100") parse_range("bytes=1-100") parse_range("bytes=1-") parse_range("bytes=-100") parse_range("bytes=0-100, 200-") parse_range("bytes=200-300, 0-100") }) }) test_that("intervals_overlap", { ok <- list( matrix(0, nrow = 0, ncol = 2), rbind(1:2), rbind(1:2, 3:4) ) for (x in ok) expect_false(intervals_overlap(x), info = x) bad <- list( rbind(c(1, 10), c(5, 10)), rbind(c(1, 20), c(1, 20)), rbind(c(1, 1), c(1, 4)) ) for (x in bad) expect_true(intervals_overlap(x), info = x) }) webfakes/tests/testthat/test-local.R0000644000176200001440000000046614172041777017255 0ustar liggesusers test_that("app from setup", { # This app was created in setup.R url <- httpbin2$url("/get", query = c(q1 = "one", q2 = "two")) handle <- curl::new_handle() curl::handle_setheaders(handle, "foo" = "bar") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) }) webfakes/tests/testthat.R0000644000176200001440000000007414172041777015201 0ustar liggesuserslibrary(testthat) library(webfakes) test_check("webfakes") webfakes/src/0000755000176200001440000000000014535460366012644 5ustar liggesuserswebfakes/src/cleancall.c0000644000176200001440000001017214323222672014716 0ustar liggesusers#define R_NO_REMAP #include #include "cleancall.h" #if (defined(R_VERSION) && R_VERSION < R_Version(3, 4, 0)) SEXP R_MakeExternalPtrFn(DL_FUNC p, SEXP tag, SEXP prot) { fn_ptr ptr; ptr.fn = p; return R_MakeExternalPtr(ptr.p, tag, prot); } DL_FUNC R_ExternalPtrAddrFn(SEXP s) { fn_ptr ptr; ptr.p = R_ExternalPtrAddr(s); return ptr.fn; } #endif // The R API does not have a setter for function pointers SEXP cleancall_MakeExternalPtrFn(DL_FUNC p, SEXP tag, SEXP prot) { fn_ptr tmp; tmp.fn = p; return R_MakeExternalPtr(tmp.p, tag, prot); } void cleancall_SetExternalPtrAddrFn(SEXP s, DL_FUNC p) { fn_ptr ptr; ptr.fn = p; R_SetExternalPtrAddr(s, ptr.p); } // Initialised at load time with the `.Call` primitive SEXP cleancall_fns_dot_call = NULL; void cleancall_init(void) { cleancall_fns_dot_call = Rf_findVar(Rf_install(".Call"), R_BaseEnv); } struct eval_args { SEXP call; SEXP env; }; static SEXP eval_wrap(void* data) { struct eval_args* args = (struct eval_args*) data; return Rf_eval(args->call, args->env); } SEXP cleancall_call(SEXP args, SEXP env) { SEXP call = PROTECT(Rf_lcons(cleancall_fns_dot_call, args)); struct eval_args data = { call, env }; SEXP out = r_with_cleanup_context(&eval_wrap, &data); UNPROTECT(1); return out; } static SEXP callbacks = NULL; // Preallocate a callback static void push_callback(SEXP stack) { SEXP top = CDR(stack); SEXP early_handler = PROTECT(Rf_allocVector(LGLSXP, 1)); SEXP fn_extptr = PROTECT(cleancall_MakeExternalPtrFn(NULL, R_NilValue, R_NilValue)); SEXP data_extptr = PROTECT(R_MakeExternalPtr(NULL, early_handler, R_NilValue)); SEXP cb = Rf_cons(Rf_cons(fn_extptr, data_extptr), top); SETCDR(stack, cb); UNPROTECT(3); } struct data_wrapper { SEXP (*fn)(void* data); void *data; SEXP callbacks; int success; }; static void call_exits(void* data) { // Remove protecting node. Don't remove the preallocated callback on // the top as it might contain a handler when something went wrong. SEXP top = CDR(callbacks); // Restore old stack struct data_wrapper* state = data; callbacks = (SEXP) state->callbacks; // Handlers should not jump while (top != R_NilValue) { SEXP cb = CAR(top); top = CDR(top); void (*fn)(void*) = (void (*)(void*)) R_ExternalPtrAddrFn(CAR(cb)); void *data = (void*) R_ExternalPtrAddr(CDR(cb)); int early_handler = LOGICAL(R_ExternalPtrTag(CDR(cb)))[0]; // Check for empty pointer in preallocated callbacks if (fn) { if (!early_handler || !state->success) fn(data); } } } static SEXP with_cleanup_context_wrap(void *data) { struct data_wrapper* cdata = data; SEXP ret = cdata->fn(cdata->data); cdata->success = 1; return ret; } SEXP r_with_cleanup_context(SEXP (*fn)(void* data), void* data) { // Preallocate new stack before changing `callbacks` to avoid // leaving the global variable in a bad state if alloc fails SEXP new = PROTECT(Rf_cons(R_NilValue, R_NilValue)); push_callback(new); if (!callbacks) callbacks = R_NilValue; SEXP old = callbacks; callbacks = new; struct data_wrapper state = { fn, data, old, 0 }; SEXP out = R_ExecWithCleanup(with_cleanup_context_wrap, &state, &call_exits, &state); UNPROTECT(1); return out; } static void call_save_handler(void (*fn)(void *data), void* data, int early) { if (!callbacks) { fn(data); Rf_error("Internal error: Exit handler pushed outside " "of an exit context"); } SEXP cb = CADR(callbacks); // Update pointers cleancall_SetExternalPtrAddrFn(CAR(cb), (DL_FUNC) fn); R_SetExternalPtrAddr(CDR(cb), data); LOGICAL(R_ExternalPtrTag(CDR(cb)))[0] = early; // Preallocate the next callback in case the allocator jumps push_callback(callbacks); } void r_call_on_exit(void (*fn)(void* data), void* data) { call_save_handler(fn, data, /* early = */ 0); } void r_call_on_early_exit(void (*fn)(void* data), void* data) { call_save_handler(fn, data, /* early = */ 1); } webfakes/src/rweb.c0000644000176200001440000007022714535456314013755 0ustar liggesusers #include #include #include #include "civetweb.h" #include "errors.h" #include "cleancall.h" #include #include #ifdef _WIN32 #include #else #include #include #endif /* --------------------------------------------------------------------- */ /* registration */ /* --------------------------------------------------------------------- */ SEXP server_start(SEXP options); SEXP server_poll(SEXP rsrv, SEXP clean); SEXP server_stop(SEXP rsrv); SEXP server_get_ports(SEXP rsrv); SEXP response_delay(SEXP req, SEXP secs); SEXP response_send_headers(SEXP req); SEXP response_send(SEXP req); SEXP response_write(SEXP req, SEXP data); SEXP response_send_error(SEXP req, SEXP message, SEXP status); SEXP response_send_chunk(SEXP req, SEXP data); SEXP webfakes_crc32(SEXP v); static const R_CallMethodDef callMethods[] = { CLEANCALL_METHOD_RECORD, /* server */ { "server_start", (DL_FUNC) &server_start, 1 }, { "server_poll", (DL_FUNC) &server_poll, 2 }, { "server_stop", (DL_FUNC) &server_stop, 1 }, { "server_get_ports", (DL_FUNC) &server_get_ports, 1 }, /* request/response/connection */ { "response_delay", (DL_FUNC) &response_delay, 2 }, { "response_send_headers", (DL_FUNC) &response_send_headers, 1 }, { "response_send", (DL_FUNC) &response_send, 1 }, { "response_write", (DL_FUNC) &response_write, 2 }, { "response_send_error", (DL_FUNC) &response_send_error, 3 }, { "response_send_chunk", (DL_FUNC) &response_send_chunk, 2 }, /* others */ { "webfakes_crc32", (DL_FUNC) &webfakes_crc32, 1 }, { NULL, NULL, 0 } }; void R_init_webfakes(DllInfo *dll) { R_registerRoutines(dll, NULL, callMethods, NULL, NULL); R_useDynamicSymbols(dll, FALSE); R_forceSymbols(dll, TRUE); cleancall_fns_dot_call = Rf_findVar(Rf_install(".Call"), R_BaseEnv); /* Once we require some features we need to check the return value. */ mg_init_library(0); } /* --------------------------------------------------------------------- */ /* for older macOS versions */ /* --------------------------------------------------------------------- */ #if defined(__MACH__) && defined(__APPLE__) #include #include #include #include /* It doesn't really matter what these are defined to, as long as they are defined */ #ifndef CLOCK_REALTIME #define CLOCK_REALTIME 0 #endif #ifndef CLOCK_MONOTONIC #define CLOCK_MONOTONIC 1 #endif static int webfakes_clock_gettime(int clk_id, struct timespec *t) { memset(t, 0, sizeof(*t)); if (clk_id == CLOCK_REALTIME) { struct timeval now; int rv = gettimeofday(&now, NULL); if (rv) { return rv; } t->tv_sec = now.tv_sec; t->tv_nsec = now.tv_usec * 1000; return 0; } else if (clk_id == CLOCK_MONOTONIC) { static uint64_t clock_start_time = 0; static mach_timebase_info_data_t timebase_ifo = {0, 0}; uint64_t now = mach_absolute_time(); if (clock_start_time == 0) { kern_return_t mach_status = mach_timebase_info(&timebase_ifo); /* appease "unused variable" warning for release builds */ (void)mach_status; clock_start_time = now; } now = (uint64_t)((double)(now - clock_start_time) * (double)timebase_ifo.numer / (double)timebase_ifo.denom); t->tv_sec = now / 1000000000; t->tv_nsec = now % 1000000000; return 0; } return -1; /* EINVAL - Clock ID is unknown */ } #else #define webfakes_clock_gettime(a,b) clock_gettime(a,b) #endif /* --------------------------------------------------------------------- */ /* internals */ /* --------------------------------------------------------------------- */ #ifdef _WIN32 int check_stdin(void) { HANDLE hnd = GetStdHandle(STD_INPUT_HANDLE); if (hnd == INVALID_HANDLE_VALUE) { R_THROW_SYSTEM_ERROR("Cannot get stdin handle"); } DWORD type = GetFileType(hnd); if (type != FILE_TYPE_PIPE) return 0; DWORD ret = WaitForSingleObject(hnd, 0); if (ret == WAIT_TIMEOUT) return 0; if (ret == WAIT_FAILED) R_THROW_SYSTEM_ERROR("Cannot poll stdin"); if (ret == WAIT_OBJECT_0) { DWORD avail; DWORD ret2 = PeekNamedPipe(hnd, NULL, 0, NULL, &avail, NULL); if (!ret2) { DWORD err = GetLastError(); if (err == ERROR_BROKEN_PIPE) return 1; R_THROW_SYSTEM_ERROR_CODE(err, "Cannot peek stdin pipe"); } } return 0; } #else int check_stdin(void) { static char buffer[4096]; struct pollfd pfd; pfd.fd = 0; pfd.events = POLLIN; pfd.revents = 0; int ret = poll(&pfd, 1, 0); if (ret == -1) R_THROW_SYSTEM_ERROR("Cannot poll stdin"); // Nothing happened on stdin, keep running if (ret == 0) return 0; // Event on stdin, if we cannot read, then it if EOF ssize_t num = read(0, buffer, 4096); if (num == -1) R_THROW_SYSTEM_ERROR("Cannot read from stdin"); if (num == 0) return 1; // Otherwise fine return 0; } #endif #define WEBFAKES_NOTHING 0 #define WEBFAKES_REQ 1 /* request just came it */ #define WEBFAKES_WAIT 2 /* waited a bit / wait a bit */ #define WEBFAKES_DONE 3 /* request is done */ struct server_user_data { SEXP requests; pthread_cond_t process_more; /* there is something to process */ pthread_cond_t process_less; /* we can process something */ pthread_mutex_t process_lock; struct mg_connection *nextconn; struct mg_server_port ports[4]; int num_ports; int shutdown; }; struct connection_user_data { pthread_cond_t finish_cond; /* can finish callback? */ pthread_mutex_t finish_lock; int main_todo; /* what should the main thread do? */ int req_todo; /* what shoudl the request thread do? */ double secs; /* how much should we wait? */ SEXP req; int id; }; SEXP webfakes_create_request(struct mg_connection *conn); #define PTHCHK(expr) if ((ret = expr)) { \ mg_cry(conn, "ERROR @ %s %s:%d", __func__, __FILE__, __LINE__); \ R_THROW_SYSTEM_ERROR_CODE( \ ret, "Cannot process webfakes web server requests"); \ } #define CHK(expr) if ((ret = expr) < 0) { \ mg_cry(conn, "ERROR @ %s %s:%d", __func__, __FILE__, __LINE__); \ R_THROW_ERROR("Cannot process webfakes web server requests"); \ } static R_INLINE SEXP new_env(void) { SEXP env; PROTECT(env = allocSExp(ENVSXP)); SET_FRAME(env, R_NilValue); SET_ENCLOS(env, R_EmptyEnv); SET_HASHTAB(env, R_NilValue); SET_ATTRIB(env, R_NilValue); UNPROTECT(1); return env; } static void SEXP_to_char_vector(SEXP x, char*** vec) { int i, len = LENGTH(x); SEXP nms = PROTECT(getAttrib(x, R_NamesSymbol)); *vec = (char**) R_alloc(2 * len + 1, sizeof(char*)); for (i = 0; i < len; i++) { (*vec)[2 * i ] = (char*) CHAR(STRING_ELT(nms, i)); (*vec)[2 * i + 1] = (char*) CHAR(STRING_ELT(x, i)); } (*vec)[2 * len] = NULL; UNPROTECT(1); } static int ms_sleep(struct server_user_data* srv_data, int ms) { int tosleep = ms > 100 ? 100 : ms; while (tosleep > 0) { #ifdef _WIN32 Sleep(tosleep); #else usleep(tosleep * 1000); #endif if (srv_data->shutdown) return 1; ms -= tosleep; tosleep = ms > 100 ? 100 : ms; } return 0; } /* --------------------------------------------------------------------- */ /* civetweb callbacks */ /* --------------------------------------------------------------------- */ static int begin_request(struct mg_connection *conn) { #ifndef NDEBUG fprintf(stderr, "conn %p: starting\n", (void*) conn); #endif struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); if (srv_data->shutdown) return 1; struct connection_user_data conn_data = { PTHREAD_COND_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, WEBFAKES_REQ, WEBFAKES_NOTHING, 0.0, R_NilValue, 0, }; mg_set_user_connection_data(conn, &conn_data); if (pthread_mutex_lock(&conn_data.finish_lock)) goto exit; while (1) { if (pthread_mutex_lock(&srv_data->process_lock)) goto exit; #ifndef NDEBUG fprintf(stderr, "conn %p: waiting for slot\n", (void*) conn); #endif while (srv_data->nextconn != NULL) { pthread_cond_wait(&srv_data->process_less, &srv_data->process_lock); } #ifndef NDEBUG fprintf(stderr, "conn %p: scheduled\n", (void*) conn); #endif srv_data->nextconn = conn; /* only used to pass it to the main thread */ if (srv_data->shutdown) goto exit; if (pthread_cond_signal(&srv_data->process_more)) goto exit; if (pthread_mutex_unlock(&srv_data->process_lock)) goto exit; /* Need to wait for the response... */ #ifndef NDEBUG fprintf(stderr, "conn %p: waiting for order\n", (void*) conn); #endif while (conn_data.req_todo == WEBFAKES_NOTHING) { if (pthread_cond_wait(&conn_data.finish_cond, &conn_data.finish_lock)) { goto exit; } } #ifndef NDEBUG fprintf(stderr, "conn %p: got order: %d\n", (void*) conn, conn_data.req_todo); #endif if (conn_data.req_todo == WEBFAKES_DONE) goto exit; if (conn_data.req_todo == WEBFAKES_WAIT) { #ifndef NDEBUG fprintf(stderr, "conn %p: sleeping\n", (void*) conn); #endif if (ms_sleep(srv_data, conn_data.secs * 1000)) goto exit; #ifndef NDEBUG fprintf(stderr, "conn %p: sleeping done\n", (void*) conn); #endif } if (srv_data->shutdown) goto exit; conn_data.main_todo = WEBFAKES_WAIT; conn_data.req_todo = WEBFAKES_NOTHING; } exit: #ifndef NDEBUG fprintf(stderr, "conn %p: good bye all\n", (void*) conn); #endif mg_set_user_connection_data(conn, NULL); pthread_mutex_unlock(&conn_data.finish_lock); pthread_mutex_destroy(&conn_data.finish_lock); pthread_cond_destroy(&conn_data.finish_cond); return 1; } /* --------------------------------------------------------------------- */ /* server */ /* --------------------------------------------------------------------- */ static int register_request(struct server_user_data *srv_data, SEXP req) { SEXP nextid = PROTECT(Rf_install("nextid")); int id = INTEGER(Rf_findVar(nextid, srv_data->requests))[0] + 1; SEXP xid = PROTECT(ScalarInteger(id)); Rf_defineVar(nextid, xid, srv_data->requests); SEXP cid = PROTECT(asChar(xid)); SEXP rname = PROTECT(Rf_installChar(cid)); Rf_defineVar(rname, req, srv_data->requests); UNPROTECT(4); return id; } static void deregister_request(struct server_user_data *srv_data, int id) { SEXP xid = PROTECT(ScalarInteger(id)); SEXP cid = PROTECT(asChar(xid)); SEXP rname = PROTECT(Rf_installChar(cid)); Rf_defineVar(rname, R_NilValue, srv_data->requests); UNPROTECT(3); } static void release_all_requests(SEXP requests) { SEXP nms = PROTECT(R_lsInternal3(requests, 1, 0)); int i, n = LENGTH(nms); for (i = 0; i < n; i++) { const char *nm = CHAR(STRING_ELT(nms, i)); if (!strcmp("nextid", nm)) continue; SEXP sym = PROTECT(Rf_installChar(STRING_ELT(nms, i))); SEXP req = Rf_findVar(sym, requests); if (!isNull(req)) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn) { #ifndef NDEBUG fprintf(stderr, "conn %p: emergency cleanup\n", (void*) conn); #endif struct connection_user_data *conn_data = mg_get_user_connection_data(conn); struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; conn_data->req = R_NilValue; pthread_cond_signal(&conn_data->finish_cond); pthread_mutex_unlock(&conn_data->finish_lock); pthread_cond_signal(&srv_data->process_less); } } UNPROTECT(1); } UNPROTECT(1); } static void webfakes_server_finalizer(SEXP server) { struct mg_context *ctx = R_ExternalPtrAddr(server); if (ctx == NULL) return; #ifndef NDEBUG fprintf(stderr, "serv %p: cleaning up\n", (void*) ctx); #endif R_ClearExternalPtr(server); struct server_user_data* srv_data = mg_get_user_data(ctx); srv_data->shutdown = 1; release_all_requests(srv_data->requests); #ifndef NDEBUG fprintf(stderr, "serv %p: waiting for worker threads\n", (void*) ctx); void *ctx_addr = ctx; #endif mg_stop(ctx); pthread_mutex_unlock(&srv_data->process_lock); pthread_mutex_destroy(&srv_data->process_lock); pthread_cond_destroy(&srv_data->process_more); pthread_cond_destroy(&srv_data->process_less); free(srv_data); #ifndef NDEBUG fprintf(stderr, "serv %p: that would be all for today\n", (void*) ctx_addr); #endif } SEXP server_start(SEXP options) { #ifndef NDEBUG fprintf(stderr, "creating new server\n"); #endif SEXP server = R_NilValue; struct server_user_data *srv_data = malloc(sizeof(struct server_user_data)); if (!srv_data) R_THROW_SYSTEM_ERROR("Cannot start webfakes server"); struct mg_context *ctx = NULL; int ret = 0; memset(srv_data, 0, sizeof(struct server_user_data)); srv_data->requests = PROTECT(new_env()); SEXP x1 = PROTECT(ScalarInteger(1)); Rf_defineVar(Rf_install("nextid"), x1, srv_data->requests); UNPROTECT(1); if ((ret = pthread_cond_init(&srv_data->process_more, NULL))) goto cleanup; if ((ret = pthread_cond_init(&srv_data->process_less, NULL))) goto cleanup; if ((ret = pthread_mutex_init(&srv_data->process_lock, NULL))) goto cleanup; char **coptions; SEXP_to_char_vector(options, &coptions); struct mg_callbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.begin_request = begin_request; if ((ret = pthread_mutex_lock(&srv_data->process_lock))) goto cleanup; ctx = mg_start(&callbacks, srv_data, (const char **) coptions); if (ctx == NULL) goto cleanup; PROTECT(server = R_MakeExternalPtr(ctx, srv_data->requests, R_NilValue)); R_RegisterCFinalizer(server, webfakes_server_finalizer); #ifndef NDEBUG fprintf(stderr, "serv %p: hi everyone, ready to serve\n", (void*) ctx); #endif memset(srv_data->ports, 0, sizeof(srv_data->ports)); srv_data->num_ports = mg_get_server_ports( ctx, sizeof(srv_data->ports) / sizeof(struct mg_server_port), srv_data->ports ); if (srv_data->num_ports < 0) goto cleanup; UNPROTECT(2); return server; cleanup: #ifndef NDEBUG fprintf(stderr, "serv %p: failed to start new server\n", (void*) ctx); #endif /* This is unlocked in the finalizer, but that might be much later... */ if (ctx) mg_stop(ctx); pthread_mutex_unlock(&srv_data->process_lock); if (ret) { R_THROW_SYSTEM_ERROR_CODE(ret, "Cannot start webfakes web server"); } else { R_THROW_ERROR("Cannot start webfakes web server"); } /* Never reached */ return R_NilValue; } static void server_poll_cleanup(void *ptr) { #ifndef NDEBUG fprintf(stderr, "conn %p: oh-oh, forced cleanup\n", (void*) ptr); #endif struct mg_connection *conn = (struct mg_connection*) ptr; struct connection_user_data *conn_data = mg_get_user_connection_data(conn); struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); mg_cry(conn, "Cleaning up broken connection at %s:%d", __FILE__, __LINE__); pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; deregister_request(srv_data, conn_data->id); conn_data->req = R_NilValue; pthread_cond_signal(&conn_data->finish_cond); pthread_mutex_unlock(&conn_data->finish_lock); pthread_cond_signal(&srv_data->process_less); } SEXP server_poll(SEXP server, SEXP clean) { struct mg_context *ctx = R_ExternalPtrAddr(server); int cclean = LOGICAL(clean)[0]; #ifndef NDEBUG fprintf(stderr, "serv %p: polling\n", (void*) ctx); #endif if (ctx == NULL) R_THROW_ERROR("webfakes server has stopped already"); struct server_user_data *srv_data = mg_get_user_data(ctx); struct timespec limit; while (srv_data->nextconn == NULL) { webfakes_clock_gettime(CLOCK_REALTIME, &limit); limit.tv_nsec += 50 * 1000 * 1000; if (limit.tv_nsec >= 1000 * 1000 * 1000) { limit.tv_sec += 1; limit.tv_nsec %= 1000 * 1000 * 1000; } R_CheckUserInterrupt(); if (cclean && check_stdin()) R_THROW_ERROR("Cleaning up web server"); (void) pthread_cond_timedwait(&srv_data->process_more, &srv_data->process_lock, &limit); } struct mg_connection *conn = srv_data->nextconn; srv_data->nextconn = NULL; struct connection_user_data *conn_data = mg_get_user_connection_data(conn); #ifndef NDEBUG fprintf(stderr, "serv %p: processing conn %p\n", (void*) ctx, (void*) conn); #endif SEXP req = R_NilValue; switch(conn_data->main_todo) { case WEBFAKES_REQ: r_call_on_early_exit(server_poll_cleanup, conn); req = webfakes_create_request(conn); break; case WEBFAKES_WAIT: req = conn_data->req; break; default: break; } #ifndef NDEBUG fprintf(stderr, "serv %p: returning request from conn %p\n", (void*) ctx, (void*) conn); #endif return req; } SEXP server_stop(SEXP server) { webfakes_server_finalizer(server); return R_NilValue; } SEXP server_get_ports(SEXP server) { struct mg_context *ctx = R_ExternalPtrAddr(server); if (ctx == NULL) R_THROW_ERROR("webfakes server has stopped already"); struct server_user_data *srv_data = mg_get_user_data(ctx); int i, num_ports = srv_data->num_ports; SEXP ipv4 = PROTECT(allocVector(LGLSXP, num_ports)); SEXP ipv6 = PROTECT(allocVector(LGLSXP, num_ports)); SEXP port = PROTECT(allocVector(INTSXP, num_ports)); SEXP ssl = PROTECT(allocVector(LGLSXP, num_ports)); const char *res_names[] = { "ipv4", "ipv6", "port", "ssl", "" }; SEXP res = PROTECT(Rf_mkNamed(VECSXP, res_names)); for (i = 0; i < num_ports; i++) { LOGICAL(ipv4)[i] = (srv_data->ports[i].protocol) & 1; LOGICAL(ipv6)[i] = (srv_data->ports[i].protocol) & 2; INTEGER(port)[i] = srv_data->ports[i].port; LOGICAL(ssl )[i] = srv_data->ports[i].is_ssl == 1; } SET_VECTOR_ELT(res, 0, ipv4); SET_VECTOR_ELT(res, 1, ipv6); SET_VECTOR_ELT(res, 2, port); SET_VECTOR_ELT(res, 3, ssl); UNPROTECT(5); return res; } /* --------------------------------------------------------------------- */ /* request */ /* --------------------------------------------------------------------- */ SEXP webfakes_create_request(struct mg_connection *conn) { static char request_link[8192]; int i; SEXP x; #ifndef NDEBUG fprintf(stderr, "conn %p: creating an R request object\n", (void*) conn); #endif const struct mg_request_info *req_info = mg_get_request_info(conn); SEXP req = PROTECT(new_env()); x = PROTECT(mkString(req_info->request_method)); defineVar(Rf_install("method"), x, req); UNPROTECT(1); mg_get_request_link(conn, request_link, sizeof(request_link)); x = PROTECT(mkString(request_link)); defineVar(Rf_install("url"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->request_uri)); defineVar(Rf_install("request_uri"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->local_uri)); defineVar(Rf_install("path"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->http_version)); defineVar(Rf_install("http_version"), x, req); UNPROTECT(1); x = PROTECT(req_info->query_string ? mkString(req_info->query_string) : mkString("")); defineVar(Rf_install("query_string"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->remote_addr)); defineVar(Rf_install("remote_addr"), x, req); UNPROTECT(1); x = PROTECT(ScalarReal(req_info->content_length)); defineVar(Rf_install("content_length"), x, req); UNPROTECT(1); x = PROTECT(ScalarInteger(req_info->remote_port)); defineVar(Rf_install("remote_port"), x, req); UNPROTECT(1); SEXP hdr = PROTECT(allocVector(VECSXP, req_info->num_headers)); SEXP nms = PROTECT(allocVector(STRSXP, req_info->num_headers)); for (i = 0; i < req_info->num_headers; i++) { SET_VECTOR_ELT(hdr, i, mkString(req_info->http_headers[i].value)); SET_STRING_ELT(nms, i, mkChar(req_info->http_headers[i].name)); } Rf_setAttrib(hdr, R_NamesSymbol, nms); defineVar(Rf_install("headers"), hdr, req); if (req_info->content_length != -1) { SEXP body = PROTECT(allocVector(RAWSXP, req_info->content_length)); int ret = mg_read(conn, RAW(body), req_info->content_length); if (ret < 0) { mg_cry(conn, "ERROR @ %s %s:%d", __func__, __FILE__, __LINE__); R_THROW_ERROR("Cannot read from webfakes HTTP client"); } if (ret != req_info->content_length) { warning("Partial HTTP request body from client"); } defineVar(Rf_install(".body"), body, req); UNPROTECT(1); } else { defineVar(Rf_install(".body"), R_NilValue, req); } SEXP xreq = PROTECT(R_MakeExternalPtr(conn, R_NilValue, R_NilValue)); defineVar(Rf_install(".xconn"), xreq, req); UNPROTECT(1); struct connection_user_data *conn_data = mg_get_user_connection_data(conn); conn_data->req = req; struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); conn_data->id = register_request(srv_data, req); UNPROTECT(3); return req; } static void response_cleanup(void *ptr) { struct mg_connection *conn = (struct mg_connection*) ptr; struct connection_user_data *conn_data = mg_get_user_connection_data(conn); struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); if (conn_data) { #ifndef NDEBUG fprintf(stderr, "conn %p: oh-oh, emergency cleanup\n", (void*) ptr); #endif mg_set_user_connection_data(conn, NULL); mg_cry(conn, "Cleaning up broken connection %p at %s:%d", (void*) conn, __FILE__, __LINE__); pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; deregister_request(srv_data, conn_data->id); SEXP req = conn_data->req; SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); R_ClearExternalPtr(xconn); conn_data->req = R_NilValue; pthread_cond_signal(&conn_data->finish_cond); pthread_mutex_unlock(&conn_data->finish_lock); } pthread_cond_signal(&srv_data->process_less); } SEXP response_delay(SEXP req, SEXP secs) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } struct mg_context *ctx = mg_get_context(conn); #ifndef NDEBUG fprintf(stderr, "serv %p: telling conn %p to delay\n", (void*) ctx, (void*) conn); #endif struct connection_user_data *conn_data = mg_get_user_connection_data(conn); int ret; r_call_on_early_exit(response_cleanup, conn); pthread_mutex_lock(&conn_data->finish_lock); conn_data->secs = REAL(secs)[0]; conn_data->req_todo = WEBFAKES_WAIT; PTHCHK(pthread_cond_signal(&conn_data->finish_cond)); PTHCHK(pthread_mutex_unlock(&conn_data->finish_lock)); struct server_user_data *srv_data = mg_get_user_data(ctx); #ifndef NDEBUG fprintf(stderr, "serv %p: inviting request threads\n", (void*) ctx); #endif PTHCHK(pthread_cond_signal(&srv_data->process_less)); return R_NilValue; } SEXP response_send_headers(SEXP req) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } #ifndef NDEBUG fprintf(stderr, "conn %p: sending response headers\n", (void*) conn); #endif r_call_on_early_exit(response_cleanup, conn); SEXP http_version = PROTECT(Rf_findVar(Rf_install("http_version"), req)); SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers = PROTECT(Rf_findVar(Rf_install(".headers"), res)); SEXP names = PROTECT(Rf_getAttrib(headers, R_NamesSymbol)); SEXP status = PROTECT(Rf_findVar(Rf_install(".status"), res)); int ret, i, nh = isNull(headers) ? 0 : LENGTH(headers); CHK(mg_printf(conn, "HTTP/%s %d %s\r\n", CHAR(STRING_ELT(http_version, 0)), INTEGER(status)[0], mg_get_response_code_text(conn, INTEGER(status)[0]))); for (i = 0; i < nh; i++) { const char *k = CHAR(STRING_ELT(names, i)); const char *v = CHAR(STRING_ELT(VECTOR_ELT(headers, i), 0)); CHK(mg_printf(conn, "%s: %s\r\n", k, v)); } CHK(mg_printf(conn, "\r\n")); #ifndef NDEBUG fprintf(stderr, "conn %p: response headers sent\n", (void*) conn); #endif UNPROTECT(5); return R_NilValue; } SEXP response_send(SEXP req) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } #ifndef NDEBUG fprintf(stderr, "conn %p: sending response body\n", (void*) conn); #endif SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers_sent = Rf_findVar(Rf_install("headers_sent"), res); if (! LOGICAL(headers_sent)[0]) response_send_headers(req); struct connection_user_data *conn_data = mg_get_user_connection_data(conn); r_call_on_early_exit(response_cleanup, conn); SEXP body = Rf_findVar(Rf_install(".body"), res); int ret; if (TYPEOF(body) == RAWSXP) { CHK(mg_write(conn, RAW(body), LENGTH(body))); } else if (TYPEOF(body) == STRSXP) { const char *cbody = CHAR(STRING_ELT(body, 0)); CHK(mg_write(conn, cbody, strlen(cbody))); } struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); #ifndef NDEBUG fprintf(stderr, "conn %p: response body sent\n", (void*) conn); #endif pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; deregister_request(srv_data, conn_data->id); conn_data->req = R_NilValue; #ifndef NDEBUG fprintf(stderr, "serv %p: telling conn %p to quit\n", (void*) ctx, (void*) conn); #endif PTHCHK(pthread_cond_signal(&conn_data->finish_cond)); PTHCHK(pthread_mutex_unlock(&conn_data->finish_lock)); #ifndef NDEBUG fprintf(stderr, "serv %p: inviting request threads\n", (void*) ctx); #endif PTHCHK(pthread_cond_signal(&srv_data->process_less)); UNPROTECT(1); return R_NilValue; } SEXP response_write(SEXP req, SEXP data) { SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers_sent = PROTECT(Rf_findVar(Rf_install("headers_sent"), res)); if (! LOGICAL(headers_sent)[0]) response_send_headers(req); SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } r_call_on_early_exit(response_cleanup, conn); int ret = 0; int len = LENGTH(data); #ifndef NDEBUG fprintf(stderr, "conn %p: writing %d bytes\n", (void*) conn, len); #endif CHK(mg_write(conn, RAW(data), len)); UNPROTECT(2); return R_NilValue; } SEXP response_send_chunk(SEXP req, SEXP data) { SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers_sent = PROTECT(Rf_findVar(Rf_install("headers_sent"), res)); if (! LOGICAL(headers_sent)[0]) response_send_headers(req); SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } r_call_on_early_exit(response_cleanup, conn); int ret = 0; int len = LENGTH(data); #ifndef NDEBUG fprintf(stderr, "conn %p: sending chunk of %d bytes\n", (void*) conn, len); #endif CHK(mg_send_chunk(conn, (const char*) RAW(data), len)); UNPROTECT(2); return R_NilValue; } SEXP response_send_error(SEXP req, SEXP message, SEXP status) { #ifndef NDEBUG fprintf(stderr, "sending 500 response"); #endif SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); Rf_defineVar(Rf_install(".body"), message, res); Rf_defineVar(Rf_install(".status"), status, res); UNPROTECT(1); return response_send(req); } webfakes/src/civetweb.h0000644000176200001440000016616614426614510014634 0ustar liggesusers/* Copyright (c) 2013-2020 the Civetweb developers * Copyright (c) 2004-2013 Sergey Lyubka * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef CIVETWEB_HEADER_INCLUDED #define CIVETWEB_HEADER_INCLUDED #define CIVETWEB_VERSION "1.12" #define CIVETWEB_VERSION_MAJOR (1) #define CIVETWEB_VERSION_MINOR (12) #define CIVETWEB_VERSION_PATCH (0) #ifndef CIVETWEB_API #if defined(_WIN32) #if defined(CIVETWEB_DLL_EXPORTS) #define CIVETWEB_API __declspec(dllexport) #elif defined(CIVETWEB_DLL_IMPORTS) #define CIVETWEB_API __declspec(dllimport) #else #define CIVETWEB_API #endif #elif __GNUC__ >= 4 #define CIVETWEB_API __attribute__((visibility("default"))) #else #define CIVETWEB_API #endif #endif #include #include #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ /* Init Features */ enum { MG_FEATURES_DEFAULT = 0x0u, /* Support files from local directories */ /* Will only work, if NO_FILES is not set. */ MG_FEATURES_FILES = 0x1u, /* Support transport layer security (TLS). */ /* SSL is still often used synonymously for TLS. */ /* Will only work, if NO_SSL is not set. */ MG_FEATURES_TLS = 0x2u, MG_FEATURES_SSL = 0x2u, /* Support common gateway interface (CGI). */ /* Will only work, if NO_CGI is not set. */ MG_FEATURES_CGI = 0x4u, /* Support IPv6. */ /* Will only work, if USE_IPV6 is set. */ MG_FEATURES_IPV6 = 0x8u, /* Support WebSocket protocol. */ /* Will only work, if USE_WEBSOCKET is set. */ MG_FEATURES_WEBSOCKET = 0x10u, /* Support server side Lua scripting. */ /* Will only work, if USE_LUA is set. */ MG_FEATURES_LUA = 0x20u, /* Support server side JavaScript scripting. */ /* Will only work, if USE_DUKTAPE is set. */ MG_FEATURES_SSJS = 0x40u, /* Provide data required for caching files. */ /* Will only work, if NO_CACHING is not set. */ MG_FEATURES_CACHE = 0x80u, /* Collect server status information. */ /* Will only work, if USE_SERVER_STATS is set. */ MG_FEATURES_STATS = 0x100u, /* Support on-the-fly compression. */ /* Will only work, if USE_ZLIB is set. */ MG_FEATURES_COMPRESSION = 0x200u, /* Collect server status information. */ /* Will only work, if USE_SERVER_STATS is set. */ MG_FEATURES_ALL = 0xFFFFu }; /* Initialize this library. This should be called once before any other * function from this library. This function is not guaranteed to be * thread safe. * Parameters: * features: bit mask for features to be initialized. * Note: The TLS libraries (like OpenSSL) is initialized * only if the MG_FEATURES_TLS bit is set. * Currently the other bits do not influence * initialization, but this may change in future * versions. * Return value: * initialized features * 0: error */ CIVETWEB_API unsigned mg_init_library(unsigned features); /* Un-initialize this library. * Return value: * 0: error */ CIVETWEB_API unsigned mg_exit_library(void); struct mg_context; /* Handle for the HTTP service itself */ struct mg_connection; /* Handle for the individual connection */ /* Maximum number of headers */ #define MG_MAX_HEADERS (64) struct mg_header { const char *name; /* HTTP header name */ const char *value; /* HTTP header value */ }; /* This structure contains information about the HTTP request. */ struct mg_request_info { const char *request_method; /* "GET", "POST", etc */ const char *request_uri; /* URL-decoded URI (absolute or relative, * as in the request) */ const char *local_uri; /* URL-decoded URI (relative). Can be NULL * if the request_uri does not address a * resource at the server host. */ #if defined(MG_LEGACY_INTERFACE) /* 2017-02-04, deprecated 2014-09-14 */ const char *uri; /* Deprecated: use local_uri instead */ #endif const char *http_version; /* E.g. "1.0", "1.1" */ const char *query_string; /* URL part after '?', not including '?', or NULL */ const char *remote_user; /* Authenticated user, or NULL if no auth used */ char remote_addr[48]; /* Client's IP address as a string. */ long long content_length; /* Length (in bytes) of the request body, can be -1 if no length was given. */ int remote_port; /* Client's port */ int is_ssl; /* 1 if SSL-ed, 0 if not */ void *user_data; /* User data pointer passed to mg_start() */ void *conn_data; /* Connection-specific user data */ int num_headers; /* Number of HTTP headers */ struct mg_header http_headers[MG_MAX_HEADERS]; /* Allocate maximum headers */ struct mg_client_cert *client_cert; /* Client certificate information */ const char *acceptedWebSocketSubprotocol; /* websocket subprotocol, * accepted during handshake */ }; /* This structure contains information about the HTTP request. */ /* This structure may be extended in future versions. */ struct mg_response_info { int status_code; /* E.g. 200 */ const char *status_text; /* E.g. "OK" */ const char *http_version; /* E.g. "1.0", "1.1" */ long long content_length; /* Length (in bytes) of the request body, can be -1 if no length was given. */ int num_headers; /* Number of HTTP headers */ struct mg_header http_headers[MG_MAX_HEADERS]; /* Allocate maximum headers */ }; /* Client certificate information (part of mg_request_info) */ /* New nomenclature. */ struct mg_client_cert { void *peer_cert; const char *subject; const char *issuer; const char *serial; const char *finger; }; #if defined(MG_LEGACY_INTERFACE) /* 2017-10-05 */ /* Old nomenclature. */ struct client_cert { const char *subject; const char *issuer; const char *serial; const char *finger; }; #endif /* This structure needs to be passed to mg_start(), to let civetweb know which callbacks to invoke. For a detailed description, see https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md */ struct mg_callbacks { /* Called when civetweb has received new HTTP request. If the callback returns one, it must process the request by sending valid HTTP headers and a body. Civetweb will not do any further processing. Otherwise it must return zero. Note that since V1.7 the "begin_request" function is called before an authorization check. If an authorization check is required, use a request_handler instead. Return value: 0: civetweb will process the request itself. In this case, the callback must not send any data to the client. 1-999: callback already processed the request. Civetweb will not send any data after the callback returned. The return code is stored as a HTTP status code for the access log. */ int (*begin_request)(struct mg_connection *); /* Called when civetweb has finished processing request. */ void (*end_request)(const struct mg_connection *, int reply_status_code); /* Called when civetweb is about to log a message. If callback returns non-zero, civetweb does not log anything. */ int (*log_message)(const struct mg_connection *, const char *message); /* Called when civetweb is about to log access. If callback returns non-zero, civetweb does not log anything. */ int (*log_access)(const struct mg_connection *, const char *message); /* Called when civetweb initializes SSL library. Parameters: ssl_ctx: SSL_CTX pointer. user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will set up the SSL certificate. 1: civetweb assumes the callback already set up the certificate. -1: initializing ssl fails. */ int (*init_ssl)(void *ssl_ctx, void *user_data); /* Called when civetweb initializes SSL library for a domain. Parameters: server_domain: authentication_domain from the domain config. ssl_ctx: SSL_CTX pointer. user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will set up the SSL certificate. 1: civetweb assumes the callback already set up the certificate. -1: initializing ssl fails. */ int (*init_ssl_domain)(const char *server_domain, void *ssl_ctx, void *user_data); /* Called when civetweb is about to create or free a SSL_CTX. Parameters: ssl_ctx: SSL_CTX pointer. NULL at creation time, Not NULL when mg_context will be freed user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will continue to create the context, just as if the callback would not be present. The value in *ssl_ctx when the function returns is ignored. 1: civetweb will copy the value from *ssl_ctx to the civetweb context and doesn't create its own. -1: initializing ssl fails.*/ int (*external_ssl_ctx)(void **ssl_ctx, void *user_data); /* Called when civetweb is about to create or free a SSL_CTX for a domain. Parameters: server_domain: authentication_domain from the domain config. ssl_ctx: SSL_CTX pointer. NULL at creation time, Not NULL when mg_context will be freed user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will continue to create the context, just as if the callback would not be present. The value in *ssl_ctx when the function returns is ignored. 1: civetweb will copy the value from *ssl_ctx to the civetweb context and doesn't create its own. -1: initializing ssl fails.*/ int (*external_ssl_ctx_domain)(const char *server_domain, void **ssl_ctx, void *user_data); #if defined(MG_LEGACY_INTERFACE) /* 2015-08-19 */ \ || defined(MG_EXPERIMENTAL_INTERFACES) /* 2019-11-03 */ /* Called when websocket request is received, before websocket handshake. Return value: 0: civetweb proceeds with websocket handshake. 1: connection is closed immediately. This callback is deprecated: Use mg_set_websocket_handler instead. */ int (*websocket_connect)(const struct mg_connection *); /* Called when websocket handshake is successfully completed, and connection is ready for data exchange. This callback is deprecated: Use mg_set_websocket_handler instead. */ void (*websocket_ready)(struct mg_connection *); /* Called when data frame has been received from the client. Parameters: bits: first byte of the websocket frame, see websocket RFC at http://tools.ietf.org/html/rfc6455, section 5.2 data, data_len: payload, with mask (if any) already applied. Return value: 1: keep this websocket connection open. 0: close this websocket connection. This callback is deprecated: Use mg_set_websocket_handler instead. */ int (*websocket_data)(struct mg_connection *, int bits, char *data, size_t data_len); #endif /* MG_LEGACY_INTERFACE */ /* Called when civetweb is closing a connection. The per-context mutex is locked when this is invoked. Websockets: Before mg_set_websocket_handler has been added, it was primarily useful for noting when a websocket is closing, and used to remove it from any application-maintained list of clients. Using this callback for websocket connections is deprecated: Use mg_set_websocket_handler instead. Connection specific data: If memory has been allocated for the connection specific user data (mg_request_info->conn_data, mg_get_user_connection_data), this is the last chance to free it. */ void (*connection_close)(const struct mg_connection *); /* Called when civetweb is about to serve Lua server page, if Lua support is enabled. Parameters: conn: current connection. lua_context: "lua_State *" pointer. */ void (*init_lua)(const struct mg_connection *conn, void *lua_context); #if defined(MG_LEGACY_INTERFACE) /* 2016-05-14 */ /* Called when civetweb has uploaded a file to a temporary directory as a result of mg_upload() call. Note that mg_upload is deprecated. Use mg_handle_form_request instead. Parameters: file_name: full path name to the uploaded file. */ void (*upload)(struct mg_connection *, const char *file_name); #endif /* Called when civetweb is about to send HTTP error to the client. Implementing this callback allows to create custom error pages. Parameters: conn: current connection. status: HTTP error status code. errmsg: error message text. Return value: 1: run civetweb error handler. 0: callback already handled the error. */ int (*http_error)(struct mg_connection *conn, int status, const char *errmsg); /* Called after civetweb context has been created, before requests are processed. Parameters: ctx: context handle */ void (*init_context)(const struct mg_context *ctx); /* Called when civetweb context is deleted. Parameters: ctx: context handle */ void (*exit_context)(const struct mg_context *ctx); /* Called when a new worker thread is initialized. * Parameters: * ctx: context handle * thread_type: * 0 indicates the master thread * 1 indicates a worker thread handling client connections * 2 indicates an internal helper thread (timer thread) * Return value: * This function returns a user supplied pointer. The pointer is assigned * to the thread and can be obtained from the mg_connection object using * mg_get_thread_pointer in all server callbacks. Note: A connection and * a thread are not directly related. Threads will serve several different * connections, and data from a single connection may call different * callbacks using different threads. The thread pointer can be obtained * in a callback handler, but should not be stored beyond the scope of * one call to one callback. */ void *(*init_thread)(const struct mg_context *ctx, int thread_type); /* Called when a worker exits. * The parameters "ctx" and "thread_type" correspond to the "init_thread" * call. The "thread_pointer" parameter is the value returned by * "init_thread". */ void (*exit_thread)(const struct mg_context *ctx, int thread_type, void *thread_pointer); /* Called when initializing a new connection object. * Can be used to initialize the connection specific user data * (mg_request_info->conn_data, mg_get_user_connection_data). * When the callback is called, it is not yet known if a * valid HTTP(S) request will be made. * Parameters: * conn: not yet fully initialized connection object * conn_data: output parameter, set to initialize the * connection specific user data * Return value: * must be 0 * Otherwise, the result is undefined */ int (*init_connection)(const struct mg_connection *conn, void **conn_data); }; /* Start web server. Parameters: callbacks: mg_callbacks structure with user-defined callbacks. options: NULL terminated list of option_name, option_value pairs that specify Civetweb configuration parameters. Side-effects: on UNIX, ignores SIGCHLD and SIGPIPE signals. If custom processing is required for these, signal handlers must be set up after calling mg_start(). Example: const char *options[] = { "document_root", "/var/www", "listening_ports", "80,443s", NULL }; struct mg_context *ctx = mg_start(&my_func, NULL, options); Refer to https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md for the list of valid option and their possible values. Return: web server context, or NULL on error. */ CIVETWEB_API struct mg_context *mg_start(const struct mg_callbacks *callbacks, void *user_data, const char **configuration_options); /* Stop the web server. Must be called last, when an application wants to stop the web server and release all associated resources. This function blocks until all Civetweb threads are stopped. Context pointer becomes invalid. */ CIVETWEB_API void mg_stop(struct mg_context *); #if defined(MG_EXPERIMENTAL_INTERFACES) /* Add an additional domain to an already running web server. * * Parameters: * ctx: Context handle of a server started by mg_start. * options: NULL terminated list of option_name, option_value pairs that * specify CivetWeb configuration parameters. * * Return: * < 0 in case of an error * -1 for a parameter error * -2 invalid options * -3 initializing SSL failed * -4 mandatory domain option missing * -5 duplicate domain * -6 out of memory * > 0 index / handle of a new domain */ CIVETWEB_API int mg_start_domain(struct mg_context *ctx, const char **configuration_options); #endif /* mg_request_handler Called when a new request comes in. This callback is URI based and configured with mg_set_request_handler(). Parameters: conn: current connection information. cbdata: the callback data configured with mg_set_request_handler(). Returns: 0: the handler could not handle the request, so fall through. 1 - 999: the handler processed the request. The return code is stored as a HTTP status code for the access log. */ typedef int (*mg_request_handler)(struct mg_connection *conn, void *cbdata); /* mg_set_request_handler Sets or removes a URI mapping for a request handler. This function uses mg_lock_context internally. URI's are ordered and prefixed URI's are supported. For example, consider two URIs: /a/b and /a /a matches /a /a/b matches /a/b /a/c matches /a Parameters: ctx: server context uri: the URI (exact or pattern) for the handler handler: the callback handler to use when the URI is requested. If NULL, an already registered handler for this URI will be removed. The URI used to remove a handler must match exactly the one used to register it (not only a pattern match). cbdata: the callback data to give to the handler when it is called. */ CIVETWEB_API void mg_set_request_handler(struct mg_context *ctx, const char *uri, mg_request_handler handler, void *cbdata); /* Callback types for websocket handlers in C/C++. mg_websocket_connect_handler Is called when the client intends to establish a websocket connection, before websocket handshake. Return value: 0: civetweb proceeds with websocket handshake. 1: connection is closed immediately. mg_websocket_ready_handler Is called when websocket handshake is successfully completed, and connection is ready for data exchange. mg_websocket_data_handler Is called when a data frame has been received from the client. Parameters: bits: first byte of the websocket frame, see websocket RFC at http://tools.ietf.org/html/rfc6455, section 5.2 data, data_len: payload, with mask (if any) already applied. Return value: 1: keep this websocket connection open. 0: close this websocket connection. mg_connection_close_handler Is called, when the connection is closed.*/ typedef int (*mg_websocket_connect_handler)(const struct mg_connection *, void *); typedef void (*mg_websocket_ready_handler)(struct mg_connection *, void *); typedef int (*mg_websocket_data_handler)(struct mg_connection *, int, char *, size_t, void *); typedef void (*mg_websocket_close_handler)(const struct mg_connection *, void *); /* struct mg_websocket_subprotocols * * List of accepted subprotocols */ struct mg_websocket_subprotocols { int nb_subprotocols; char **subprotocols; }; /* mg_set_websocket_handler Set or remove handler functions for websocket connections. This function works similar to mg_set_request_handler - see there. */ CIVETWEB_API void mg_set_websocket_handler(struct mg_context *ctx, const char *uri, mg_websocket_connect_handler connect_handler, mg_websocket_ready_handler ready_handler, mg_websocket_data_handler data_handler, mg_websocket_close_handler close_handler, void *cbdata); /* mg_set_websocket_handler Set or remove handler functions for websocket connections. This function works similar to mg_set_request_handler - see there. */ CIVETWEB_API void mg_set_websocket_handler_with_subprotocols( struct mg_context *ctx, const char *uri, struct mg_websocket_subprotocols *subprotocols, mg_websocket_connect_handler connect_handler, mg_websocket_ready_handler ready_handler, mg_websocket_data_handler data_handler, mg_websocket_close_handler close_handler, void *cbdata); /* mg_authorization_handler Callback function definition for mg_set_auth_handler Parameters: conn: current connection information. cbdata: the callback data configured with mg_set_request_handler(). Returns: 0: access denied 1: access granted */ typedef int (*mg_authorization_handler)(struct mg_connection *conn, void *cbdata); /* mg_set_auth_handler Sets or removes a URI mapping for an authorization handler. This function works similar to mg_set_request_handler - see there. */ CIVETWEB_API void mg_set_auth_handler(struct mg_context *ctx, const char *uri, mg_authorization_handler handler, void *cbdata); /* Get the value of particular configuration parameter. The value returned is read-only. Civetweb does not allow changing configuration at run time. If given parameter name is not valid, NULL is returned. For valid names, return value is guaranteed to be non-NULL. If parameter is not set, zero-length string is returned. */ CIVETWEB_API const char *mg_get_option(const struct mg_context *ctx, const char *name); /* Get context from connection. */ CIVETWEB_API struct mg_context * mg_get_context(const struct mg_connection *conn); /* Get user data passed to mg_start from context. */ CIVETWEB_API void *mg_get_user_data(const struct mg_context *ctx); /* Get user data passed to mg_start from connection. */ CIVETWEB_API void *mg_get_user_context_data(const struct mg_connection *conn); /* Get user defined thread pointer for server threads (see init_thread). */ CIVETWEB_API void *mg_get_thread_pointer(const struct mg_connection *conn); /* Set user data for the current connection. */ /* Note: This function is deprecated. Use the init_connection callback instead to initialize the user connection data pointer. It is reccomended to supply a pointer to some user defined data structure as conn_data initializer in init_connection. In case it is required to change some data after the init_connection call, store another data pointer in the user defined data structure and modify that pointer. In either case, after the init_connection callback, only calls to mg_get_user_connection_data should be required. */ CIVETWEB_API void mg_set_user_connection_data(struct mg_connection *conn, void *data); /* Get user data set for the current connection. */ CIVETWEB_API void * mg_get_user_connection_data(const struct mg_connection *conn); /* Get a formatted link corresponding to the current request Parameters: conn: current connection information. buf: string buffer (out) buflen: length of the string buffer Returns: <0: error >=0: ok */ CIVETWEB_API int mg_get_request_link(const struct mg_connection *conn, char *buf, size_t buflen); #if defined(MG_LEGACY_INTERFACE) /* 2014-02-21 */ /* Return array of strings that represent valid configuration options. For each option, option name and default value is returned, i.e. the number of entries in the array equals to number_of_options x 2. Array is NULL terminated. */ /* Deprecated: Use mg_get_valid_options instead. */ CIVETWEB_API const char **mg_get_valid_option_names(void); #endif struct mg_option { const char *name; int type; const char *default_value; }; /* Old nomenclature */ #if defined(MG_LEGACY_INTERFACE) /* 2017-10-05 */ enum { CONFIG_TYPE_UNKNOWN = 0x0, CONFIG_TYPE_NUMBER = 0x1, CONFIG_TYPE_STRING = 0x2, CONFIG_TYPE_FILE = 0x3, CONFIG_TYPE_DIRECTORY = 0x4, CONFIG_TYPE_BOOLEAN = 0x5, CONFIG_TYPE_EXT_PATTERN = 0x6, CONFIG_TYPE_STRING_LIST = 0x7, CONFIG_TYPE_STRING_MULTILINE = 0x8 }; #endif /* New nomenclature */ enum { MG_CONFIG_TYPE_UNKNOWN = 0x0, MG_CONFIG_TYPE_NUMBER = 0x1, MG_CONFIG_TYPE_STRING = 0x2, MG_CONFIG_TYPE_FILE = 0x3, MG_CONFIG_TYPE_DIRECTORY = 0x4, MG_CONFIG_TYPE_BOOLEAN = 0x5, MG_CONFIG_TYPE_EXT_PATTERN = 0x6, MG_CONFIG_TYPE_STRING_LIST = 0x7, MG_CONFIG_TYPE_STRING_MULTILINE = 0x8, MG_CONFIG_TYPE_YES_NO_OPTIONAL = 0x9 }; /* Return array of struct mg_option, representing all valid configuration options of civetweb.c. The array is terminated by a NULL name option. */ CIVETWEB_API const struct mg_option *mg_get_valid_options(void); struct mg_server_port { int protocol; /* 1 = IPv4, 2 = IPv6, 3 = both */ int port; /* port number */ int is_ssl; /* https port: 0 = no, 1 = yes */ int is_redirect; /* redirect all requests: 0 = no, 1 = yes */ int _reserved1; int _reserved2; int _reserved3; int _reserved4; }; /* Legacy name */ #define mg_server_ports mg_server_port /* Get the list of ports that civetweb is listening on. The parameter size is the size of the ports array in elements. The caller is responsibility to allocate the required memory. This function returns the number of struct mg_server_port elements filled in, or <0 in case of an error. */ CIVETWEB_API int mg_get_server_ports(const struct mg_context *ctx, int size, struct mg_server_port *ports); #if defined(MG_LEGACY_INTERFACE) /* 2017-04-02 */ /* Deprecated: Use mg_get_server_ports instead. */ CIVETWEB_API size_t mg_get_ports(const struct mg_context *ctx, size_t size, int *ports, int *ssl); #endif /* Add, edit or delete the entry in the passwords file. * * This function allows an application to manipulate .htpasswd files on the * fly by adding, deleting and changing user records. This is one of the * several ways of implementing authentication on the server side. For another, * cookie-based way please refer to the examples/chat in the source tree. * * Parameter: * passwords_file_name: Path and name of a file storing multiple passwords * realm: HTTP authentication realm (authentication domain) name * user: User name * password: * If password is not NULL, entry modified or added. * If password is NULL, entry is deleted. * * Return: * 1 on success, 0 on error. */ CIVETWEB_API int mg_modify_passwords_file(const char *passwords_file_name, const char *realm, const char *user, const char *password); /* Return information associated with the request. * Use this function to implement a server and get data about a request * from a HTTP/HTTPS client. * Note: Before CivetWeb 1.10, this function could be used to read * a response from a server, when implementing a client, although the * values were never returned in appropriate mg_request_info elements. * It is strongly advised to use mg_get_response_info for clients. */ CIVETWEB_API const struct mg_request_info * mg_get_request_info(const struct mg_connection *); /* Return information associated with a HTTP/HTTPS response. * Use this function in a client, to check the response from * the server. */ CIVETWEB_API const struct mg_response_info * mg_get_response_info(const struct mg_connection *); /* Send data to the client. Return: 0 when the connection has been closed -1 on error >0 number of bytes written on success */ CIVETWEB_API int mg_write(struct mg_connection *, const void *buf, size_t len); /* Send data to a websocket client wrapped in a websocket frame. Uses mg_lock_connection to ensure that the transmission is not interrupted, i.e., when the application is proactively communicating and responding to a request simultaneously. Send data to a websocket client wrapped in a websocket frame. This function is available when civetweb is compiled with -DUSE_WEBSOCKET Return: 0 when the connection has been closed -1 on error >0 number of bytes written on success */ CIVETWEB_API int mg_websocket_write(struct mg_connection *conn, int opcode, const char *data, size_t data_len); /* Send data to a websocket server wrapped in a masked websocket frame. Uses mg_lock_connection to ensure that the transmission is not interrupted, i.e., when the application is proactively communicating and responding to a request simultaneously. Send data to a websocket server wrapped in a masked websocket frame. This function is available when civetweb is compiled with -DUSE_WEBSOCKET Return: 0 when the connection has been closed -1 on error >0 number of bytes written on success */ CIVETWEB_API int mg_websocket_client_write(struct mg_connection *conn, int opcode, const char *data, size_t data_len); /* Blocks until unique access is obtained to this connection. Intended for use with websockets only. Invoke this before mg_write or mg_printf when communicating with a websocket if your code has server-initiated communication as well as communication in direct response to a message. */ CIVETWEB_API void mg_lock_connection(struct mg_connection *conn); CIVETWEB_API void mg_unlock_connection(struct mg_connection *conn); #if defined(MG_LEGACY_INTERFACE) /* 2014-06-21 */ #define mg_lock mg_lock_connection #define mg_unlock mg_unlock_connection #endif /* Lock server context. This lock may be used to protect resources that are shared between different connection/worker threads. */ CIVETWEB_API void mg_lock_context(struct mg_context *ctx); CIVETWEB_API void mg_unlock_context(struct mg_context *ctx); /* Opcodes, from http://tools.ietf.org/html/rfc6455 */ #if defined(MG_LEGACY_INTERFACE) /* 2017-10-05 */ enum { WEBSOCKET_OPCODE_CONTINUATION = 0x0, WEBSOCKET_OPCODE_TEXT = 0x1, WEBSOCKET_OPCODE_BINARY = 0x2, WEBSOCKET_OPCODE_CONNECTION_CLOSE = 0x8, WEBSOCKET_OPCODE_PING = 0x9, WEBSOCKET_OPCODE_PONG = 0xa }; #endif /* New nomenclature */ enum { MG_WEBSOCKET_OPCODE_CONTINUATION = 0x0, MG_WEBSOCKET_OPCODE_TEXT = 0x1, MG_WEBSOCKET_OPCODE_BINARY = 0x2, MG_WEBSOCKET_OPCODE_CONNECTION_CLOSE = 0x8, MG_WEBSOCKET_OPCODE_PING = 0x9, MG_WEBSOCKET_OPCODE_PONG = 0xa }; /* Macros for enabling compiler-specific checks for printf-like arguments. */ #undef PRINTF_FORMAT_STRING #if defined(_MSC_VER) && _MSC_VER >= 1400 #include #if defined(_MSC_VER) && _MSC_VER > 1400 #define PRINTF_FORMAT_STRING(s) _Printf_format_string_ s #else #define PRINTF_FORMAT_STRING(s) __format_string s #endif #else #define PRINTF_FORMAT_STRING(s) s #endif // #ifdef __GNUC__ // #define PRINTF_ARGS(x, y) __attribute__((format(printf, x, y))) // #else #define PRINTF_ARGS(x, y) // #endif /* Send data to the client using printf() semantics. Works exactly like mg_write(), but allows to do message formatting. */ CIVETWEB_API int mg_printf(struct mg_connection *, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(2, 3); /* Send a part of the message body, if chunked transfer encoding is set. * Only use this function after sending a complete HTTP request or response * header with "Transfer-Encoding: chunked" set. */ CIVETWEB_API int mg_send_chunk(struct mg_connection *conn, const char *chunk, unsigned int chunk_len); /* Send contents of the entire file together with HTTP headers. * Parameters: * conn: Current connection information. * path: Full path to the file to send. * This function has been superseded by mg_send_mime_file */ CIVETWEB_API void mg_send_file(struct mg_connection *conn, const char *path); /* Send contents of the file without HTTP headers. * The code must send a valid HTTP response header before using this function. * * Parameters: * conn: Current connection information. * path: Full path to the file to send. * * Return: * < 0 Error */ CIVETWEB_API int mg_send_file_body(struct mg_connection *conn, const char *path); /* Send HTTP error reply. */ CIVETWEB_API int mg_send_http_error(struct mg_connection *conn, int status_code, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(3, 4); /* Send "HTTP 200 OK" response header. * After calling this function, use mg_write or mg_send_chunk to send the * response body. * Parameters: * conn: Current connection handle. * mime_type: Set Content-Type for the following content. * content_length: Size of the following content, if content_length >= 0. * Will set transfer-encoding to chunked, if set to -1. * Return: * < 0 Error */ CIVETWEB_API int mg_send_http_ok(struct mg_connection *conn, const char *mime_type, long long content_length); /* Send "HTTP 30x" redirect response. * The response has content-size zero: do not send any body data after calling * this function. * Parameters: * conn: Current connection handle. * target_url: New location. * redirect_code: HTTP redirect type. Could be 301, 302, 303, 307, 308. * Return: * < 0 Error (-1 send error, -2 parameter error) */ CIVETWEB_API int mg_send_http_redirect(struct mg_connection *conn, const char *target_url, int redirect_code); /* Send HTTP digest access authentication request. * Browsers will send a user name and password in their next request, showing * an authentication dialog if the password is not stored. * Parameters: * conn: Current connection handle. * realm: Authentication realm. If NULL is supplied, the sever domain * set in the authentication_domain configuration is used. * Return: * < 0 Error */ CIVETWEB_API int mg_send_digest_access_authentication_request(struct mg_connection *conn, const char *realm); /* Check if the current request has a valid authentication token set. * A file is used to provide a list of valid user names, realms and * password hashes. The file can be created and modified using the * mg_modify_passwords_file API function. * Parameters: * conn: Current connection handle. * realm: Authentication realm. If NULL is supplied, the sever domain * set in the authentication_domain configuration is used. * filename: Path and name of a file storing multiple password hashes. * Return: * > 0 Valid authentication * 0 Invalid authentication * < 0 Error (all values < 0 should be considered as invalid * authentication, future error codes will have negative * numbers) * -1 Parameter error * -2 File not found */ CIVETWEB_API int mg_check_digest_access_authentication(struct mg_connection *conn, const char *realm, const char *filename); /* Send contents of the entire file together with HTTP headers. * Parameters: * conn: Current connection handle. * path: Full path to the file to send. * mime_type: Content-Type for file. NULL will cause the type to be * looked up by the file extension. */ CIVETWEB_API void mg_send_mime_file(struct mg_connection *conn, const char *path, const char *mime_type); /* Send contents of the entire file together with HTTP headers. Parameters: conn: Current connection information. path: Full path to the file to send. mime_type: Content-Type for file. NULL will cause the type to be looked up by the file extension. additional_headers: Additional custom header fields appended to the header. Each header should start with an X-, to ensure it is not included twice. NULL does not append anything. */ CIVETWEB_API void mg_send_mime_file2(struct mg_connection *conn, const char *path, const char *mime_type, const char *additional_headers); /* Store body data into a file. */ CIVETWEB_API long long mg_store_body(struct mg_connection *conn, const char *path); /* Read entire request body and store it in a file "path". Return: < 0 Error >= 0 Number of bytes stored in file "path". */ /* Read data from the remote end, return number of bytes read. Return: 0 connection has been closed by peer. No more data could be read. < 0 read error. No more data could be read from the connection. > 0 number of bytes read into the buffer. */ CIVETWEB_API int mg_read(struct mg_connection *, void *buf, size_t len); /* Get the value of particular HTTP header. This is a helper function. It traverses request_info->http_headers array, and if the header is present in the array, returns its value. If it is not present, NULL is returned. */ CIVETWEB_API const char *mg_get_header(const struct mg_connection *, const char *name); /* Get a value of particular form variable. Parameters: data: pointer to form-uri-encoded buffer. This could be either POST data, or request_info.query_string. data_len: length of the encoded data. var_name: variable name to decode from the buffer dst: destination buffer for the decoded variable dst_len: length of the destination buffer Return: On success, length of the decoded variable. On error: -1 (variable not found). -2 (destination buffer is NULL, zero length or too small to hold the decoded variable). Destination buffer is guaranteed to be '\0' - terminated if it is not NULL or zero length. */ CIVETWEB_API int mg_get_var(const char *data, size_t data_len, const char *var_name, char *dst, size_t dst_len); /* Get a value of particular form variable. Parameters: data: pointer to form-uri-encoded buffer. This could be either POST data, or request_info.query_string. data_len: length of the encoded data. var_name: variable name to decode from the buffer dst: destination buffer for the decoded variable dst_len: length of the destination buffer occurrence: which occurrence of the variable, 0 is the first, 1 the second... this makes it possible to parse a query like b=x&a=y&a=z which will have occurrence values b:0, a:0 and a:1 Return: On success, length of the decoded variable. On error: -1 (variable not found). -2 (destination buffer is NULL, zero length or too small to hold the decoded variable). Destination buffer is guaranteed to be '\0' - terminated if it is not NULL or zero length. */ CIVETWEB_API int mg_get_var2(const char *data, size_t data_len, const char *var_name, char *dst, size_t dst_len, size_t occurrence); /* Fetch value of certain cookie variable into the destination buffer. Destination buffer is guaranteed to be '\0' - terminated. In case of failure, dst[0] == '\0'. Note that RFC allows many occurrences of the same parameter. This function returns only first occurrence. Return: On success, value length. On error: -1 (either "Cookie:" header is not present at all or the requested parameter is not found). -2 (destination buffer is NULL, zero length or too small to hold the value). */ CIVETWEB_API int mg_get_cookie(const char *cookie, const char *var_name, char *buf, size_t buf_len); /* Download data from the remote web server. host: host name to connect to, e.g. "foo.com", or "10.12.40.1". port: port number, e.g. 80. use_ssl: whether to use SSL connection. error_buffer, error_buffer_size: error message placeholder. request_fmt,...: HTTP request. Return: On success, valid pointer to the new connection, suitable for mg_read(). On error, NULL. error_buffer contains error message. Example: char ebuf[100]; struct mg_connection *conn; conn = mg_download("google.com", 80, 0, ebuf, sizeof(ebuf), "%s", "GET / HTTP/1.0\r\nHost: google.com\r\n\r\n"); mg_download is equivalent to calling mg_connect_client followed by mg_printf and mg_get_response. Using these three functions directly may allow more control as compared to using mg_download. */ CIVETWEB_API struct mg_connection * mg_download(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size, PRINTF_FORMAT_STRING(const char *request_fmt), ...) PRINTF_ARGS(6, 7); /* Close the connection opened by mg_download(). */ CIVETWEB_API void mg_close_connection(struct mg_connection *conn); #if defined(MG_LEGACY_INTERFACE) /* 2016-05-14 */ /* File upload functionality. Each uploaded file gets saved into a temporary file and MG_UPLOAD event is sent. Return number of uploaded files. Deprecated: Use mg_handle_form_request instead. */ CIVETWEB_API int mg_upload(struct mg_connection *conn, const char *destination_dir); #endif /* This structure contains callback functions for handling form fields. It is used as an argument to mg_handle_form_request. */ struct mg_form_data_handler { /* This callback function is called, if a new field has been found. * The return value of this callback is used to define how the field * should be processed. * * Parameters: * key: Name of the field ("name" property of the HTML input field). * filename: Name of a file to upload, at the client computer. * Only set for input fields of type "file", otherwise NULL. * path: Output parameter: File name (incl. path) to store the file * at the server computer. Only used if FORM_FIELD_STORAGE_STORE * is returned by this callback. Existing files will be * overwritten. * pathlen: Length of the buffer for path. * user_data: Value of the member user_data of mg_form_data_handler * * Return value: * The callback must return the intended storage for this field * (See FORM_FIELD_STORAGE_*). */ int (*field_found)(const char *key, const char *filename, char *path, size_t pathlen, void *user_data); /* If the "field_found" callback returned FORM_FIELD_STORAGE_GET, * this callback will receive the field data. * * Parameters: * key: Name of the field ("name" property of the HTML input field). * value: Value of the input field. * user_data: Value of the member user_data of mg_form_data_handler * * Return value: * The return code determines how the server should continue processing * the current request (See MG_FORM_FIELD_HANDLE_*). */ int (*field_get)(const char *key, const char *value, size_t valuelen, void *user_data); /* If the "field_found" callback returned FORM_FIELD_STORAGE_STORE, * the data will be stored into a file. If the file has been written * successfully, this callback will be called. This callback will * not be called for only partially uploaded files. The * mg_handle_form_request function will either store the file completely * and call this callback, or it will remove any partial content and * not call this callback function. * * Parameters: * path: Path of the file stored at the server. * file_size: Size of the stored file in bytes. * user_data: Value of the member user_data of mg_form_data_handler * * Return value: * The return code determines how the server should continue processing * the current request (See MG_FORM_FIELD_HANDLE_*). */ int (*field_store)(const char *path, long long file_size, void *user_data); /* User supplied argument, passed to all callback functions. */ void *user_data; }; /* Return values definition for the "field_found" callback in * mg_form_data_handler. */ #if defined(MG_LEGACY_INTERFACE) /* 2017-10-05 */ enum { /* Skip this field (neither get nor store it). Continue with the * next field. */ FORM_FIELD_STORAGE_SKIP = 0x0, /* Get the field value. */ FORM_FIELD_STORAGE_GET = 0x1, /* Store the field value into a file. */ FORM_FIELD_STORAGE_STORE = 0x2, /* Stop parsing this request. Skip the remaining fields. */ FORM_FIELD_STORAGE_ABORT = 0x10 }; #endif /* New nomenclature */ enum { /* Skip this field (neither get nor store it). Continue with the * next field. */ MG_FORM_FIELD_STORAGE_SKIP = 0x0, /* Get the field value. */ MG_FORM_FIELD_STORAGE_GET = 0x1, /* Store the field value into a file. */ MG_FORM_FIELD_STORAGE_STORE = 0x2, /* Stop parsing this request. Skip the remaining fields. */ MG_FORM_FIELD_STORAGE_ABORT = 0x10 }; /* Return values for "field_get" and "field_store" */ enum { /* Only "field_get": If there is more data in this field, get the next * chunk. Otherwise: handle the next field. */ MG_FORM_FIELD_HANDLE_GET = 0x1, /* Handle the next field */ MG_FORM_FIELD_HANDLE_NEXT = 0x8, /* Stop parsing this request */ MG_FORM_FIELD_HANDLE_ABORT = 0x10 }; /* Process form data. * Returns the number of fields handled, or < 0 in case of an error. * Note: It is possible that several fields are already handled successfully * (e.g., stored into files), before the request handling is stopped with an * error. In this case a number < 0 is returned as well. * In any case, it is the duty of the caller to remove files once they are * no longer required. */ CIVETWEB_API int mg_handle_form_request(struct mg_connection *conn, struct mg_form_data_handler *fdh); /* Convenience function -- create detached thread. Return: 0 on success, non-0 on error. */ typedef void *(*mg_thread_func_t)(void *); CIVETWEB_API int mg_start_thread(mg_thread_func_t f, void *p); /* Return builtin mime type for the given file name. For unrecognized extensions, "text/plain" is returned. */ CIVETWEB_API const char *mg_get_builtin_mime_type(const char *file_name); /* Get text representation of HTTP status code. */ CIVETWEB_API const char * mg_get_response_code_text(const struct mg_connection *conn, int response_code); /* Return CivetWeb version. */ CIVETWEB_API const char *mg_version(void); /* URL-decode input buffer into destination buffer. 0-terminate the destination buffer. form-url-encoded data differs from URI encoding in a way that it uses '+' as character for space, see RFC 1866 section 8.2.1 http://ftp.ics.uci.edu/pub/ietf/html/rfc1866.txt Return: length of the decoded data, or -1 if dst buffer is too small. */ CIVETWEB_API int mg_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded); /* URL-encode input buffer into destination buffer. returns the length of the resulting buffer or -1 is the buffer is too small. */ CIVETWEB_API int mg_url_encode(const char *src, char *dst, size_t dst_len); /* MD5 hash given strings. Buffer 'buf' must be 33 bytes long. Varargs is a NULL terminated list of ASCIIz strings. When function returns, buf will contain human-readable MD5 hash. Example: char buf[33]; mg_md5(buf, "aa", "bb", NULL); */ CIVETWEB_API char *mg_md5(char buf[33], ...); /* Print error message to the opened error log stream. This utilizes the provided logging configuration. conn: connection (not used for sending data, but to get perameters) fmt: format string without the line return ...: variable argument list Example: mg_cry(conn,"i like %s", "logging"); */ CIVETWEB_API void mg_cry(const struct mg_connection *conn, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(2, 3); /* utility methods to compare two buffers, case insensitive. */ CIVETWEB_API int mg_strcasecmp(const char *s1, const char *s2); CIVETWEB_API int mg_strncasecmp(const char *s1, const char *s2, size_t len); /* Connect to a websocket as a client Parameters: host: host to connect to, i.e. "echo.websocket.org" or "192.168.1.1" or "localhost" port: server port use_ssl: make a secure connection to server error_buffer, error_buffer_size: buffer for an error message path: server path you are trying to connect to, i.e. if connection to localhost/app, path should be "/app" origin: value of the Origin HTTP header data_func: callback that should be used when data is received from the server user_data: user supplied argument Return: On success, valid mg_connection object. On error, NULL. Se error_buffer for details. */ CIVETWEB_API struct mg_connection * mg_connect_websocket_client(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data); /* Connect to a TCP server as a client (can be used to connect to a HTTP server) Parameters: host: host to connect to, i.e. "www.wikipedia.org" or "192.168.1.1" or "localhost" port: server port use_ssl: make a secure connection to server error_buffer, error_buffer_size: buffer for an error message Return: On success, valid mg_connection object. On error, NULL. Se error_buffer for details. */ CIVETWEB_API struct mg_connection *mg_connect_client(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size); struct mg_client_options { const char *host; int port; const char *client_cert; const char *server_cert; const char *host_name; /* TODO: add more data */ }; CIVETWEB_API struct mg_connection * mg_connect_client_secure(const struct mg_client_options *client_options, char *error_buffer, size_t error_buffer_size); #if defined(MG_LEGACY_INTERFACE) /* 2019-11-02 */ enum { TIMEOUT_INFINITE = -1 }; #endif enum { MG_TIMEOUT_INFINITE = -1 }; /* Wait for a response from the server Parameters: conn: connection ebuf, ebuf_len: error message placeholder. timeout: time to wait for a response in milliseconds (if < 0 then wait forever) Return: On success, >= 0 On error/timeout, < 0 */ CIVETWEB_API int mg_get_response(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int timeout); /* Check which features where set when the civetweb library has been compiled. The function explicitly addresses compile time defines used when building the library - it does not mean, the feature has been initialized using a mg_init_library call. mg_check_feature can be called anytime, even before mg_init_library has been called. Parameters: feature: specifies which feature should be checked The value is a bit mask. The individual bits are defined as: 1 serve files (NO_FILES not set) 2 support HTTPS (NO_SSL not set) 4 support CGI (NO_CGI not set) 8 support IPv6 (USE_IPV6 set) 16 support WebSocket (USE_WEBSOCKET set) 32 support Lua scripts and Lua server pages (USE_LUA is set) 64 support server side JavaScript (USE_DUKTAPE is set) 128 support caching (NO_CACHING not set) 256 support server statistics (USE_SERVER_STATS is set) 512 support for on the fly compression (USE_ZLIB is set) These values are defined as MG_FEATURES_* The result is undefined, if bits are set that do not represent a defined feature (currently: feature >= 1024). The result is undefined, if no bit is set (feature == 0). Return: If a feature is available, the corresponding bit is set If a feature is not available, the bit is 0 */ CIVETWEB_API unsigned mg_check_feature(unsigned feature); /* Get information on the system. Useful for support requests. Parameters: buffer: Store system information as string here. buflen: Length of buffer (including a byte required for a terminating 0). Return: Available size of system information, exluding a terminating 0. The information is complete, if the return value is smaller than buflen. The result is a JSON formatted string, the exact content may vary. Note: It is possible to determine the required buflen, by first calling this function with buffer = NULL and buflen = NULL. The required buflen is one byte more than the returned value. */ CIVETWEB_API int mg_get_system_info(char *buffer, int buflen); /* Get context information. Useful for server diagnosis. Parameters: ctx: Context handle buffer: Store context information here. buflen: Length of buffer (including a byte required for a terminating 0). Return: Available size of system information, exluding a terminating 0. The information is complete, if the return value is smaller than buflen. The result is a JSON formatted string, the exact content may vary. Note: It is possible to determine the required buflen, by first calling this function with buffer = NULL and buflen = NULL. The required buflen is one byte more than the returned value. However, since the available context information changes, you should allocate a few bytes more. */ CIVETWEB_API int mg_get_context_info(const struct mg_context *ctx, char *buffer, int buflen); #if defined(MG_EXPERIMENTAL_INTERFACES) /* Get connection information. Useful for server diagnosis. Parameters: ctx: Context handle idx: Connection index buffer: Store context information here. buflen: Length of buffer (including a byte required for a terminating 0). Return: Available size of system information, exluding a terminating 0. The information is complete, if the return value is smaller than buflen. The result is a JSON formatted string, the exact content may vary. Note: It is possible to determine the required buflen, by first calling this function with buffer = NULL and buflen = NULL. The required buflen is one byte more than the returned value. However, since the available context information changes, you should allocate a few bytes more. */ CIVETWEB_API int mg_get_connection_info(const struct mg_context *ctx, int idx, char *buffer, int buflen); #endif /* New APIs for enhanced option and error handling. These mg_*2 API functions have the same purpose as their original versions, but provide additional options and/or provide improved error diagnostics. Note: Experimental interfaces may change */ struct mg_error_data { unsigned *code; /* error code (number) */ char *text; /* buffer for error text */ size_t text_buffer_size; /* size of buffer of "text" */ }; struct mg_init_data { const struct mg_callbacks *callbacks; /* callback function pointer */ void *user_data; /* data */ const char **configuration_options; }; #if defined(MG_EXPERIMENTAL_INTERFACES) CIVETWEB_API struct mg_connection * mg_connect_client2(const char *host, const char *protocol, int port, const char *path, struct mg_init_data *init, struct mg_error_data *error); CIVETWEB_API int mg_get_response2(struct mg_connection *conn, struct mg_error_data *error, int timeout); CIVETWEB_API struct mg_context *mg_start2(struct mg_init_data *init, struct mg_error_data *error); CIVETWEB_API int mg_start_domain2(struct mg_context *ctx, const char **configuration_options, struct mg_error_data *error); #endif #ifdef __cplusplus } #endif /* __cplusplus */ #endif /* CIVETWEB_HEADER_INCLUDED */ webfakes/src/errors.c0000644000176200001440000000415614535455671014335 0ustar liggesusers #include "errors.h" #include #include #define ERRORBUF_SIZE 4096 static char errorbuf[ERRORBUF_SIZE]; SEXP r_throw_error(const char *func, const char *filename, int line, const char *msg, ...) { va_list args; errorbuf[0] = '\0'; va_start(args, msg); vsnprintf(errorbuf, ERRORBUF_SIZE, msg, args); va_end (args); error("%s @%s:%d (%s)", errorbuf, filename, line, func); return R_NilValue; } #ifdef _WIN32 SEXP r_throw_system_error(const char *func, const char *filename, int line, DWORD errorcode, const char *sysmsg, const char *msg, ...) { va_list args; LPVOID lpMsgBuf; char *realsysmsg = sysmsg ? (char*) sysmsg : NULL; char *failmsg = "Formatting the system message failed :("; if (errorcode == -1) errorcode = GetLastError(); if (!realsysmsg) { DWORD ret = FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorcode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); if (ret == 0) { realsysmsg = failmsg; } else { realsysmsg = R_alloc(1, strlen(lpMsgBuf) + 1); strcpy(realsysmsg, lpMsgBuf); LocalFree(lpMsgBuf); } } errorbuf[0] = '\0'; va_start(args, msg); vsnprintf(errorbuf, ERRORBUF_SIZE, msg, args); va_end(args); error("%s (system error %ld, %s) @%s:%d (%s)", errorbuf, errorcode, realsysmsg, filename, line, func); return R_NilValue; } #endif #ifdef _WIN32 SEXP r_throw_posix_error( #else SEXP r_throw_system_error( #endif const char *func, const char *filename, int line, int errorcode, const char *sysmsg, const char *msg, ...) { va_list args; if (!sysmsg) sysmsg = strerror(errorcode); errorbuf[0] = '\0'; va_start(args, msg); vsnprintf(errorbuf, ERRORBUF_SIZE, msg, args); va_end(args); error("%s (system error %d, %s) @%s:%d (%s)", errorbuf, errorcode, sysmsg, filename, line, func); return R_NilValue; } webfakes/src/civetweb.c0000644000176200001440000213204314426614510014614 0ustar liggesusers/* Copyright (c) 2013-2020 the Civetweb developers * Copyright (c) 2004-2013 Sergey Lyubka * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #if defined(__GNUC__) || defined(__MINGW32__) #define GCC_VERSION \ (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) #if GCC_VERSION >= 40500 /* gcc diagnostic pragmas available */ //#define GCC_DIAGNOSTIC #endif #endif #if defined(GCC_DIAGNOSTIC) /* Disable unused macros warnings - not all defines are required * for all systems and all compilers. */ # pragma GCC diagnostic ignored "-Wunused-macros" /* A padding warning is just plain useless */ # pragma GCC diagnostic ignored "-Wpadded" #endif #if defined(__clang__) /* GCC does not (yet) support this pragma */ /* We must set some flags for the headers we include. These flags * are reserved ids according to C99, so we need to disable a * warning for that. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wreserved-id-macro" #endif #if defined(_WIN32) #if !defined(_CRT_SECURE_NO_WARNINGS) #define _CRT_SECURE_NO_WARNINGS /* Disable deprecation warning in VS2005 */ #endif #if !defined(_WIN32_WINNT) /* defined for tdm-gcc so we can use getnameinfo */ #define _WIN32_WINNT 0x0501 #endif #else #if !defined(_GNU_SOURCE) #define _GNU_SOURCE /* for setgroups(), pthread_setname_np() */ #endif #if defined(__linux__) && !defined(_XOPEN_SOURCE) #define _XOPEN_SOURCE 600 /* For flockfile() on Linux */ #endif #if defined(__LSB_VERSION__) || defined(__sun) #define NEED_TIMEGM #define NO_THREAD_NAME #endif #if !defined(_LARGEFILE_SOURCE) #define _LARGEFILE_SOURCE /* For fseeko(), ftello() */ #endif #if !defined(_FILE_OFFSET_BITS) #define _FILE_OFFSET_BITS 64 /* Use 64-bit file offsets by default */ #endif #if !defined(__STDC_FORMAT_MACROS) #define __STDC_FORMAT_MACROS /* wants this for C++ */ #endif #if !defined(__STDC_LIMIT_MACROS) #define __STDC_LIMIT_MACROS /* C++ wants that for INT64_MAX */ #endif #if !defined(_DARWIN_UNLIMITED_SELECT) #define _DARWIN_UNLIMITED_SELECT #endif #if defined(__sun) #define __EXTENSIONS__ /* to expose flockfile and friends in stdio.h */ #define __inline inline /* not recognized on older compiler versions */ #endif #endif #if defined(__clang__) /* Enable reserved-id-macro warning again. */ # pragma GCC diagnostic pop #endif #if defined(USE_LUA) #define USE_TIMERS #endif #if defined(_MSC_VER) /* 'type cast' : conversion from 'int' to 'HANDLE' of greater size */ # pragma warning(disable : 4306) /* conditional expression is constant: introduced by FD_SET(..) */ # pragma warning(disable : 4127) /* non-constant aggregate initializer: issued due to missing C99 support */ # pragma warning(disable : 4204) /* padding added after data member */ # pragma warning(disable : 4820) /* not defined as a preprocessor macro, replacing with '0' for '#if/#elif' */ # pragma warning(disable : 4668) /* no function prototype given: converting '()' to '(void)' */ # pragma warning(disable : 4255) /* function has been selected for automatic inline expansion */ # pragma warning(disable : 4711) #endif /* This code uses static_assert to check some conditions. * Unfortunately some compilers still do not support it, so we have a * replacement function here. */ #if defined(__STDC_VERSION__) && __STDC_VERSION__ > 201100L #define mg_static_assert _Static_assert #elif defined(__cplusplus) && __cplusplus >= 201103L #define mg_static_assert static_assert #else char static_assert_replacement[1]; #define mg_static_assert(cond, txt) \ extern char static_assert_replacement[(cond) ? 1 : -1] #endif mg_static_assert(sizeof(int) == 4 || sizeof(int) == 8, "int data type size check"); mg_static_assert(sizeof(void *) == 4 || sizeof(void *) == 8, "pointer data type size check"); mg_static_assert(sizeof(void *) >= sizeof(int), "data type size check"); /* Select queue implementation. Diagnosis features originally only implemented * for the "ALTERNATIVE_QUEUE" have been ported to the previous queue * implementation (NO_ALTERNATIVE_QUEUE) as well. The new configuration value * "CONNECTION_QUEUE_SIZE" is only available for the previous queue * implementation, since the queue length is independent from the number of * worker threads there, while the new queue is one element per worker thread. * */ #if defined(NO_ALTERNATIVE_QUEUE) && defined(ALTERNATIVE_QUEUE) /* The queues are exclusive or - only one can be used. */ #error \ "Define ALTERNATIVE_QUEUE or NO_ALTERNATIVE_QUEUE (or none of them), but not both" #endif #if !defined(NO_ALTERNATIVE_QUEUE) && !defined(ALTERNATIVE_QUEUE) /* Use a default implementation */ #define NO_ALTERNATIVE_QUEUE #endif #if defined(NO_FILESYSTEMS) && !defined(NO_FILES) /* File system access: * NO_FILES = do not serve any files from the file system automatically. * However, with NO_FILES CivetWeb may still write log files, read access * control files, default error page files or use API functions like * mg_send_file in callbacks to send files from the server local * file system. * NO_FILES only disables the automatic mapping between URLs and local * file names. * NO_FILESYSTEM = do not access any file at all. Useful for embedded * devices without file system. Logging to files in not available * (use callbacks instead) and API functions like mg_send_file are not * available. * If NO_FILESYSTEM is set, NO_FILES must be set as well. */ #error "Inconsistent build flags, NO_FILESYSTEMS requires NO_FILES" #endif /* DTL -- including winsock2.h works better if lean and mean */ #if !defined(WIN32_LEAN_AND_MEAN) #define WIN32_LEAN_AND_MEAN #endif #if defined(__SYMBIAN32__) /* According to https://en.wikipedia.org/wiki/Symbian#History, * Symbian is no longer maintained since 2014-01-01. * Support for Symbian has been removed from CivetWeb */ #error "Symbian is no longer maintained. CivetWeb no longer supports Symbian." #endif /* __SYMBIAN32__ */ #if defined(__ZEPHYR__) #include #include #include #include #include #include #include #include #include #include #include /* Max worker threads is the max of pthreads minus the main application thread * and minus the main civetweb thread, thus -2 */ #define MAX_WORKER_THREADS (CONFIG_MAX_PTHREAD_COUNT - 2) #if defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) #define ZEPHYR_STACK_SIZE USE_STACK_SIZE #else #define ZEPHYR_STACK_SIZE 8096 #endif K_THREAD_STACK_DEFINE(civetweb_main_stack, ZEPHYR_STACK_SIZE); K_THREAD_STACK_ARRAY_DEFINE(civetweb_worker_stacks, MAX_WORKER_THREADS, ZEPHYR_STACK_SIZE); static int zephyr_worker_stack_index; #endif #if !defined(CIVETWEB_HEADER_INCLUDED) /* Include the header file here, so the CivetWeb interface is defined for the * entire implementation, including the following forward definitions. */ #include "civetweb.h" #endif #if !defined(DEBUG_TRACE) #if defined(DEBUG) static void DEBUG_TRACE_FUNC(const char *func, unsigned line, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(3, 4); #define DEBUG_TRACE(fmt, ...) \ DEBUG_TRACE_FUNC(__func__, __LINE__, fmt, __VA_ARGS__) #define NEED_DEBUG_TRACE_FUNC #ifndef DEBUG_TRACE_STREAM # define DEBUG_TRACE_STREAM stdout #endif #else #define DEBUG_TRACE(fmt, ...) \ do { \ } while (0) #endif /* DEBUG */ #endif /* DEBUG_TRACE */ #if !defined(DEBUG_ASSERT) #if defined(DEBUG) #define DEBUG_ASSERT(cond) \ do { \ if (!(cond)) { \ DEBUG_TRACE("ASSERTION FAILED: %s", #cond); \ exit(2); /* Exit with error */ \ } \ } while (0) #else #define DEBUG_ASSERT(cond) #endif /* DEBUG */ #endif #if defined(__GNUC__) && defined(GCC_INSTRUMENTATION) void __cyg_profile_func_enter(void *this_fn, void *call_site) __attribute__((no_instrument_function)); void __cyg_profile_func_exit(void *this_fn, void *call_site) __attribute__((no_instrument_function)); void __cyg_profile_func_enter(void *this_fn, void *call_site) { if ((void *)this_fn != (void *)printf) { printf("E %p %p\n", this_fn, call_site); } } void __cyg_profile_func_exit(void *this_fn, void *call_site) { if ((void *)this_fn != (void *)printf) { printf("X %p %p\n", this_fn, call_site); } } #endif #if !defined(IGNORE_UNUSED_RESULT) #define IGNORE_UNUSED_RESULT(a) ((void)((a) && 1)) #endif #if defined(__GNUC__) || defined(__MINGW32__) /* GCC unused function attribute seems fundamentally broken. * Several attempts to tell the compiler "THIS FUNCTION MAY BE USED * OR UNUSED" for individual functions failed. * Either the compiler creates an "unused-function" warning if a * function is not marked with __attribute__((unused)). * On the other hand, if the function is marked with this attribute, * but is used, the compiler raises a completely idiotic * "used-but-marked-unused" warning - and * # pragma GCC diagnostic ignored "-Wused-but-marked-unused" * raises error: unknown option after "# pragma GCC diagnostic". * Disable this warning completely, until the GCC guys sober up * again. */ # pragma GCC diagnostic ignored "-Wunused-function" #define FUNCTION_MAY_BE_UNUSED /* __attribute__((unused)) */ #else #define FUNCTION_MAY_BE_UNUSED #endif /* Some ANSI #includes are not available on Windows CE */ #if !defined(_WIN32_WCE) && !defined(__ZEPHYR__) #include #include #include #include #include #include #endif /* !_WIN32_WCE */ #if defined(__clang__) /* When using -Weverything, clang does not accept it's own headers * in a release build configuration. Disable what is too much in * -Weverything. */ # pragma clang diagnostic ignored "-Wdisabled-macro-expansion" #endif #if defined(__GNUC__) || defined(__MINGW32__) /* Who on earth came to the conclusion, using __DATE__ should rise * an "expansion of date or time macro is not reproducible" * warning. That's exactly what was intended by using this macro. * Just disable this nonsense warning. */ /* And disabling them does not work either: * # pragma clang diagnostic ignored "-Wno-error=date-time" * # pragma clang diagnostic ignored "-Wdate-time" * So we just have to disable ALL warnings for some lines * of code. * This seems to be a known GCC bug, not resolved since 2012: * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431 */ #endif #if defined(__MACH__) && defined(__APPLE__) /* Apple OSX section */ #if defined(__clang__) #if (__clang_major__ == 3) && ((__clang_minor__ == 7) || (__clang_minor__ == 8)) /* Avoid warnings for Xcode 7. It seems it does no longer exist in Xcode 8 */ # pragma clang diagnostic ignored "-Wno-reserved-id-macro" # pragma clang diagnostic ignored "-Wno-keyword-macro" #endif #endif #define CLOCK_MONOTONIC (1) #define CLOCK_REALTIME (2) #include #include #include #include #include /* clock_gettime is not implemented on OSX prior to 10.12 */ static int _civet_clock_gettime(int clk_id, struct timespec *t) { memset(t, 0, sizeof(*t)); if (clk_id == CLOCK_REALTIME) { struct timeval now; int rv = gettimeofday(&now, NULL); if (rv) { return rv; } t->tv_sec = now.tv_sec; t->tv_nsec = now.tv_usec * 1000; return 0; } else if (clk_id == CLOCK_MONOTONIC) { static uint64_t clock_start_time = 0; static mach_timebase_info_data_t timebase_ifo = {0, 0}; uint64_t now = mach_absolute_time(); if (clock_start_time == 0) { kern_return_t mach_status = mach_timebase_info(&timebase_ifo); DEBUG_ASSERT(mach_status == KERN_SUCCESS); /* appease "unused variable" warning for release builds */ (void)mach_status; clock_start_time = now; } now = (uint64_t)((double)(now - clock_start_time) * (double)timebase_ifo.numer / (double)timebase_ifo.denom); t->tv_sec = now / 1000000000; t->tv_nsec = now % 1000000000; return 0; } return -1; /* EINVAL - Clock ID is unknown */ } /* if clock_gettime is declared, then __CLOCK_AVAILABILITY will be defined */ #if defined(__CLOCK_AVAILABILITY) /* If we compiled with Mac OSX 10.12 or later, then clock_gettime will be * declared but it may be NULL at runtime. So we need to check before using * it. */ static int _civet_safe_clock_gettime(int clk_id, struct timespec *t) { if (clock_gettime) { return clock_gettime(clk_id, t); } return _civet_clock_gettime(clk_id, t); } #define clock_gettime _civet_safe_clock_gettime #else #define clock_gettime _civet_clock_gettime #endif #endif /********************************************************************/ /* CivetWeb configuration defines */ /********************************************************************/ /* Maximum number of threads that can be configured. * The number of threads actually created depends on the "num_threads" * configuration parameter, but this is the upper limit. */ #if !defined(MAX_WORKER_THREADS) #define MAX_WORKER_THREADS (1024 * 64) /* in threads (count) */ #endif /* Timeout interval for select/poll calls. * The timeouts depend on "*_timeout_ms" configuration values, but long * timeouts are split into timouts as small as SOCKET_TIMEOUT_QUANTUM. * This reduces the time required to stop the server. */ #if !defined(SOCKET_TIMEOUT_QUANTUM) #define SOCKET_TIMEOUT_QUANTUM (2000) /* in ms */ #endif /* Do not try to compress files smaller than this limit. */ #if !defined(MG_FILE_COMPRESSION_SIZE_LIMIT) #define MG_FILE_COMPRESSION_SIZE_LIMIT (1024) /* in bytes */ #endif #if !defined(PASSWORDS_FILE_NAME) #define PASSWORDS_FILE_NAME ".htpasswd" #endif /* Initial buffer size for all CGI environment variables. In case there is * not enough space, another block is allocated. */ #if !defined(CGI_ENVIRONMENT_SIZE) #define CGI_ENVIRONMENT_SIZE (4096) /* in bytes */ #endif /* Maximum number of environment variables. */ #if !defined(MAX_CGI_ENVIR_VARS) #define MAX_CGI_ENVIR_VARS (256) /* in variables (count) */ #endif /* General purpose buffer size. */ #if !defined(MG_BUF_LEN) /* in bytes */ #define MG_BUF_LEN (1024 * 8) #endif /********************************************************************/ /* Helper makros */ #if !defined(ARRAY_SIZE) #define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0])) #endif #include /* Standard defines */ #if !defined(INT64_MAX) #define INT64_MAX (9223372036854775807) #endif #define SHUTDOWN_RD (0) #define SHUTDOWN_WR (1) #define SHUTDOWN_BOTH (2) mg_static_assert(MAX_WORKER_THREADS >= 1, "worker threads must be a positive number"); mg_static_assert(sizeof(size_t) == 4 || sizeof(size_t) == 8, "size_t data type size check"); #if defined(_WIN32) /* WINDOWS include block */ #include /* *alloc( */ #include /* *alloc( */ #include /* struct timespec */ #include #include /* DTL add for SO_EXCLUSIVE */ #include typedef const char *SOCK_OPT_TYPE; #if !defined(PATH_MAX) #define W_PATH_MAX (MAX_PATH) /* at most three UTF-8 chars per wchar_t */ #define PATH_MAX (W_PATH_MAX * 3) #else #define W_PATH_MAX ((PATH_MAX + 2) / 3) #endif mg_static_assert(PATH_MAX >= 1, "path length must be a positive number"); #if !defined(_IN_PORT_T) #if !defined(in_port_t) #define in_port_t u_short #endif #endif #if !defined(_WIN32_WCE) #include #include #include #else /* _WIN32_WCE */ #define NO_CGI /* WinCE has no pipes */ #define NO_POPEN /* WinCE has no popen */ typedef long off_t; #define errno ((int)(GetLastError())) #define strerror(x) (_ultoa(x, (char *)_alloca(sizeof(x) * 3), 10)) #endif /* _WIN32_WCE */ #define MAKEUQUAD(lo, hi) \ ((uint64_t)(((uint32_t)(lo)) | ((uint64_t)((uint32_t)(hi))) << 32)) #define RATE_DIFF (10000000) /* 100 nsecs */ #define EPOCH_DIFF (MAKEUQUAD(0xd53e8000, 0x019db1de)) #define SYS2UNIX_TIME(lo, hi) \ ((time_t)((MAKEUQUAD((lo), (hi)) - EPOCH_DIFF) / RATE_DIFF)) /* Visual Studio 6 does not know __func__ or __FUNCTION__ * The rest of MS compilers use __FUNCTION__, not C99 __func__ * Also use _strtoui64 on modern M$ compilers */ #if defined(_MSC_VER) #if (_MSC_VER < 1300) #define STRX(x) #x #define STR(x) STRX(x) #define __func__ __FILE__ ":" STR(__LINE__) #define strtoull(x, y, z) ((unsigned __int64)_atoi64(x)) #define strtoll(x, y, z) (_atoi64(x)) #else #define __func__ __FUNCTION__ #define strtoull(x, y, z) (_strtoui64(x, y, z)) #define strtoll(x, y, z) (_strtoi64(x, y, z)) #endif #endif /* _MSC_VER */ #define ERRNO ((int)(GetLastError())) #define NO_SOCKLEN_T #if defined(_WIN64) || defined(__MINGW64__) #if !defined(SSL_LIB) #if defined(OPENSSL_API_1_1) #define SSL_LIB "libssl-1_1-x64.dll" #else /* OPENSSL_API_1_1 */ #define SSL_LIB "ssleay64.dll" #endif /* OPENSSL_API_1_1 */ #endif /* SSL_LIB */ #if !defined(CRYPTO_LIB) #if defined(OPENSSL_API_1_1) #define CRYPTO_LIB "libcrypto-1_1-x64.dll" #else /* OPENSSL_API_1_1 */ #define CRYPTO_LIB "libeay64.dll" #endif /* OPENSSL_API_1_1 */ #endif /* CRYPTO_LIB */ #else /* defined(_WIN64) || defined(__MINGW64__) */ #if !defined(SSL_LIB) #define SSL_LIB "ssleay32.dll" #endif /* SSL_LIB */ #if !defined(CRYPTO_LIB) #define CRYPTO_LIB "libeay32.dll" #endif /* CRYPTO_LIB */ #endif /* defined(_WIN64) || defined(__MINGW64__) */ #define O_NONBLOCK (0) #if !defined(W_OK) #define W_OK (2) /* http://msdn.microsoft.com/en-us/library/1w06ktdy.aspx */ #endif #define _POSIX_ #define __STDC_FORMAT_MACROS #include #define INT64_FMT PRId64 #define UINT64_FMT PRIu64 #define WINCDECL __cdecl #define vsnprintf_impl _vsnprintf #define access _access #define mg_sleep(x) (Sleep(x)) #define pipe(x) _pipe(x, MG_BUF_LEN, _O_BINARY) #if !defined(popen) #define popen(x, y) (_popen(x, y)) #endif #if !defined(pclose) #define pclose(x) (_pclose(x)) #endif #define close(x) (_close(x)) #define dlsym(x, y) (GetProcAddress((HINSTANCE)(x), (y))) #define RTLD_LAZY (0) #define fseeko(x, y, z) ((_lseeki64(_fileno(x), (y), (z)) == -1) ? -1 : 0) #define fdopen(x, y) (_fdopen((x), (y))) #define write(x, y, z) (_write((x), (y), (unsigned)z)) #define read(x, y, z) (_read((x), (y), (unsigned)z)) #define flockfile(x) ((void)pthread_mutex_lock(&global_log_file_lock)) #define funlockfile(x) ((void)pthread_mutex_unlock(&global_log_file_lock)) #define sleep(x) (Sleep((x)*1000)) #define rmdir(x) (_rmdir(x)) #if defined(_WIN64) || !defined(__MINGW32__) /* Only MinGW 32 bit is missing this function */ #define timegm(x) (_mkgmtime(x)) #else time_t timegm(struct tm *tm); #define NEED_TIMEGM #endif #if !defined(fileno) #define fileno(x) (_fileno(x)) #endif /* !fileno MINGW #defines fileno */ typedef struct { CRITICAL_SECTION sec; /* Immovable */ } pthread_mutex_t; typedef DWORD pthread_key_t; typedef HANDLE pthread_t; typedef struct { pthread_mutex_t threadIdSec; struct mg_workerTLS *waiting_thread; /* The chain of threads */ } pthread_cond_t; #if !defined(__clockid_t_defined) typedef DWORD clockid_t; #endif #if !defined(CLOCK_MONOTONIC) #define CLOCK_MONOTONIC (1) #endif #if !defined(CLOCK_REALTIME) #define CLOCK_REALTIME (2) #endif #if !defined(CLOCK_THREAD) #define CLOCK_THREAD (3) #endif #if !defined(CLOCK_PROCESS) #define CLOCK_PROCESS (4) #endif #if defined(_MSC_VER) && (_MSC_VER >= 1900) #define _TIMESPEC_DEFINED #endif #if !defined(_TIMESPEC_DEFINED) struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; #endif #if !defined(WIN_PTHREADS_TIME_H) #define MUST_IMPLEMENT_CLOCK_GETTIME #endif #if defined(MUST_IMPLEMENT_CLOCK_GETTIME) #define clock_gettime mg_clock_gettime static int clock_gettime(clockid_t clk_id, struct timespec *tp) { FILETIME ft; ULARGE_INTEGER li, li2; BOOL ok = FALSE; double d; static double perfcnt_per_sec = 0.0; static BOOL initialized = FALSE; if (!initialized) { QueryPerformanceFrequency((LARGE_INTEGER *)&li); perfcnt_per_sec = 1.0 / li.QuadPart; initialized = TRUE; } if (tp) { memset(tp, 0, sizeof(*tp)); if (clk_id == CLOCK_REALTIME) { /* BEGIN: CLOCK_REALTIME = wall clock (date and time) */ GetSystemTimeAsFileTime(&ft); li.LowPart = ft.dwLowDateTime; li.HighPart = ft.dwHighDateTime; li.QuadPart -= 116444736000000000; /* 1.1.1970 in filedate */ tp->tv_sec = (time_t)(li.QuadPart / 10000000); tp->tv_nsec = (long)(li.QuadPart % 10000000) * 100; ok = TRUE; /* END: CLOCK_REALTIME */ } else if (clk_id == CLOCK_MONOTONIC) { /* BEGIN: CLOCK_MONOTONIC = stopwatch (time differences) */ QueryPerformanceCounter((LARGE_INTEGER *)&li); d = li.QuadPart * perfcnt_per_sec; tp->tv_sec = (time_t)d; d -= (double)tp->tv_sec; tp->tv_nsec = (long)(d * 1.0E9); ok = TRUE; /* END: CLOCK_MONOTONIC */ } else if (clk_id == CLOCK_THREAD) { /* BEGIN: CLOCK_THREAD = CPU usage of thread */ FILETIME t_create, t_exit, t_kernel, t_user; if (GetThreadTimes(GetCurrentThread(), &t_create, &t_exit, &t_kernel, &t_user)) { li.LowPart = t_user.dwLowDateTime; li.HighPart = t_user.dwHighDateTime; li2.LowPart = t_kernel.dwLowDateTime; li2.HighPart = t_kernel.dwHighDateTime; li.QuadPart += li2.QuadPart; tp->tv_sec = (time_t)(li.QuadPart / 10000000); tp->tv_nsec = (long)(li.QuadPart % 10000000) * 100; ok = TRUE; } /* END: CLOCK_THREAD */ } else if (clk_id == CLOCK_PROCESS) { /* BEGIN: CLOCK_PROCESS = CPU usage of process */ FILETIME t_create, t_exit, t_kernel, t_user; if (GetProcessTimes(GetCurrentProcess(), &t_create, &t_exit, &t_kernel, &t_user)) { li.LowPart = t_user.dwLowDateTime; li.HighPart = t_user.dwHighDateTime; li2.LowPart = t_kernel.dwLowDateTime; li2.HighPart = t_kernel.dwHighDateTime; li.QuadPart += li2.QuadPart; tp->tv_sec = (time_t)(li.QuadPart / 10000000); tp->tv_nsec = (long)(li.QuadPart % 10000000) * 100; ok = TRUE; } /* END: CLOCK_PROCESS */ } else { /* BEGIN: unknown clock */ /* ok = FALSE; already set by init */ /* END: unknown clock */ } } return ok ? 0 : -1; } #endif #define pid_t HANDLE /* MINGW typedefs pid_t to int. Using #define here. */ static int pthread_mutex_lock(pthread_mutex_t *); static int pthread_mutex_unlock(pthread_mutex_t *); static void path_to_unicode(const struct mg_connection *conn, const char *path, wchar_t *wbuf, size_t wbuf_len); /* All file operations need to be rewritten to solve #246. */ struct mg_file; static const char * mg_fgets(char *buf, size_t size, struct mg_file *filep, char **p); /* POSIX dirent interface */ struct dirent { char d_name[PATH_MAX]; }; typedef struct DIR { HANDLE handle; WIN32_FIND_DATAW info; struct dirent result; } DIR; #if defined(HAVE_POLL) #define mg_pollfd pollfd #else struct mg_pollfd { SOCKET fd; short events; short revents; }; #endif /* Mark required libraries */ #if defined(_MSC_VER) # pragma comment(lib, "Ws2_32.lib") #endif #else /* defined(_WIN32) - WINDOWS vs UNIX include block */ #include typedef const void *SOCK_OPT_TYPE; #if defined(ANDROID) typedef unsigned short int in_port_t; #endif #if !defined(__ZEPHYR__) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif #define vsnprintf_impl vsnprintf #if !defined(NO_SSL_DL) && !defined(NO_SSL) #include #endif #if defined(__MACH__) && defined(__APPLE__) #define SSL_LIB "libssl.dylib" #define CRYPTO_LIB "libcrypto.dylib" #else #if !defined(SSL_LIB) #define SSL_LIB "libssl.so" #endif #if !defined(CRYPTO_LIB) #define CRYPTO_LIB "libcrypto.so" #endif #endif #if !defined(O_BINARY) #define O_BINARY (0) #endif /* O_BINARY */ #define closesocket(a) (close(a)) #define mg_mkdir(conn, path, mode) (mkdir(path, mode)) #define mg_remove(conn, x) (remove(x)) #define mg_sleep(x) (usleep((x)*1000)) #define mg_opendir(conn, x) (opendir(x)) #define mg_closedir(x) (closedir(x)) #define mg_readdir(x) (readdir(x)) #define ERRNO (errno) #define INVALID_SOCKET (-1) #define INT64_FMT PRId64 #define UINT64_FMT PRIu64 #define INT64_SFMT SCNd64 #define UINT64_SFMT SCNu64 typedef int SOCKET; #define WINCDECL #if defined(__hpux) /* HPUX 11 does not have monotonic, fall back to realtime */ #if !defined(CLOCK_MONOTONIC) #define CLOCK_MONOTONIC CLOCK_REALTIME #endif /* HPUX defines socklen_t incorrectly as size_t which is 64bit on * Itanium. Without defining _XOPEN_SOURCE or _XOPEN_SOURCE_EXTENDED * the prototypes use int* rather than socklen_t* which matches the * actual library expectation. When called with the wrong size arg * accept() returns a zero client inet addr and check_acl() always * fails. Since socklen_t is widely used below, just force replace * their typedef with int. - DTL */ #define socklen_t int #endif /* hpux */ #define mg_pollfd pollfd #endif /* defined(_WIN32) - WINDOWS vs UNIX include block */ /* In case our C library is missing "timegm", provide an implementation */ #if defined(NEED_TIMEGM) static inline int is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; } static inline int count_leap(int y) { return (y - 1969) / 4 - (y - 1901) / 100 + (y - 1601) / 400; } time_t timegm(struct tm *tm) { static const unsigned short ydays[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; int year = tm->tm_year + 1900; int mon = tm->tm_mon; int mday = tm->tm_mday - 1; int hour = tm->tm_hour; int min = tm->tm_min; int sec = tm->tm_sec; if (year < 1970 || mon < 0 || mon > 11 || mday < 0 || (mday >= ydays[mon + 1] - ydays[mon] + (mon == 1 && is_leap(year) ? 1 : 0)) || hour < 0 || hour > 23 || min < 0 || min > 59 || sec < 0 || sec > 60) return -1; time_t res = year - 1970; res *= 365; res += mday; res += ydays[mon] + (mon > 1 && is_leap(year) ? 1 : 0); res += count_leap(year); res *= 24; res += hour; res *= 60; res += min; res *= 60; res += sec; return res; } #endif /* NEED_TIMEGM */ /* va_copy should always be a macro, C99 and C++11 - DTL */ #if !defined(va_copy) #define va_copy(x, y) ((x) = (y)) #endif #if defined(_WIN32) /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif static pthread_mutex_t global_log_file_lock; FUNCTION_MAY_BE_UNUSED static DWORD pthread_self(void) { return GetCurrentThreadId(); } FUNCTION_MAY_BE_UNUSED static int pthread_key_create( pthread_key_t *key, void (*_ignored)(void *) /* destructor not supported for Windows */ ) { (void)_ignored; if ((key != 0)) { *key = TlsAlloc(); return (*key != TLS_OUT_OF_INDEXES) ? 0 : -1; } return -2; } FUNCTION_MAY_BE_UNUSED static int pthread_key_delete(pthread_key_t key) { return TlsFree(key) ? 0 : 1; } FUNCTION_MAY_BE_UNUSED static int pthread_setspecific(pthread_key_t key, void *value) { return TlsSetValue(key, value) ? 0 : 1; } FUNCTION_MAY_BE_UNUSED static void * pthread_getspecific(pthread_key_t key) { return TlsGetValue(key); } #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif static struct pthread_mutex_undefined_struct *pthread_mutex_attr = NULL; #else static pthread_mutexattr_t pthread_mutex_attr; #endif /* _WIN32 */ #if defined(_WIN32_WCE) /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif FUNCTION_MAY_BE_UNUSED static time_t time(time_t *ptime) { time_t t; SYSTEMTIME st; FILETIME ft; GetSystemTime(&st); SystemTimeToFileTime(&st, &ft); t = SYS2UNIX_TIME(ft.dwLowDateTime, ft.dwHighDateTime); if (ptime != NULL) { *ptime = t; } return t; } FUNCTION_MAY_BE_UNUSED static struct tm * localtime_s(const time_t *ptime, struct tm *ptm) { int64_t t = ((int64_t)*ptime) * RATE_DIFF + EPOCH_DIFF; FILETIME ft, lft; SYSTEMTIME st; TIME_ZONE_INFORMATION tzinfo; if (ptm == NULL) { return NULL; } *(int64_t *)&ft = t; FileTimeToLocalFileTime(&ft, &lft); FileTimeToSystemTime(&lft, &st); ptm->tm_year = st.wYear - 1900; ptm->tm_mon = st.wMonth - 1; ptm->tm_wday = st.wDayOfWeek; ptm->tm_mday = st.wDay; ptm->tm_hour = st.wHour; ptm->tm_min = st.wMinute; ptm->tm_sec = st.wSecond; ptm->tm_yday = 0; /* hope nobody uses this */ ptm->tm_isdst = (GetTimeZoneInformation(&tzinfo) == TIME_ZONE_ID_DAYLIGHT) ? 1 : 0; return ptm; } FUNCTION_MAY_BE_UNUSED static struct tm * gmtime_s(const time_t *ptime, struct tm *ptm) { /* FIXME(lsm): fix this. */ return localtime_s(ptime, ptm); } static int mg_atomic_inc(volatile int *addr); static struct tm tm_array[MAX_WORKER_THREADS]; static int tm_index = 0; FUNCTION_MAY_BE_UNUSED static struct tm * localtime(const time_t *ptime) { int i = mg_atomic_inc(&tm_index) % (sizeof(tm_array) / sizeof(tm_array[0])); return localtime_s(ptime, tm_array + i); } FUNCTION_MAY_BE_UNUSED static struct tm * gmtime(const time_t *ptime) { int i = mg_atomic_inc(&tm_index) % ARRAY_SIZE(tm_array); return gmtime_s(ptime, tm_array + i); } FUNCTION_MAY_BE_UNUSED static size_t strftime(char *dst, size_t dst_size, const char *fmt, const struct tm *tm) { /* TODO: (void)mg_snprintf(NULL, dst, dst_size, "implement strftime() * for WinCE"); */ return 0; } #define _beginthreadex(psec, stack, func, prm, flags, ptid) \ (uintptr_t) CreateThread(psec, stack, func, prm, flags, ptid) #define remove(f) mg_remove(NULL, f) FUNCTION_MAY_BE_UNUSED static int rename(const char *a, const char *b) { wchar_t wa[W_PATH_MAX]; wchar_t wb[W_PATH_MAX]; path_to_unicode(NULL, a, wa, ARRAY_SIZE(wa)); path_to_unicode(NULL, b, wb, ARRAY_SIZE(wb)); return MoveFileW(wa, wb) ? 0 : -1; } struct stat { int64_t st_size; time_t st_mtime; }; FUNCTION_MAY_BE_UNUSED static int stat(const char *name, struct stat *st) { wchar_t wbuf[W_PATH_MAX]; WIN32_FILE_ATTRIBUTE_DATA attr; time_t creation_time, write_time; path_to_unicode(NULL, name, wbuf, ARRAY_SIZE(wbuf)); memset(&attr, 0, sizeof(attr)); GetFileAttributesExW(wbuf, GetFileExInfoStandard, &attr); st->st_size = (((int64_t)attr.nFileSizeHigh) << 32) + (int64_t)attr.nFileSizeLow; write_time = SYS2UNIX_TIME(attr.ftLastWriteTime.dwLowDateTime, attr.ftLastWriteTime.dwHighDateTime); creation_time = SYS2UNIX_TIME(attr.ftCreationTime.dwLowDateTime, attr.ftCreationTime.dwHighDateTime); if (creation_time > write_time) { st->st_mtime = creation_time; } else { st->st_mtime = write_time; } return 0; } #define access(x, a) 1 /* not required anyway */ /* WinCE-TODO: define stat, remove, rename, _rmdir, _lseeki64 */ /* Values from errno.h in Windows SDK (Visual Studio). */ #define EEXIST 17 #define EACCES 13 #define ENOENT 2 #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif #endif /* defined(_WIN32_WCE) */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-function" #endif static pthread_mutex_t global_lock_mutex; FUNCTION_MAY_BE_UNUSED static void mg_global_lock(void) { (void)pthread_mutex_lock(&global_lock_mutex); } FUNCTION_MAY_BE_UNUSED static void mg_global_unlock(void) { (void)pthread_mutex_unlock(&global_lock_mutex); } FUNCTION_MAY_BE_UNUSED static int mg_atomic_inc(volatile int *addr) { int ret; #if defined(_WIN32) && !defined(NO_ATOMICS) /* Depending on the SDK, this function uses either * (volatile unsigned int *) or (volatile LONG *), * so whatever you use, the other SDK is likely to raise a warning. */ ret = InterlockedIncrement((volatile long *)addr); #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_add_and_fetch(addr, 1); #else mg_global_lock(); ret = (++(*addr)); mg_global_unlock(); #endif return ret; } FUNCTION_MAY_BE_UNUSED static int mg_atomic_dec(volatile int *addr) { int ret; #if defined(_WIN32) && !defined(NO_ATOMICS) /* Depending on the SDK, this function uses either * (volatile unsigned int *) or (volatile LONG *), * so whatever you use, the other SDK is likely to raise a warning. */ ret = InterlockedDecrement((volatile long *)addr); #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_sub_and_fetch(addr, 1); #else mg_global_lock(); ret = (--(*addr)); mg_global_unlock(); #endif return ret; } #if defined(USE_SERVER_STATS) static int64_t mg_atomic_add(volatile int64_t *addr, int64_t value) { int64_t ret; #if defined(_WIN64) && !defined(NO_ATOMICS) ret = InterlockedAdd64(addr, value); #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_add_and_fetch(addr, value); #else mg_global_lock(); *addr += value; ret = (*addr); mg_global_unlock(); #endif return ret; } #endif #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic pop #endif #if defined(USE_SERVER_STATS) struct mg_memory_stat { volatile int64_t totalMemUsed; volatile int64_t maxMemUsed; volatile int blockCount; }; static struct mg_memory_stat *get_memory_stat(struct mg_context *ctx); static void * mg_malloc_ex(size_t size, struct mg_context *ctx, const char *file, unsigned line) { void *data = malloc(size + 2 * sizeof(uintptr_t)); void *memory = 0; struct mg_memory_stat *mstat = get_memory_stat(ctx); #if defined(MEMORY_DEBUGGING) char mallocStr[256]; #else (void)file; (void)line; #endif if (data) { int64_t mmem = mg_atomic_add(&mstat->totalMemUsed, (int64_t)size); if (mmem > mstat->maxMemUsed) { /* could use atomic compare exchange, but this * seems overkill for statistics data */ mstat->maxMemUsed = mmem; } mg_atomic_inc(&mstat->blockCount); ((uintptr_t *)data)[0] = size; ((uintptr_t *)data)[1] = (uintptr_t)mstat; memory = (void *)(((char *)data) + 2 * sizeof(uintptr_t)); } #if defined(MEMORY_DEBUGGING) snprintf(mallocStr, sizeof(mallocStr), "MEM: %p %5lu alloc %7lu %4lu --- %s:%u\n", memory, (unsigned long)size, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); #if defined(_WIN32) OutputDebugStringA(mallocStr); #else DEBUG_TRACE("%s", mallocStr); #endif #endif return memory; } static void * mg_calloc_ex(size_t count, size_t size, struct mg_context *ctx, const char *file, unsigned line) { void *data = mg_malloc_ex(size * count, ctx, file, line); if (data) { memset(data, 0, size * count); } return data; } static void mg_free_ex(void *memory, const char *file, unsigned line) { void *data = (void *)(((char *)memory) - 2 * sizeof(uintptr_t)); #if defined(MEMORY_DEBUGGING) char mallocStr[256]; #else (void)file; (void)line; #endif if (memory) { uintptr_t size = ((uintptr_t *)data)[0]; struct mg_memory_stat *mstat = (struct mg_memory_stat *)(((uintptr_t *)data)[1]); mg_atomic_add(&mstat->totalMemUsed, -(int64_t)size); mg_atomic_dec(&mstat->blockCount); #if defined(MEMORY_DEBUGGING) snprintf(mallocStr, sizeof(mallocStr), "MEM: %p %5lu free %7lu %4lu --- %s:%u\n", memory, (unsigned long)size, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); #if defined(_WIN32) OutputDebugStringA(mallocStr); #else DEBUG_TRACE("%s", mallocStr); #endif #endif free(data); } } static void * mg_realloc_ex(void *memory, size_t newsize, struct mg_context *ctx, const char *file, unsigned line) { void *data; void *_realloc; uintptr_t oldsize; #if defined(MEMORY_DEBUGGING) char mallocStr[256]; #else (void)file; (void)line; #endif if (newsize) { if (memory) { /* Reallocate existing block */ struct mg_memory_stat *mstat; data = (void *)(((char *)memory) - 2 * sizeof(uintptr_t)); oldsize = ((uintptr_t *)data)[0]; mstat = (struct mg_memory_stat *)((uintptr_t *)data)[1]; _realloc = realloc(data, newsize + 2 * sizeof(uintptr_t)); if (_realloc) { data = _realloc; mg_atomic_add(&mstat->totalMemUsed, -(int64_t)oldsize); #if defined(MEMORY_DEBUGGING) snprintf(mallocStr, sizeof(mallocStr), "MEM: %p %5lu r-free %7lu %4lu --- %s:%u\n", memory, (unsigned long)oldsize, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); #if defined(_WIN32) OutputDebugStringA(mallocStr); #else DEBUG_TRACE("%s", mallocStr); #endif #endif mg_atomic_add(&mstat->totalMemUsed, (int64_t)newsize); #if defined(MEMORY_DEBUGGING) snprintf(mallocStr, sizeof(mallocStr), "MEM: %p %5lu r-alloc %7lu %4lu --- %s:%u\n", memory, (unsigned long)newsize, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); #if defined(_WIN32) OutputDebugStringA(mallocStr); #else DEBUG_TRACE("%s", mallocStr); #endif #endif *(uintptr_t *)data = newsize; data = (void *)(((char *)data) + 2 * sizeof(uintptr_t)); } else { #if defined(MEMORY_DEBUGGING) #if defined(_WIN32) OutputDebugStringA("MEM: realloc failed\n"); #else DEBUG_TRACE("%s", "MEM: realloc failed\n"); #endif #endif return _realloc; } } else { /* Allocate new block */ data = mg_malloc_ex(newsize, ctx, file, line); } } else { /* Free existing block */ data = 0; mg_free_ex(memory, file, line); } return data; } #define mg_malloc(a) mg_malloc_ex(a, NULL, __FILE__, __LINE__) #define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __FILE__, __LINE__) #define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __FILE__, __LINE__) #define mg_free(a) mg_free_ex(a, __FILE__, __LINE__) #define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __FILE__, __LINE__) #define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __FILE__, __LINE__) #define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __FILE__, __LINE__) #else /* USE_SERVER_STATS */ static __inline void * mg_malloc(size_t a) { return malloc(a); } static __inline void * mg_calloc(size_t a, size_t b) { return calloc(a, b); } static __inline void * mg_realloc(void *a, size_t b) { return realloc(a, b); } static __inline void mg_free(void *a) { free(a); } #define mg_malloc_ctx(a, c) mg_malloc(a) #define mg_calloc_ctx(a, b, c) mg_calloc(a, b) #define mg_realloc_ctx(a, b, c) mg_realloc(a, b) #define mg_free_ctx(a, c) mg_free(a) #endif /* USE_SERVER_STATS */ static void mg_vsnprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, va_list ap); static void mg_snprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(5, 6); /* This following lines are just meant as a reminder to use the mg-functions * for memory management */ #if defined(malloc) #undef malloc #endif #if defined(calloc) #undef calloc #endif #if defined(realloc) #undef realloc #endif #if defined(free) #undef free #endif #if defined(snprintf) #undef snprintf #endif #if defined(vsnprintf) #undef vsnprintf #endif #define malloc DO_NOT_USE_THIS_FUNCTION__USE_mg_malloc #define calloc DO_NOT_USE_THIS_FUNCTION__USE_mg_calloc #define realloc DO_NOT_USE_THIS_FUNCTION__USE_mg_realloc #define free DO_NOT_USE_THIS_FUNCTION__USE_mg_free /* #define snprintf DO_NOT_USE_THIS_FUNCTION__USE_mg_snprintf */ #if defined(_WIN32) /* vsnprintf must not be used in any system, * but this define only works well for Windows. */ #define vsnprintf DO_NOT_USE_THIS_FUNCTION__USE_mg_vsnprintf #endif /* mg_init_library counter */ static int mg_init_library_called = 0; #if !defined(NO_SSL) static int mg_ssl_initialized = 0; #endif static pthread_key_t sTlsKey; /* Thread local storage index */ static int thread_idx_max = 0; #if defined(MG_LEGACY_INTERFACE) #define MG_ALLOW_USING_GET_REQUEST_INFO_FOR_RESPONSE #endif struct mg_workerTLS { int is_master; unsigned long thread_idx; void *user_ptr; #if defined(_WIN32) HANDLE pthread_cond_helper_mutex; struct mg_workerTLS *next_waiting_thread; #endif #if defined(MG_ALLOW_USING_GET_REQUEST_INFO_FOR_RESPONSE) char txtbuf[4]; #endif }; #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-function" #endif /* Get a unique thread ID as unsigned long, independent from the data type * of thread IDs defined by the operating system API. * If two calls to mg_current_thread_id return the same value, they calls * are done from the same thread. If they return different values, they are * done from different threads. (Provided this function is used in the same * process context and threads are not repeatedly created and deleted, but * CivetWeb does not do that). * This function must match the signature required for SSL id callbacks: * CRYPTO_set_id_callback */ FUNCTION_MAY_BE_UNUSED static unsigned long mg_current_thread_id(void) { #if defined(_WIN32) return GetCurrentThreadId(); #else #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunreachable-code" /* For every compiler, either "sizeof(pthread_t) > sizeof(unsigned long)" * or not, so one of the two conditions will be unreachable by construction. * Unfortunately the C standard does not define a way to check this at * compile time, since the #if preprocessor conditions can not use the sizeof * operator as an argument. */ #endif if (sizeof(pthread_t) > sizeof(unsigned long)) { /* This is the problematic case for CRYPTO_set_id_callback: * The OS pthread_t can not be cast to unsigned long. */ struct mg_workerTLS *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); if (tls == NULL) { /* SSL called from an unknown thread: Create some thread index. */ tls = (struct mg_workerTLS *)mg_malloc(sizeof(struct mg_workerTLS)); tls->is_master = -2; /* -2 means "3rd party thread" */ tls->thread_idx = (unsigned)mg_atomic_inc(&thread_idx_max); pthread_setspecific(sTlsKey, tls); } return tls->thread_idx; } else { /* pthread_t may be any data type, so a simple cast to unsigned long * can rise a warning/error, depending on the platform. * Here memcpy is used as an anything-to-anything cast. */ unsigned long ret = 0; pthread_t t = pthread_self(); memcpy(&ret, &t, sizeof(pthread_t)); return ret; } #if defined(__clang__) # pragma clang diagnostic pop #endif #endif } FUNCTION_MAY_BE_UNUSED static uint64_t mg_get_current_time_ns(void) { struct timespec tsnow; clock_gettime(CLOCK_REALTIME, &tsnow); return (((uint64_t)tsnow.tv_sec) * 1000000000) + (uint64_t)tsnow.tv_nsec; } #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic pop #endif #if defined(NEED_DEBUG_TRACE_FUNC) static void DEBUG_TRACE_FUNC(const char *func, unsigned line, const char *fmt, ...) { va_list args; uint64_t nsnow; static uint64_t nslast; struct timespec tsnow; /* Get some operating system independent thread id */ unsigned long thread_id = mg_current_thread_id(); clock_gettime(CLOCK_REALTIME, &tsnow); nsnow = ((uint64_t)tsnow.tv_sec) * ((uint64_t)1000000000) + ((uint64_t)tsnow.tv_nsec); if (!nslast) { nslast = nsnow; } flockfile(DEBUG_TRACE_STREAM); fprintf( DEBUG_TRACE_STREAM,"*** %lu.%09lu %12" INT64_FMT " %lu %s:%u: ", (unsigned long)tsnow.tv_sec, (unsigned long)tsnow.tv_nsec, nsnow - nslast, thread_id, func, line); va_start(args, fmt); vfprintf(DEBUG_TRACE_STREAM, fmt, args); va_end(args); putc('\n', DEBUG_TRACE_STREAM); fflush(DEBUG_TRACE_STREAM); funlockfile(DEBUG_TRACE_STREAM); nslast = nsnow; } #endif /* NEED_DEBUG_TRACE_FUNC */ #define MD5_STATIC static #include "md5.h" /* Darwin prior to 7.0 and Win32 do not have socklen_t */ #if defined(NO_SOCKLEN_T) typedef int socklen_t; #endif /* NO_SOCKLEN_T */ #define IP_ADDR_STR_LEN (50) /* IPv6 hex string is 46 chars */ #if !defined(MSG_NOSIGNAL) #define MSG_NOSIGNAL (0) #endif #if defined(NO_SSL) typedef struct SSL SSL; /* dummy for SSL argument to push/pull */ typedef struct SSL_CTX SSL_CTX; #else #if defined(NO_SSL_DL) #include #include #include #include #include #include #include #include #include #include #include #if defined(WOLFSSL_VERSION) /* Additional defines for WolfSSL, see * https://github.com/civetweb/civetweb/issues/583 */ #include "wolfssl_extras.inl" #endif #if defined(OPENSSL_IS_BORINGSSL) /* From boringssl/src/include/openssl/mem.h: * * OpenSSL has, historically, had a complex set of malloc debugging options. * However, that was written in a time before Valgrind and ASAN. Since we now * have those tools, the OpenSSL allocation functions are simply macros around * the standard memory functions. * * #define OPENSSL_free free */ #define free free // disable for boringssl #define CONF_modules_unload(a) ((void)0) #define ENGINE_cleanup() ((void)0) #endif #if (OPENSSL_VERSION_NUMBER >= 0x10100000L) /* If OpenSSL headers are included, automatically select the API version */ #if !defined(OPENSSL_API_1_1) #define OPENSSL_API_1_1 #endif #define OPENSSL_REMOVE_THREAD_STATE() #else #define OPENSSL_REMOVE_THREAD_STATE() ERR_remove_thread_state(NULL) #endif #else /* SSL loaded dynamically from DLL. * I put the prototypes here to be independent from OpenSSL source * installation. */ typedef struct ssl_st SSL; typedef struct ssl_method_st SSL_METHOD; typedef struct ssl_ctx_st SSL_CTX; typedef struct x509_store_ctx_st X509_STORE_CTX; typedef struct x509_name X509_NAME; typedef struct asn1_integer ASN1_INTEGER; typedef struct bignum BIGNUM; typedef struct ossl_init_settings_st OPENSSL_INIT_SETTINGS; typedef struct evp_md EVP_MD; typedef struct x509 X509; #define SSL_CTRL_OPTIONS (32) #define SSL_CTRL_CLEAR_OPTIONS (77) #define SSL_CTRL_SET_ECDH_AUTO (94) #define OPENSSL_INIT_NO_LOAD_SSL_STRINGS 0x00100000L #define OPENSSL_INIT_LOAD_SSL_STRINGS 0x00200000L #define OPENSSL_INIT_LOAD_CRYPTO_STRINGS 0x00000002L #define SSL_VERIFY_NONE (0) #define SSL_VERIFY_PEER (1) #define SSL_VERIFY_FAIL_IF_NO_PEER_CERT (2) #define SSL_VERIFY_CLIENT_ONCE (4) #define SSL_OP_ALL (0x80000BFFul) #define SSL_OP_NO_SSLv2 (0x01000000ul) #define SSL_OP_NO_SSLv3 (0x02000000ul) #define SSL_OP_NO_TLSv1 (0x04000000ul) #define SSL_OP_NO_TLSv1_2 (0x08000000ul) #define SSL_OP_NO_TLSv1_1 (0x10000000ul) #define SSL_OP_NO_TLSv1_3 (0x20000000ul) #define SSL_OP_SINGLE_DH_USE (0x00100000ul) #define SSL_OP_CIPHER_SERVER_PREFERENCE (0x00400000ul) #define SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION (0x00010000ul) #define SSL_OP_NO_COMPRESSION (0x00020000ul) #define SSL_OP_NO_RENEGOTIATION (0x40000000ul) #define SSL_CB_HANDSHAKE_START (0x10) #define SSL_CB_HANDSHAKE_DONE (0x20) #define SSL_ERROR_NONE (0) #define SSL_ERROR_SSL (1) #define SSL_ERROR_WANT_READ (2) #define SSL_ERROR_WANT_WRITE (3) #define SSL_ERROR_WANT_X509_LOOKUP (4) #define SSL_ERROR_SYSCALL (5) /* see errno */ #define SSL_ERROR_ZERO_RETURN (6) #define SSL_ERROR_WANT_CONNECT (7) #define SSL_ERROR_WANT_ACCEPT (8) #define TLSEXT_TYPE_server_name (0) #define TLSEXT_NAMETYPE_host_name (0) #define SSL_TLSEXT_ERR_OK (0) #define SSL_TLSEXT_ERR_ALERT_WARNING (1) #define SSL_TLSEXT_ERR_ALERT_FATAL (2) #define SSL_TLSEXT_ERR_NOACK (3) struct ssl_func { const char *name; /* SSL function name */ void (*ptr)(void); /* Function pointer */ }; #if defined(OPENSSL_API_1_1) #define SSL_free (*(void (*)(SSL *))ssl_sw[0].ptr) #define SSL_accept (*(int (*)(SSL *))ssl_sw[1].ptr) #define SSL_connect (*(int (*)(SSL *))ssl_sw[2].ptr) #define SSL_read (*(int (*)(SSL *, void *, int))ssl_sw[3].ptr) #define SSL_write (*(int (*)(SSL *, const void *, int))ssl_sw[4].ptr) #define SSL_get_error (*(int (*)(SSL *, int))ssl_sw[5].ptr) #define SSL_set_fd (*(int (*)(SSL *, SOCKET))ssl_sw[6].ptr) #define SSL_new (*(SSL * (*)(SSL_CTX *)) ssl_sw[7].ptr) #define SSL_CTX_new (*(SSL_CTX * (*)(SSL_METHOD *)) ssl_sw[8].ptr) #define TLS_server_method (*(SSL_METHOD * (*)(void)) ssl_sw[9].ptr) #define OPENSSL_init_ssl \ (*(int (*)(uint64_t opts, \ const OPENSSL_INIT_SETTINGS *settings))ssl_sw[10] \ .ptr) #define SSL_CTX_use_PrivateKey_file \ (*(int (*)(SSL_CTX *, const char *, int))ssl_sw[11].ptr) #define SSL_CTX_use_certificate_file \ (*(int (*)(SSL_CTX *, const char *, int))ssl_sw[12].ptr) #define SSL_CTX_set_default_passwd_cb \ (*(void (*)(SSL_CTX *, mg_callback_t))ssl_sw[13].ptr) #define SSL_CTX_free (*(void (*)(SSL_CTX *))ssl_sw[14].ptr) #define SSL_CTX_use_certificate_chain_file \ (*(int (*)(SSL_CTX *, const char *))ssl_sw[15].ptr) #define TLS_client_method (*(SSL_METHOD * (*)(void)) ssl_sw[16].ptr) #define SSL_pending (*(int (*)(SSL *))ssl_sw[17].ptr) #define SSL_CTX_set_verify \ (*(void (*)(SSL_CTX *, \ int, \ int (*verify_callback)(int, X509_STORE_CTX *)))ssl_sw[18] \ .ptr) #define SSL_shutdown (*(int (*)(SSL *))ssl_sw[19].ptr) #define SSL_CTX_load_verify_locations \ (*(int (*)(SSL_CTX *, const char *, const char *))ssl_sw[20].ptr) #define SSL_CTX_set_default_verify_paths (*(int (*)(SSL_CTX *))ssl_sw[21].ptr) #define SSL_CTX_set_verify_depth (*(void (*)(SSL_CTX *, int))ssl_sw[22].ptr) #define SSL_get_peer_certificate (*(X509 * (*)(SSL *)) ssl_sw[23].ptr) #define SSL_get_version (*(const char *(*)(SSL *))ssl_sw[24].ptr) #define SSL_get_current_cipher (*(SSL_CIPHER * (*)(SSL *)) ssl_sw[25].ptr) #define SSL_CIPHER_get_name \ (*(const char *(*)(const SSL_CIPHER *))ssl_sw[26].ptr) #define SSL_CTX_check_private_key (*(int (*)(SSL_CTX *))ssl_sw[27].ptr) #define SSL_CTX_set_session_id_context \ (*(int (*)(SSL_CTX *, const unsigned char *, unsigned int))ssl_sw[28].ptr) #define SSL_CTX_ctrl (*(long (*)(SSL_CTX *, int, long, void *))ssl_sw[29].ptr) #define SSL_CTX_set_cipher_list \ (*(int (*)(SSL_CTX *, const char *))ssl_sw[30].ptr) #define SSL_CTX_set_options \ (*(unsigned long (*)(SSL_CTX *, unsigned long))ssl_sw[31].ptr) #define SSL_CTX_set_info_callback \ (*(void (*)(SSL_CTX * ctx, void (*callback)(const SSL *, int, int))) \ ssl_sw[32] \ .ptr) #define SSL_get_ex_data (*(char *(*)(const SSL *, int))ssl_sw[33].ptr) #define SSL_set_ex_data (*(void (*)(SSL *, int, char *))ssl_sw[34].ptr) #define SSL_CTX_callback_ctrl \ (*(long (*)(SSL_CTX *, int, void (*)(void)))ssl_sw[35].ptr) #define SSL_get_servername \ (*(const char *(*)(const SSL *, int type))ssl_sw[36].ptr) #define SSL_set_SSL_CTX (*(SSL_CTX * (*)(SSL *, SSL_CTX *)) ssl_sw[37].ptr) #define SSL_ctrl (*(long (*)(SSL *, int, long, void *))ssl_sw[38].ptr) #define SSL_CTX_clear_options(ctx, op) \ SSL_CTX_ctrl((ctx), SSL_CTRL_CLEAR_OPTIONS, (op), NULL) #define SSL_CTX_set_ecdh_auto(ctx, onoff) \ SSL_CTX_ctrl(ctx, SSL_CTRL_SET_ECDH_AUTO, onoff, NULL) #define SSL_CTRL_SET_TLSEXT_SERVERNAME_CB 53 #define SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG 54 #define SSL_CTRL_SET_TLSEXT_HOSTNAME 55 #define SSL_CTX_set_tlsext_servername_callback(ctx, cb) \ SSL_CTX_callback_ctrl(ctx, \ SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, \ (void (*)(void))cb) #define SSL_CTX_set_tlsext_servername_arg(ctx, arg) \ SSL_CTX_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG, 0, (void *)arg) #define SSL_set_tlsext_host_name(ctx, arg) \ SSL_ctrl(ctx, SSL_CTRL_SET_TLSEXT_HOSTNAME, 0, (void *)arg) #define X509_get_notBefore(x) ((x)->cert_info->validity->notBefore) #define X509_get_notAfter(x) ((x)->cert_info->validity->notAfter) #define SSL_set_app_data(s, arg) (SSL_set_ex_data(s, 0, (char *)arg)) #define SSL_get_app_data(s) (SSL_get_ex_data(s, 0)) #define ERR_get_error (*(unsigned long (*)(void))crypto_sw[0].ptr) #define ERR_error_string (*(char *(*)(unsigned long, char *))crypto_sw[1].ptr) #define CONF_modules_unload (*(void (*)(int))crypto_sw[2].ptr) #define X509_free (*(void (*)(X509 *))crypto_sw[3].ptr) #define X509_get_subject_name (*(X509_NAME * (*)(X509 *)) crypto_sw[4].ptr) #define X509_get_issuer_name (*(X509_NAME * (*)(X509 *)) crypto_sw[5].ptr) #define X509_NAME_oneline \ (*(char *(*)(X509_NAME *, char *, int))crypto_sw[6].ptr) #define X509_get_serialNumber (*(ASN1_INTEGER * (*)(X509 *)) crypto_sw[7].ptr) #define EVP_get_digestbyname \ (*(const EVP_MD *(*)(const char *))crypto_sw[8].ptr) #define EVP_Digest \ (*(int (*)( \ const void *, size_t, void *, unsigned int *, const EVP_MD *, void *)) \ crypto_sw[9] \ .ptr) #define i2d_X509 (*(int (*)(X509 *, unsigned char **))crypto_sw[10].ptr) #define BN_bn2hex (*(char *(*)(const BIGNUM *a))crypto_sw[11].ptr) #define ASN1_INTEGER_to_BN \ (*(BIGNUM * (*)(const ASN1_INTEGER *ai, BIGNUM *bn)) crypto_sw[12].ptr) #define BN_free (*(void (*)(const BIGNUM *a))crypto_sw[13].ptr) #define CRYPTO_free (*(void (*)(void *addr))crypto_sw[14].ptr) #define ERR_clear_error (*(void (*)(void))crypto_sw[15].ptr) #define OPENSSL_free(a) CRYPTO_free(a) #define OPENSSL_REMOVE_THREAD_STATE() /* init_ssl_ctx() function updates this array. * It loads SSL library dynamically and changes NULLs to the actual addresses * of respective functions. The macros above (like SSL_connect()) are really * just calling these functions indirectly via the pointer. */ static struct ssl_func ssl_sw[] = {{"SSL_free", NULL}, {"SSL_accept", NULL}, {"SSL_connect", NULL}, {"SSL_read", NULL}, {"SSL_write", NULL}, {"SSL_get_error", NULL}, {"SSL_set_fd", NULL}, {"SSL_new", NULL}, {"SSL_CTX_new", NULL}, {"TLS_server_method", NULL}, {"OPENSSL_init_ssl", NULL}, {"SSL_CTX_use_PrivateKey_file", NULL}, {"SSL_CTX_use_certificate_file", NULL}, {"SSL_CTX_set_default_passwd_cb", NULL}, {"SSL_CTX_free", NULL}, {"SSL_CTX_use_certificate_chain_file", NULL}, {"TLS_client_method", NULL}, {"SSL_pending", NULL}, {"SSL_CTX_set_verify", NULL}, {"SSL_shutdown", NULL}, {"SSL_CTX_load_verify_locations", NULL}, {"SSL_CTX_set_default_verify_paths", NULL}, {"SSL_CTX_set_verify_depth", NULL}, {"SSL_get_peer_certificate", NULL}, {"SSL_get_version", NULL}, {"SSL_get_current_cipher", NULL}, {"SSL_CIPHER_get_name", NULL}, {"SSL_CTX_check_private_key", NULL}, {"SSL_CTX_set_session_id_context", NULL}, {"SSL_CTX_ctrl", NULL}, {"SSL_CTX_set_cipher_list", NULL}, {"SSL_CTX_set_options", NULL}, {"SSL_CTX_set_info_callback", NULL}, {"SSL_get_ex_data", NULL}, {"SSL_set_ex_data", NULL}, {"SSL_CTX_callback_ctrl", NULL}, {"SSL_get_servername", NULL}, {"SSL_set_SSL_CTX", NULL}, {"SSL_ctrl", NULL}, {NULL, NULL}}; /* Similar array as ssl_sw. These functions could be located in different * lib. */ static struct ssl_func crypto_sw[] = {{"ERR_get_error", NULL}, {"ERR_error_string", NULL}, {"CONF_modules_unload", NULL}, {"X509_free", NULL}, {"X509_get_subject_name", NULL}, {"X509_get_issuer_name", NULL}, {"X509_NAME_oneline", NULL}, {"X509_get_serialNumber", NULL}, {"EVP_get_digestbyname", NULL}, {"EVP_Digest", NULL}, {"i2d_X509", NULL}, {"BN_bn2hex", NULL}, {"ASN1_INTEGER_to_BN", NULL}, {"BN_free", NULL}, {"CRYPTO_free", NULL}, {"ERR_clear_error", NULL}, {NULL, NULL}}; #else #define SSL_free (*(void (*)(SSL *))ssl_sw[0].ptr) #define SSL_accept (*(int (*)(SSL *))ssl_sw[1].ptr) #define SSL_connect (*(int (*)(SSL *))ssl_sw[2].ptr) #define SSL_read (*(int (*)(SSL *, void *, int))ssl_sw[3].ptr) #define SSL_write (*(int (*)(SSL *, const void *, int))ssl_sw[4].ptr) #define SSL_get_error (*(int (*)(SSL *, int))ssl_sw[5].ptr) #define SSL_set_fd (*(int (*)(SSL *, SOCKET))ssl_sw[6].ptr) #define SSL_new (*(SSL * (*)(SSL_CTX *)) ssl_sw[7].ptr) #define SSL_CTX_new (*(SSL_CTX * (*)(SSL_METHOD *)) ssl_sw[8].ptr) #define SSLv23_server_method (*(SSL_METHOD * (*)(void)) ssl_sw[9].ptr) #define SSL_library_init (*(int (*)(void))ssl_sw[10].ptr) #define SSL_CTX_use_PrivateKey_file \ (*(int (*)(SSL_CTX *, const char *, int))ssl_sw[11].ptr) #define SSL_CTX_use_certificate_file \ (*(int (*)(SSL_CTX *, const char *, int))ssl_sw[12].ptr) #define SSL_CTX_set_default_passwd_cb \ (*(void (*)(SSL_CTX *, mg_callback_t))ssl_sw[13].ptr) #define SSL_CTX_free (*(void (*)(SSL_CTX *))ssl_sw[14].ptr) #define SSL_load_error_strings (*(void (*)(void))ssl_sw[15].ptr) #define SSL_CTX_use_certificate_chain_file \ (*(int (*)(SSL_CTX *, const char *))ssl_sw[16].ptr) #define SSLv23_client_method (*(SSL_METHOD * (*)(void)) ssl_sw[17].ptr) #define SSL_pending (*(int (*)(SSL *))ssl_sw[18].ptr) #define SSL_CTX_set_verify \ (*(void (*)(SSL_CTX *, \ int, \ int (*verify_callback)(int, X509_STORE_CTX *)))ssl_sw[19] \ .ptr) #define SSL_shutdown (*(int (*)(SSL *))ssl_sw[20].ptr) #define SSL_CTX_load_verify_locations \ (*(int (*)(SSL_CTX *, const char *, const char *))ssl_sw[21].ptr) #define SSL_CTX_set_default_verify_paths (*(int (*)(SSL_CTX *))ssl_sw[22].ptr) #define SSL_CTX_set_verify_depth (*(void (*)(SSL_CTX *, int))ssl_sw[23].ptr) #define SSL_get_peer_certificate (*(X509 * (*)(SSL *)) ssl_sw[24].ptr) #define SSL_get_version (*(const char *(*)(SSL *))ssl_sw[25].ptr) #define SSL_get_current_cipher (*(SSL_CIPHER * (*)(SSL *)) ssl_sw[26].ptr) #define SSL_CIPHER_get_name \ (*(const char *(*)(const SSL_CIPHER *))ssl_sw[27].ptr) #define SSL_CTX_check_private_key (*(int (*)(SSL_CTX *))ssl_sw[28].ptr) #define SSL_CTX_set_session_id_context \ (*(int (*)(SSL_CTX *, const unsigned char *, unsigned int))ssl_sw[29].ptr) #define SSL_CTX_ctrl (*(long (*)(SSL_CTX *, int, long, void *))ssl_sw[30].ptr) #define SSL_CTX_set_cipher_list \ (*(int (*)(SSL_CTX *, const char *))ssl_sw[31].ptr) #define SSL_CTX_set_info_callback \ (*(void (*)(SSL_CTX *, void (*callback)(const SSL *, int, int)))ssl_sw[32] \ .ptr) #define SSL_get_ex_data (*(char *(*)(const SSL *, int))ssl_sw[33].ptr) #define SSL_set_ex_data (*(void (*)(SSL *, int, char *))ssl_sw[34].ptr) #define SSL_CTX_callback_ctrl \ (*(long (*)(SSL_CTX *, int, void (*)(void)))ssl_sw[35].ptr) #define SSL_get_servername \ (*(const char *(*)(const SSL *, int type))ssl_sw[36].ptr) #define SSL_set_SSL_CTX (*(SSL_CTX * (*)(SSL *, SSL_CTX *)) ssl_sw[37].ptr) #define SSL_ctrl (*(long (*)(SSL *, int, long, void *))ssl_sw[38].ptr) #define SSL_CTX_set_options(ctx, op) \ SSL_CTX_ctrl((ctx), SSL_CTRL_OPTIONS, (op), NULL) #define SSL_CTX_clear_options(ctx, op) \ SSL_CTX_ctrl((ctx), SSL_CTRL_CLEAR_OPTIONS, (op), NULL) #define SSL_CTX_set_ecdh_auto(ctx, onoff) \ SSL_CTX_ctrl(ctx, SSL_CTRL_SET_ECDH_AUTO, onoff, NULL) #define SSL_CTRL_SET_TLSEXT_SERVERNAME_CB 53 #define SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG 54 #define SSL_CTRL_SET_TLSEXT_HOSTNAME 55 #define SSL_CTX_set_tlsext_servername_callback(ctx, cb) \ SSL_CTX_callback_ctrl(ctx, \ SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, \ (void (*)(void))cb) #define SSL_CTX_set_tlsext_servername_arg(ctx, arg) \ SSL_CTX_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG, 0, (void *)arg) #define SSL_set_tlsext_host_name(ctx, arg) \ SSL_ctrl(ctx, SSL_CTRL_SET_TLSEXT_HOSTNAME, 0, (void *)arg) #define X509_get_notBefore(x) ((x)->cert_info->validity->notBefore) #define X509_get_notAfter(x) ((x)->cert_info->validity->notAfter) #define SSL_set_app_data(s, arg) (SSL_set_ex_data(s, 0, (char *)arg)) #define SSL_get_app_data(s) (SSL_get_ex_data(s, 0)) #define CRYPTO_num_locks (*(int (*)(void))crypto_sw[0].ptr) #define CRYPTO_set_locking_callback \ (*(void (*)(void (*)(int, int, const char *, int)))crypto_sw[1].ptr) #define CRYPTO_set_id_callback \ (*(void (*)(unsigned long (*)(void)))crypto_sw[2].ptr) #define ERR_get_error (*(unsigned long (*)(void))crypto_sw[3].ptr) #define ERR_error_string (*(char *(*)(unsigned long, char *))crypto_sw[4].ptr) #define ERR_remove_state (*(void (*)(unsigned long))crypto_sw[5].ptr) #define ERR_free_strings (*(void (*)(void))crypto_sw[6].ptr) #define ENGINE_cleanup (*(void (*)(void))crypto_sw[7].ptr) #define CONF_modules_unload (*(void (*)(int))crypto_sw[8].ptr) #define CRYPTO_cleanup_all_ex_data (*(void (*)(void))crypto_sw[9].ptr) #define EVP_cleanup (*(void (*)(void))crypto_sw[10].ptr) #define X509_free (*(void (*)(X509 *))crypto_sw[11].ptr) #define X509_get_subject_name (*(X509_NAME * (*)(X509 *)) crypto_sw[12].ptr) #define X509_get_issuer_name (*(X509_NAME * (*)(X509 *)) crypto_sw[13].ptr) #define X509_NAME_oneline \ (*(char *(*)(X509_NAME *, char *, int))crypto_sw[14].ptr) #define X509_get_serialNumber (*(ASN1_INTEGER * (*)(X509 *)) crypto_sw[15].ptr) #define i2c_ASN1_INTEGER \ (*(int (*)(ASN1_INTEGER *, unsigned char **))crypto_sw[16].ptr) #define EVP_get_digestbyname \ (*(const EVP_MD *(*)(const char *))crypto_sw[17].ptr) #define EVP_Digest \ (*(int (*)( \ const void *, size_t, void *, unsigned int *, const EVP_MD *, void *)) \ crypto_sw[18] \ .ptr) #define i2d_X509 (*(int (*)(X509 *, unsigned char **))crypto_sw[19].ptr) #define BN_bn2hex (*(char *(*)(const BIGNUM *a))crypto_sw[20].ptr) #define ASN1_INTEGER_to_BN \ (*(BIGNUM * (*)(const ASN1_INTEGER *ai, BIGNUM *bn)) crypto_sw[21].ptr) #define BN_free (*(void (*)(const BIGNUM *a))crypto_sw[22].ptr) #define CRYPTO_free (*(void (*)(void *addr))crypto_sw[23].ptr) #define ERR_clear_error (*(void (*)(void))crypto_sw[24].ptr) #define OPENSSL_free(a) CRYPTO_free(a) /* use here ERR_remove_state, * while on some platforms function is not included into library due to * deprication */ #define OPENSSL_REMOVE_THREAD_STATE() ERR_remove_state(0) /* init_ssl_ctx() function updates this array. * It loads SSL library dynamically and changes NULLs to the actual addresses * of respective functions. The macros above (like SSL_connect()) are really * just calling these functions indirectly via the pointer. */ static struct ssl_func ssl_sw[] = {{"SSL_free", NULL}, {"SSL_accept", NULL}, {"SSL_connect", NULL}, {"SSL_read", NULL}, {"SSL_write", NULL}, {"SSL_get_error", NULL}, {"SSL_set_fd", NULL}, {"SSL_new", NULL}, {"SSL_CTX_new", NULL}, {"SSLv23_server_method", NULL}, {"SSL_library_init", NULL}, {"SSL_CTX_use_PrivateKey_file", NULL}, {"SSL_CTX_use_certificate_file", NULL}, {"SSL_CTX_set_default_passwd_cb", NULL}, {"SSL_CTX_free", NULL}, {"SSL_load_error_strings", NULL}, {"SSL_CTX_use_certificate_chain_file", NULL}, {"SSLv23_client_method", NULL}, {"SSL_pending", NULL}, {"SSL_CTX_set_verify", NULL}, {"SSL_shutdown", NULL}, {"SSL_CTX_load_verify_locations", NULL}, {"SSL_CTX_set_default_verify_paths", NULL}, {"SSL_CTX_set_verify_depth", NULL}, {"SSL_get_peer_certificate", NULL}, {"SSL_get_version", NULL}, {"SSL_get_current_cipher", NULL}, {"SSL_CIPHER_get_name", NULL}, {"SSL_CTX_check_private_key", NULL}, {"SSL_CTX_set_session_id_context", NULL}, {"SSL_CTX_ctrl", NULL}, {"SSL_CTX_set_cipher_list", NULL}, {"SSL_CTX_set_info_callback", NULL}, {"SSL_get_ex_data", NULL}, {"SSL_set_ex_data", NULL}, {"SSL_CTX_callback_ctrl", NULL}, {"SSL_get_servername", NULL}, {"SSL_set_SSL_CTX", NULL}, {"SSL_ctrl", NULL}, {NULL, NULL}}; /* Similar array as ssl_sw. These functions could be located in different * lib. */ static struct ssl_func crypto_sw[] = {{"CRYPTO_num_locks", NULL}, {"CRYPTO_set_locking_callback", NULL}, {"CRYPTO_set_id_callback", NULL}, {"ERR_get_error", NULL}, {"ERR_error_string", NULL}, {"ERR_remove_state", NULL}, {"ERR_free_strings", NULL}, {"ENGINE_cleanup", NULL}, {"CONF_modules_unload", NULL}, {"CRYPTO_cleanup_all_ex_data", NULL}, {"EVP_cleanup", NULL}, {"X509_free", NULL}, {"X509_get_subject_name", NULL}, {"X509_get_issuer_name", NULL}, {"X509_NAME_oneline", NULL}, {"X509_get_serialNumber", NULL}, {"i2c_ASN1_INTEGER", NULL}, {"EVP_get_digestbyname", NULL}, {"EVP_Digest", NULL}, {"i2d_X509", NULL}, {"BN_bn2hex", NULL}, {"ASN1_INTEGER_to_BN", NULL}, {"BN_free", NULL}, {"CRYPTO_free", NULL}, {"ERR_clear_error", NULL}, {NULL, NULL}}; #endif /* OPENSSL_API_1_1 */ #endif /* NO_SSL_DL */ #endif /* NO_SSL */ #if !defined(NO_CACHING) static const char month_names[][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; #endif /* !NO_CACHING */ /* Unified socket address. For IPv6 support, add IPv6 address structure in * the * union u. */ union usa { struct sockaddr sa; struct sockaddr_in sin; #if defined(USE_IPV6) struct sockaddr_in6 sin6; #endif }; /* Describes a string (chunk of memory). */ struct vec { const char *ptr; size_t len; }; struct mg_file_stat { /* File properties filled by mg_stat: */ uint64_t size; time_t last_modified; int is_directory; /* Set to 1 if mg_stat is called for a directory */ int is_gzipped; /* Set to 1 if the content is gzipped, in which * case we need a "Content-Eencoding: gzip" header */ int location; /* 0 = nowhere, 1 = on disk, 2 = in memory */ }; struct mg_file_in_memory { char *p; uint32_t pos; char mode; }; struct mg_file_access { /* File properties filled by mg_fopen: */ FILE *fp; #if defined(MG_USE_OPEN_FILE) /* TODO (low): Remove obsolete "file in memory" implementation. * In an "early 2017" discussion at Google groups * https://groups.google.com/forum/#!topic/civetweb/h9HT4CmeYqI * we decided to get rid of this feature (after some fade-out * phase). */ const char *membuf; #endif }; struct mg_file { struct mg_file_stat stat; struct mg_file_access access; }; #if defined(MG_USE_OPEN_FILE) #define STRUCT_FILE_INITIALIZER \ { \ {(uint64_t)0, (time_t)0, 0, 0, 0}, \ { \ (FILE *)NULL, (const char *)NULL \ } \ } #else #define STRUCT_FILE_INITIALIZER \ { \ {(uint64_t)0, (time_t)0, 0, 0, 0}, \ { \ (FILE *)NULL \ } \ } #endif /* Describes listening socket, or socket which was accept()-ed by the master * thread and queued for future handling by the worker thread. */ struct socket { SOCKET sock; /* Listening socket */ union usa lsa; /* Local socket address */ union usa rsa; /* Remote socket address */ unsigned char is_ssl; /* Is port SSL-ed */ unsigned char ssl_redir; /* Is port supposed to redirect everything to SSL * port */ unsigned char in_use; /* 0: invalid, 1: valid, 2: free */ }; /* Enum const for all options must be in sync with * static struct mg_option config_options[] * This is tested in the unit test (test/private.c) * "Private Config Options" */ enum { /* Once for each server */ LISTENING_PORTS, NUM_THREADS, RUN_AS_USER, CONFIG_TCP_NODELAY, /* Prepended CONFIG_ to avoid conflict with the * socket option typedef TCP_NODELAY. */ MAX_REQUEST_SIZE, LINGER_TIMEOUT, CONNECTION_QUEUE_SIZE, LISTEN_BACKLOG_SIZE, #if defined(__linux__) ALLOW_SENDFILE_CALL, #endif #if defined(_WIN32) CASE_SENSITIVE_FILES, #endif THROTTLE, ACCESS_LOG_FILE, ERROR_LOG_FILE, ENABLE_KEEP_ALIVE, REQUEST_TIMEOUT, KEEP_ALIVE_TIMEOUT, #if defined(USE_WEBSOCKET) WEBSOCKET_TIMEOUT, ENABLE_WEBSOCKET_PING_PONG, #endif DECODE_URL, #if defined(USE_LUA) LUA_BACKGROUND_SCRIPT, LUA_BACKGROUND_SCRIPT_PARAMS, #endif #if defined(USE_TIMERS) CGI_TIMEOUT, #endif /* Once for each domain */ DOCUMENT_ROOT, CGI_EXTENSIONS, CGI_ENVIRONMENT, PUT_DELETE_PASSWORDS_FILE, CGI_INTERPRETER, PROTECT_URI, AUTHENTICATION_DOMAIN, ENABLE_AUTH_DOMAIN_CHECK, SSI_EXTENSIONS, ENABLE_DIRECTORY_LISTING, GLOBAL_PASSWORDS_FILE, INDEX_FILES, ACCESS_CONTROL_LIST, EXTRA_MIME_TYPES, SSL_CERTIFICATE, SSL_CERTIFICATE_CHAIN, URL_REWRITE_PATTERN, HIDE_FILES, SSL_DO_VERIFY_PEER, SSL_CA_PATH, SSL_CA_FILE, SSL_VERIFY_DEPTH, SSL_DEFAULT_VERIFY_PATHS, SSL_CIPHER_LIST, SSL_PROTOCOL_VERSION, SSL_SHORT_TRUST, #if defined(USE_LUA) LUA_PRELOAD_FILE, LUA_SCRIPT_EXTENSIONS, LUA_SERVER_PAGE_EXTENSIONS, #if defined(MG_EXPERIMENTAL_INTERFACES) LUA_DEBUG_PARAMS, #endif #endif #if defined(USE_DUKTAPE) DUKTAPE_SCRIPT_EXTENSIONS, #endif #if defined(USE_WEBSOCKET) WEBSOCKET_ROOT, #endif #if defined(USE_LUA) && defined(USE_WEBSOCKET) LUA_WEBSOCKET_EXTENSIONS, #endif ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_HEADERS, ERROR_PAGES, #if !defined(NO_CACHING) STATIC_FILE_MAX_AGE, STATIC_FILE_CACHE_CONTROL, #endif #if !defined(NO_SSL) STRICT_HTTPS_MAX_AGE, #endif ADDITIONAL_HEADER, ALLOW_INDEX_SCRIPT_SUB_RES, #if defined(DAEMONIZE) ENABLE_DAEMONIZE, #endif NUM_OPTIONS }; /* Config option name, config types, default value. * Must be in the same order as the enum const above. */ static const struct mg_option config_options[] = { /* Once for each server */ {"listening_ports", MG_CONFIG_TYPE_STRING_LIST, "8080"}, {"num_threads", MG_CONFIG_TYPE_NUMBER, "50"}, {"run_as_user", MG_CONFIG_TYPE_STRING, NULL}, {"tcp_nodelay", MG_CONFIG_TYPE_NUMBER, "0"}, {"max_request_size", MG_CONFIG_TYPE_NUMBER, "16384"}, {"linger_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, {"connection_queue", MG_CONFIG_TYPE_NUMBER, "20"}, {"listen_backlog", MG_CONFIG_TYPE_NUMBER, "200"}, #if defined(__linux__) {"allow_sendfile_call", MG_CONFIG_TYPE_BOOLEAN, "yes"}, #endif #if defined(_WIN32) {"case_sensitive", MG_CONFIG_TYPE_BOOLEAN, "no"}, #endif {"throttle", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"access_log_file", MG_CONFIG_TYPE_FILE, NULL}, {"error_log_file", MG_CONFIG_TYPE_FILE, NULL}, {"enable_keep_alive", MG_CONFIG_TYPE_BOOLEAN, "no"}, {"request_timeout_ms", MG_CONFIG_TYPE_NUMBER, "30000"}, {"keep_alive_timeout_ms", MG_CONFIG_TYPE_NUMBER, "500"}, #if defined(USE_WEBSOCKET) {"websocket_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, {"enable_websocket_ping_pong", MG_CONFIG_TYPE_BOOLEAN, "no"}, #endif {"decode_url", MG_CONFIG_TYPE_BOOLEAN, "yes"}, #if defined(USE_LUA) {"lua_background_script", MG_CONFIG_TYPE_FILE, NULL}, {"lua_background_script_params", MG_CONFIG_TYPE_STRING_LIST, NULL}, #endif #if defined(USE_TIMERS) {"cgi_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, #endif /* Once for each domain */ {"document_root", MG_CONFIG_TYPE_DIRECTORY, NULL}, {"cgi_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.cgi$|**.pl$|**.php$"}, {"cgi_environment", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"put_delete_auth_file", MG_CONFIG_TYPE_FILE, NULL}, {"cgi_interpreter", MG_CONFIG_TYPE_FILE, NULL}, {"protect_uri", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"authentication_domain", MG_CONFIG_TYPE_STRING, "mydomain.com"}, {"enable_auth_domain_check", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"ssi_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.shtml$|**.shtm$"}, {"enable_directory_listing", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"global_auth_file", MG_CONFIG_TYPE_FILE, NULL}, {"index_files", MG_CONFIG_TYPE_STRING_LIST, #if defined(USE_LUA) "index.xhtml,index.html,index.htm," "index.lp,index.lsp,index.lua,index.cgi," "index.shtml,index.php"}, #else "index.xhtml,index.html,index.htm,index.cgi,index.shtml,index.php"}, #endif {"access_control_list", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"extra_mime_types", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"ssl_certificate", MG_CONFIG_TYPE_FILE, NULL}, {"ssl_certificate_chain", MG_CONFIG_TYPE_FILE, NULL}, {"url_rewrite_patterns", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"hide_files_patterns", MG_CONFIG_TYPE_EXT_PATTERN, NULL}, {"ssl_verify_peer", MG_CONFIG_TYPE_YES_NO_OPTIONAL, "no"}, {"ssl_ca_path", MG_CONFIG_TYPE_DIRECTORY, NULL}, {"ssl_ca_file", MG_CONFIG_TYPE_FILE, NULL}, {"ssl_verify_depth", MG_CONFIG_TYPE_NUMBER, "9"}, {"ssl_default_verify_paths", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"ssl_cipher_list", MG_CONFIG_TYPE_STRING, NULL}, {"ssl_protocol_version", MG_CONFIG_TYPE_NUMBER, "0"}, {"ssl_short_trust", MG_CONFIG_TYPE_BOOLEAN, "no"}, #if defined(USE_LUA) {"lua_preload_file", MG_CONFIG_TYPE_FILE, NULL}, {"lua_script_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.lua$"}, {"lua_server_page_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.lp$|**.lsp$"}, #if defined(MG_EXPERIMENTAL_INTERFACES) {"lua_debug", MG_CONFIG_TYPE_STRING, NULL}, #endif #endif #if defined(USE_DUKTAPE) /* The support for duktape is still in alpha version state. * The name of this config option might change. */ {"duktape_script_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.ssjs$"}, #endif #if defined(USE_WEBSOCKET) {"websocket_root", MG_CONFIG_TYPE_DIRECTORY, NULL}, #endif #if defined(USE_LUA) && defined(USE_WEBSOCKET) {"lua_websocket_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.lua$"}, #endif {"access_control_allow_origin", MG_CONFIG_TYPE_STRING, "*"}, {"access_control_allow_methods", MG_CONFIG_TYPE_STRING, "*"}, {"access_control_allow_headers", MG_CONFIG_TYPE_STRING, "*"}, {"error_pages", MG_CONFIG_TYPE_DIRECTORY, NULL}, #if !defined(NO_CACHING) {"static_file_max_age", MG_CONFIG_TYPE_NUMBER, "3600"}, {"static_file_cache_control", MG_CONFIG_TYPE_STRING, NULL}, #endif #if !defined(NO_SSL) {"strict_transport_security_max_age", MG_CONFIG_TYPE_NUMBER, NULL}, #endif {"additional_header", MG_CONFIG_TYPE_STRING_MULTILINE, NULL}, {"allow_index_script_resource", MG_CONFIG_TYPE_BOOLEAN, "no"}, #if defined(DAEMONIZE) {"daemonize", MG_CONFIG_TYPE_BOOLEAN, "no"}, #endif {NULL, MG_CONFIG_TYPE_UNKNOWN, NULL}}; /* Check if the config_options and the corresponding enum have compatible * sizes. */ mg_static_assert((sizeof(config_options) / sizeof(config_options[0])) == (NUM_OPTIONS + 1), "config_options and enum not sync"); enum { REQUEST_HANDLER, WEBSOCKET_HANDLER, AUTH_HANDLER }; struct mg_handler_info { /* Name/Pattern of the URI. */ char *uri; size_t uri_len; /* handler type */ int handler_type; /* Handler for http/https or authorization requests. */ mg_request_handler handler; unsigned int refcount; pthread_mutex_t refcount_mutex; /* Protects refcount */ pthread_cond_t refcount_cond; /* Signaled when handler refcount is decremented */ /* Handler for ws/wss (websocket) requests. */ mg_websocket_connect_handler connect_handler; mg_websocket_ready_handler ready_handler; mg_websocket_data_handler data_handler; mg_websocket_close_handler close_handler; /* accepted subprotocols for ws/wss requests. */ struct mg_websocket_subprotocols *subprotocols; /* Handler for authorization requests */ mg_authorization_handler auth_handler; /* User supplied argument for the handler function. */ void *cbdata; /* next handler in a linked list */ struct mg_handler_info *next; }; enum { CONTEXT_INVALID, CONTEXT_SERVER, CONTEXT_HTTP_CLIENT, CONTEXT_WS_CLIENT }; struct mg_domain_context { SSL_CTX *ssl_ctx; /* SSL context */ char *config[NUM_OPTIONS]; /* Civetweb configuration parameters */ struct mg_handler_info *handlers; /* linked list of uri handlers */ /* Server nonce */ uint64_t auth_nonce_mask; /* Mask for all nonce values */ unsigned long nonce_count; /* Used nonces, used for authentication */ #if defined(USE_LUA) && defined(USE_WEBSOCKET) /* linked list of shared lua websockets */ struct mg_shared_lua_websocket_list *shared_lua_websockets; #endif /* Linked list of domains */ struct mg_domain_context *next; }; struct mg_context { /* Part 1 - Physical context: * This holds threads, ports, timeouts, ... * set for the entire server, independent from the * addressed hostname. */ /* Connection related */ int context_type; /* See CONTEXT_* above */ struct socket *listening_sockets; struct mg_pollfd *listening_socket_fds; unsigned int num_listening_sockets; struct mg_connection *worker_connections; /* The connection struct, pre- * allocated for each worker */ #if defined(USE_SERVER_STATS) int active_connections; int max_active_connections; int64_t total_connections; int64_t total_requests; int64_t total_data_read; int64_t total_data_written; #endif /* Thread related */ volatile int stop_flag; /* Should we stop event loop */ pthread_mutex_t thread_mutex; /* Protects (max|num)_threads */ pthread_t masterthreadid; /* The master thread ID */ unsigned int cfg_worker_threads; /* The number of configured worker threads. */ pthread_t *worker_threadids; /* The worker thread IDs */ unsigned long starter_thread_idx; /* thread index which called mg_start */ /* Connection to thread dispatching */ #if defined(ALTERNATIVE_QUEUE) struct socket *client_socks; void **client_wait_events; #else struct socket *squeue; /* Socket queue (sq) : accepted sockets waiting for a worker thread */ volatile int sq_head; /* Head of the socket queue */ volatile int sq_tail; /* Tail of the socket queue */ pthread_cond_t sq_full; /* Signaled when socket is produced */ pthread_cond_t sq_empty; /* Signaled when socket is consumed */ volatile int sq_blocked; /* Status information: sq is full */ int sq_size; /* No of elements in socket queue */ #if defined(USE_SERVER_STATS) int sq_max_fill; #endif /* USE_SERVER_STATS */ #endif /* ALTERNATIVE_QUEUE */ /* Memory related */ unsigned int max_request_size; /* The max request size */ #if defined(USE_SERVER_STATS) struct mg_memory_stat ctx_memory; #endif /* Operating system related */ char *systemName; /* What operating system is running */ time_t start_time; /* Server start time, used for authentication * and for diagnstics. */ #if defined(USE_TIMERS) struct ttimers *timers; #endif /* Lua specific: Background operations and shared websockets */ #if defined(USE_LUA) void *lua_background_state; #endif /* Server nonce */ pthread_mutex_t nonce_mutex; /* Protects nonce_count */ /* Server callbacks */ struct mg_callbacks callbacks; /* User-defined callback function */ void *user_data; /* User-defined data */ /* Part 2 - Logical domain: * This holds hostname, TLS certificate, document root, ... * set for a domain hosted at the server. * There may be multiple domains hosted at one physical server. * The default domain "dd" is the first element of a list of * domains. */ struct mg_domain_context dd; /* default domain */ }; #if defined(USE_SERVER_STATS) static struct mg_memory_stat mg_common_memory = {0, 0, 0}; static struct mg_memory_stat * get_memory_stat(struct mg_context *ctx) { if (ctx) { return &(ctx->ctx_memory); } return &mg_common_memory; } #endif enum { CONNECTION_TYPE_INVALID, CONNECTION_TYPE_REQUEST, CONNECTION_TYPE_RESPONSE }; struct mg_connection { int connection_type; /* see CONNECTION_TYPE_* above */ struct mg_request_info request_info; struct mg_response_info response_info; struct mg_context *phys_ctx; struct mg_domain_context *dom_ctx; #if defined(USE_SERVER_STATS) int conn_state; /* 0 = undef, numerical value may change in different * versions. For the current definition, see * mg_get_connection_info_impl */ #endif const char *host; /* Host (HTTP/1.1 header or SNI) */ SSL *ssl; /* SSL descriptor */ struct socket client; /* Connected client */ time_t conn_birth_time; /* Time (wall clock) when connection was * established */ #if defined(USE_SERVER_STATS) time_t conn_close_time; /* Time (wall clock) when connection was * closed (or 0 if still open) */ #endif struct timespec req_time; /* Time (since system start) when the request * was received */ int64_t num_bytes_sent; /* Total bytes sent to client */ int64_t content_len; /* How many bytes of content can be read * !is_chunked: Content-Length header value * or -1 (until connection closed, * not allowed for a request) * is_chunked: >= 0, appended gradually */ int64_t consumed_content; /* How many bytes of content have been read */ int is_chunked; /* Transfer-Encoding is chunked: * 0 = not chunked, * 1 = chunked, not yet, or some data read, * 2 = chunked, has error, * 3 = chunked, all data read except trailer, * 4 = chunked, all data read */ char *buf; /* Buffer for received data */ char *path_info; /* PATH_INFO part of the URL */ int must_close; /* 1 if connection must be closed */ int accept_gzip; /* 1 if gzip encoding is accepted */ int in_error_handler; /* 1 if in handler for user defined error * pages */ #if defined(USE_WEBSOCKET) int in_websocket_handling; /* 1 if in read_websocket */ #endif int handled_requests; /* Number of requests handled by this connection */ int buf_size; /* Buffer size */ int request_len; /* Size of the request + headers in a buffer */ int data_len; /* Total size of data in a buffer */ int status_code; /* HTTP reply status code, e.g. 200 */ int throttle; /* Throttling, bytes/sec. <= 0 means no * throttle */ time_t last_throttle_time; /* Last time throttled data was sent */ int last_throttle_bytes; /* Bytes sent this second */ pthread_mutex_t mutex; /* Used by mg_(un)lock_connection to ensure * atomic transmissions for websockets */ #if defined(USE_LUA) && defined(USE_WEBSOCKET) void *lua_websocket_state; /* Lua_State for a websocket connection */ #endif void *tls_user_ptr; /* User defined pointer in thread local storage, * for quick access */ }; /* Directory entry */ struct de { struct mg_connection *conn; char *file_name; struct mg_file_stat file; }; #if defined(USE_WEBSOCKET) static int is_websocket_protocol(const struct mg_connection *conn); #else #define is_websocket_protocol(conn) (0) #endif #define mg_cry_internal(conn, fmt, ...) \ mg_cry_internal_wrap(conn, NULL, __func__, __LINE__, fmt, __VA_ARGS__) #define mg_cry_ctx_internal(ctx, fmt, ...) \ mg_cry_internal_wrap(NULL, ctx, __func__, __LINE__, fmt, __VA_ARGS__) static void mg_cry_internal_wrap(const struct mg_connection *conn, struct mg_context *ctx, const char *func, unsigned line, const char *fmt, ...) PRINTF_ARGS(5, 6); #if !defined(NO_THREAD_NAME) #if defined(_WIN32) && defined(_MSC_VER) /* Set the thread name for debugging purposes in Visual Studio * http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx */ # pragma pack(push, 8) typedef struct tagTHREADNAME_INFO { DWORD dwType; /* Must be 0x1000. */ LPCSTR szName; /* Pointer to name (in user addr space). */ DWORD dwThreadID; /* Thread ID (-1=caller thread). */ DWORD dwFlags; /* Reserved for future use, must be zero. */ } THREADNAME_INFO; # pragma pack(pop) #elif defined(__linux__) #include #include #if defined(ALTERNATIVE_QUEUE) #include #endif /* ALTERNATIVE_QUEUE */ #if defined(ALTERNATIVE_QUEUE) static void * event_create(void) { int evhdl = eventfd(0, EFD_CLOEXEC); int *ret; if (evhdl == -1) { /* Linux uses -1 on error, Windows NULL. */ /* However, Linux does not return 0 on success either. */ return 0; } ret = (int *)mg_malloc(sizeof(int)); if (ret) { *ret = evhdl; } else { (void)close(evhdl); } return (void *)ret; } static int event_wait(void *eventhdl) { uint64_t u; int evhdl, s; if (!eventhdl) { /* error */ return 0; } evhdl = *(int *)eventhdl; s = (int)read(evhdl, &u, sizeof(u)); if (s != sizeof(u)) { /* error */ return 0; } (void)u; /* the value is not required */ return 1; } static int event_signal(void *eventhdl) { uint64_t u = 1; int evhdl, s; if (!eventhdl) { /* error */ return 0; } evhdl = *(int *)eventhdl; s = (int)write(evhdl, &u, sizeof(u)); if (s != sizeof(u)) { /* error */ return 0; } return 1; } static void event_destroy(void *eventhdl) { int evhdl; if (!eventhdl) { /* error */ return; } evhdl = *(int *)eventhdl; close(evhdl); mg_free(eventhdl); } #endif #endif #if !defined(__linux__) && !defined(_WIN32) && defined(ALTERNATIVE_QUEUE) struct posix_event { pthread_mutex_t mutex; pthread_cond_t cond; int signaled; }; static void * event_create(void) { struct posix_event *ret = mg_malloc(sizeof(struct posix_event)); if (ret == 0) { /* out of memory */ return 0; } if (0 != pthread_mutex_init(&(ret->mutex), NULL)) { /* pthread mutex not available */ mg_free(ret); return 0; } if (0 != pthread_cond_init(&(ret->cond), NULL)) { /* pthread cond not available */ pthread_mutex_destroy(&(ret->mutex)); mg_free(ret); return 0; } ret->signaled = 0; return (void *)ret; } static int event_wait(void *eventhdl) { struct posix_event *ev = (struct posix_event *)eventhdl; pthread_mutex_lock(&(ev->mutex)); while (!ev->signaled) { pthread_cond_wait(&(ev->cond), &(ev->mutex)); } ev->signaled = 0; pthread_mutex_unlock(&(ev->mutex)); return 1; } static int event_signal(void *eventhdl) { struct posix_event *ev = (struct posix_event *)eventhdl; pthread_mutex_lock(&(ev->mutex)); pthread_cond_signal(&(ev->cond)); ev->signaled = 1; pthread_mutex_unlock(&(ev->mutex)); return 1; } static void event_destroy(void *eventhdl) { struct posix_event *ev = (struct posix_event *)eventhdl; pthread_cond_destroy(&(ev->cond)); pthread_mutex_destroy(&(ev->mutex)); mg_free(ev); } #endif static void mg_set_thread_name(const char *name) { char threadName[16 + 1]; /* 16 = Max. thread length in Linux/OSX/.. */ mg_snprintf( NULL, NULL, threadName, sizeof(threadName), "civetweb-%s", name); #if defined(_WIN32) #if defined(_MSC_VER) /* Windows and Visual Studio Compiler */ __try { THREADNAME_INFO info; info.dwType = 0x1000; info.szName = threadName; info.dwThreadID = ~0U; info.dwFlags = 0; RaiseException(0x406D1388, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR *)&info); } __except (EXCEPTION_EXECUTE_HANDLER) { } #elif defined(__MINGW32__) /* No option known to set thread name for MinGW known */ #endif #elif defined(_GNU_SOURCE) && defined(__GLIBC__) \ && ((__GLIBC__ > 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ >= 12))) /* pthread_setname_np first appeared in glibc in version 2.12 */ #if defined(__MACH__) && defined(__APPLE__) /* OS X only current thread name can be changed */ (void)pthread_setname_np(threadName); #else (void)pthread_setname_np(pthread_self(), threadName); #endif #elif defined(__linux__) /* On Linux we can use the prctl function. * When building for Linux Standard Base (LSB) use * NO_THREAD_NAME. However, thread names are a big * help for debugging, so the stadard is to set them. */ (void)prctl(PR_SET_NAME, threadName, 0, 0, 0); #endif } #else /* !defined(NO_THREAD_NAME) */ void mg_set_thread_name(const char *threadName) { } #endif #if defined(MG_LEGACY_INTERFACE) const char ** mg_get_valid_option_names(void) { /* This function is deprecated. Use mg_get_valid_options instead. */ static const char *data[2 * sizeof(config_options) / sizeof(config_options[0])] = {0}; int i; for (i = 0; config_options[i].name != NULL; i++) { data[i * 2] = config_options[i].name; data[i * 2 + 1] = config_options[i].default_value; } return data; } #endif const struct mg_option * mg_get_valid_options(void) { return config_options; } /* Do not open file (used in is_file_in_memory) */ #define MG_FOPEN_MODE_NONE (0) /* Open file for read only access */ #define MG_FOPEN_MODE_READ (1) /* Open file for writing, create and overwrite */ #define MG_FOPEN_MODE_WRITE (2) /* Open file for writing, create and append */ #define MG_FOPEN_MODE_APPEND (4) /* If a file is in memory, set all "stat" members and the membuf pointer of * output filep and return 1, otherwise return 0 and don't modify anything. */ static int open_file_in_memory(const struct mg_connection *conn, const char *path, struct mg_file *filep, int mode) { #if defined(MG_USE_OPEN_FILE) size_t size = 0; const char *buf = NULL; if (!conn) { return 0; } if ((mode != MG_FOPEN_MODE_NONE) && (mode != MG_FOPEN_MODE_READ)) { return 0; } if (conn->phys_ctx->callbacks.open_file) { buf = conn->phys_ctx->callbacks.open_file(conn, path, &size); if (buf != NULL) { if (filep == NULL) { /* This is a file in memory, but we cannot store the * properties * now. * Called from "is_file_in_memory" function. */ return 1; } /* NOTE: override filep->size only on success. Otherwise, it * might * break constructs like if (!mg_stat() || !mg_fopen()) ... */ filep->access.membuf = buf; filep->access.fp = NULL; /* Size was set by the callback */ filep->stat.size = size; /* Assume the data may change during runtime by setting * last_modified = now */ filep->stat.last_modified = time(NULL); filep->stat.is_directory = 0; filep->stat.is_gzipped = 0; } } return (buf != NULL); #else (void)conn; (void)path; (void)filep; (void)mode; return 0; #endif } static int is_file_in_memory(const struct mg_connection *conn, const char *path) { return open_file_in_memory(conn, path, NULL, MG_FOPEN_MODE_NONE); } static int is_file_opened(const struct mg_file_access *fileacc) { if (!fileacc) { return 0; } #if defined(MG_USE_OPEN_FILE) return (fileacc->membuf != NULL) || (fileacc->fp != NULL); #else return (fileacc->fp != NULL); #endif } #if !defined(NO_FILESYSTEMS) static int mg_stat(const struct mg_connection *conn, const char *path, struct mg_file_stat *filep); /* mg_fopen will open a file either in memory or on the disk. * The input parameter path is a string in UTF-8 encoding. * The input parameter mode is MG_FOPEN_MODE_* * On success, either fp or membuf will be set in the output * struct file. All status members will also be set. * The function returns 1 on success, 0 on error. */ static int mg_fopen(const struct mg_connection *conn, const char *path, int mode, struct mg_file *filep) { int found; if (!filep) { return 0; } filep->access.fp = NULL; #if defined(MG_USE_OPEN_FILE) filep->access.membuf = NULL; #endif if (!is_file_in_memory(conn, path)) { /* filep is initialized in mg_stat: all fields with memset to, * some fields like size and modification date with values */ found = mg_stat(conn, path, &(filep->stat)); if ((mode == MG_FOPEN_MODE_READ) && (!found)) { /* file does not exist and will not be created */ return 0; } #if defined(_WIN32) { wchar_t wbuf[W_PATH_MAX]; path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); switch (mode) { case MG_FOPEN_MODE_READ: filep->access.fp = _wfopen(wbuf, L"rb"); break; case MG_FOPEN_MODE_WRITE: filep->access.fp = _wfopen(wbuf, L"wb"); break; case MG_FOPEN_MODE_APPEND: filep->access.fp = _wfopen(wbuf, L"ab"); break; } } #else /* Linux et al already use unicode. No need to convert. */ switch (mode) { case MG_FOPEN_MODE_READ: filep->access.fp = fopen(path, "r"); break; case MG_FOPEN_MODE_WRITE: filep->access.fp = fopen(path, "w"); break; case MG_FOPEN_MODE_APPEND: filep->access.fp = fopen(path, "a"); break; } #endif if (!found) { /* File did not exist before fopen was called. * Maybe it has been created now. Get stat info * like creation time now. */ found = mg_stat(conn, path, &(filep->stat)); (void)found; } /* file is on disk */ return (filep->access.fp != NULL); } else { #if defined(MG_USE_OPEN_FILE) /* is_file_in_memory returned true */ if (open_file_in_memory(conn, path, filep, mode)) { /* file is in memory */ return (filep->access.membuf != NULL); } #endif } /* Open failed */ return 0; } /* return 0 on success, just like fclose */ static int mg_fclose(struct mg_file_access *fileacc) { int ret = -1; if (fileacc != NULL) { if (fileacc->fp != NULL) { ret = fclose(fileacc->fp); #if defined(MG_USE_OPEN_FILE) } else if (fileacc->membuf != NULL) { ret = 0; #endif } /* reset all members of fileacc */ memset(fileacc, 0, sizeof(*fileacc)); } return ret; } #endif /* NO_FILESYSTEMS */ static void mg_strlcpy(register char *dst, register const char *src, size_t n) { for (; *src != '\0' && n > 1; n--) { *dst++ = *src++; } *dst = '\0'; } static int lowercase(const char *s) { return tolower((unsigned char)*s); } int mg_strncasecmp(const char *s1, const char *s2, size_t len) { int diff = 0; if (len > 0) { do { diff = lowercase(s1++) - lowercase(s2++); } while (diff == 0 && s1[-1] != '\0' && --len > 0); } return diff; } int mg_strcasecmp(const char *s1, const char *s2) { int diff; do { diff = lowercase(s1++) - lowercase(s2++); } while (diff == 0 && s1[-1] != '\0'); return diff; } static char * mg_strndup_ctx(const char *ptr, size_t len, struct mg_context *ctx) { char *p; (void)ctx; /* Avoid Visual Studio warning if USE_SERVER_STATS is not * defined */ if ((p = (char *)mg_malloc_ctx(len + 1, ctx)) != NULL) { mg_strlcpy(p, ptr, len + 1); } return p; } static char * mg_strdup_ctx(const char *str, struct mg_context *ctx) { return mg_strndup_ctx(str, strlen(str), ctx); } static char * mg_strdup(const char *str) { return mg_strndup_ctx(str, strlen(str), NULL); } static const char * mg_strcasestr(const char *big_str, const char *small_str) { size_t i, big_len = strlen(big_str), small_len = strlen(small_str); if (big_len >= small_len) { for (i = 0; i <= (big_len - small_len); i++) { if (mg_strncasecmp(big_str + i, small_str, small_len) == 0) { return big_str + i; } } } return NULL; } /* Return null terminated string of given maximum length. * Report errors if length is exceeded. */ static void mg_vsnprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, va_list ap) { int n, ok; if (buflen == 0) { if (truncated) { *truncated = 1; } return; } #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wformat-nonliteral" /* Using fmt as a non-literal is intended here, since it is mostly called * indirectly by mg_snprintf */ #endif n = (int)vsnprintf_impl(buf, buflen, fmt, ap); ok = (n >= 0) && ((size_t)n < buflen); #if defined(__clang__) # pragma clang diagnostic pop #endif if (ok) { if (truncated) { *truncated = 0; } } else { if (truncated) { *truncated = 1; } mg_cry_internal(conn, "truncating vsnprintf buffer: [%.*s]", (int)((buflen > 200) ? 200 : (buflen - 1)), buf); n = (int)buflen - 1; } buf[n] = '\0'; } static void mg_snprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, ...) { va_list ap; va_start(ap, fmt); mg_vsnprintf(conn, truncated, buf, buflen, fmt, ap); va_end(ap); } static int get_option_index(const char *name) { int i; for (i = 0; config_options[i].name != NULL; i++) { if (strcmp(config_options[i].name, name) == 0) { return i; } } return -1; } const char * mg_get_option(const struct mg_context *ctx, const char *name) { int i; if ((i = get_option_index(name)) == -1) { return NULL; } else if (!ctx || ctx->dd.config[i] == NULL) { return ""; } else { return ctx->dd.config[i]; } } #define mg_get_option DO_NOT_USE_THIS_FUNCTION_INTERNALLY__access_directly struct mg_context * mg_get_context(const struct mg_connection *conn) { return (conn == NULL) ? (struct mg_context *)NULL : (conn->phys_ctx); } void * mg_get_user_data(const struct mg_context *ctx) { return (ctx == NULL) ? NULL : ctx->user_data; } void * mg_get_user_context_data(const struct mg_connection *conn) { return mg_get_user_data(mg_get_context(conn)); } void * mg_get_thread_pointer(const struct mg_connection *conn) { /* both methods should return the same pointer */ if (conn) { /* quick access, in case conn is known */ return conn->tls_user_ptr; } else { /* otherwise get pointer from thread local storage (TLS) */ struct mg_workerTLS *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); return tls->user_ptr; } } void mg_set_user_connection_data(struct mg_connection *conn, void *data) { if (conn != NULL) { conn->request_info.conn_data = data; } } void * mg_get_user_connection_data(const struct mg_connection *conn) { if (conn != NULL) { return conn->request_info.conn_data; } return NULL; } #if defined(MG_LEGACY_INTERFACE) /* Deprecated: Use mg_get_server_ports instead. */ size_t mg_get_ports(const struct mg_context *ctx, size_t size, int *ports, int *ssl) { size_t i; if (!ctx) { return 0; } for (i = 0; i < size && i < ctx->num_listening_sockets; i++) { ssl[i] = ctx->listening_sockets[i].is_ssl; ports[i] = #if defined(USE_IPV6) (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET6) ? ntohs(ctx->listening_sockets[i].lsa.sin6.sin6_port) : #endif ntohs(ctx->listening_sockets[i].lsa.sin.sin_port); } return i; } #endif int mg_get_server_ports(const struct mg_context *ctx, int size, struct mg_server_port *ports) { int i, cnt = 0; if (size <= 0) { return -1; } memset(ports, 0, sizeof(*ports) * (size_t)size); if (!ctx) { return -1; } if (!ctx->listening_sockets) { return -1; } for (i = 0; (i < size) && (i < (int)ctx->num_listening_sockets); i++) { ports[cnt].port = #if defined(USE_IPV6) (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET6) ? ntohs(ctx->listening_sockets[i].lsa.sin6.sin6_port) : #endif ntohs(ctx->listening_sockets[i].lsa.sin.sin_port); ports[cnt].is_ssl = ctx->listening_sockets[i].is_ssl; ports[cnt].is_redirect = ctx->listening_sockets[i].ssl_redir; if (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET) { /* IPv4 */ ports[cnt].protocol = 1; cnt++; } else if (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET6) { /* IPv6 */ ports[cnt].protocol = 3; cnt++; } } return cnt; } static void sockaddr_to_string(char *buf, size_t len, const union usa *usa) { buf[0] = '\0'; if (!usa) { return; } if (usa->sa.sa_family == AF_INET) { getnameinfo(&usa->sa, sizeof(usa->sin), buf, (unsigned)len, NULL, 0, NI_NUMERICHOST); } #if defined(USE_IPV6) else if (usa->sa.sa_family == AF_INET6) { getnameinfo(&usa->sa, sizeof(usa->sin6), buf, (unsigned)len, NULL, 0, NI_NUMERICHOST); } #endif } /* Convert time_t to a string. According to RFC2616, Sec 14.18, this must be * included in all responses other than 100, 101, 5xx. */ static void gmt_time_string(char *buf, size_t buf_len, time_t *t) { #if !defined(REENTRANT_TIME) struct tm *tm; tm = ((t != NULL) ? gmtime(t) : NULL); if (tm != NULL) { #else struct tm _tm; struct tm *tm = &_tm; if (t != NULL) { gmtime_r(t, tm); #endif strftime(buf, buf_len, "%a, %d %b %Y %H:%M:%S GMT", tm); } else { mg_strlcpy(buf, "Thu, 01 Jan 1970 00:00:00 GMT", buf_len); buf[buf_len - 1] = '\0'; } } /* difftime for struct timespec. Return value is in seconds. */ static double mg_difftimespec(const struct timespec *ts_now, const struct timespec *ts_before) { return (double)(ts_now->tv_nsec - ts_before->tv_nsec) * 1.0E-9 + (double)(ts_now->tv_sec - ts_before->tv_sec); } #if defined(MG_EXTERNAL_FUNCTION_mg_cry_internal_impl) static void mg_cry_internal_impl(const struct mg_connection *conn, const char *func, unsigned line, const char *fmt, va_list ap); #include "external_mg_cry_internal_impl.inl" #elif !defined(NO_FILESYSTEMS) /* Print error message to the opened error log stream. */ static void mg_cry_internal_impl(const struct mg_connection *conn, const char *func, unsigned line, const char *fmt, va_list ap) { char buf[MG_BUF_LEN], src_addr[IP_ADDR_STR_LEN]; struct mg_file fi; time_t timestamp; /* Unused, in the RELEASE build */ (void)func; (void)line; #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif IGNORE_UNUSED_RESULT(vsnprintf_impl(buf, sizeof(buf), fmt, ap)); #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic pop #endif buf[sizeof(buf) - 1] = 0; DEBUG_TRACE("mg_cry called from %s:%u: %s", func, line, buf); if (!conn) { /* puts(buf); */ return; } /* Do not lock when getting the callback value, here and below. * I suppose this is fine, since function cannot disappear in the * same way string option can. */ if ((conn->phys_ctx->callbacks.log_message == NULL) || (conn->phys_ctx->callbacks.log_message(conn, buf) == 0)) { if (conn->dom_ctx->config[ERROR_LOG_FILE] != NULL) { if (mg_fopen(conn, conn->dom_ctx->config[ERROR_LOG_FILE], MG_FOPEN_MODE_APPEND, &fi) == 0) { fi.access.fp = NULL; } } else { fi.access.fp = NULL; } if (fi.access.fp != NULL) { flockfile(fi.access.fp); timestamp = time(NULL); sockaddr_to_string(src_addr, sizeof(src_addr), &conn->client.rsa); fprintf(fi.access.fp, "[%010lu] [error] [client %s] ", (unsigned long)timestamp, src_addr); if (conn->request_info.request_method != NULL) { fprintf(fi.access.fp, "%s %s: ", conn->request_info.request_method, conn->request_info.request_uri ? conn->request_info.request_uri : ""); } fprintf(fi.access.fp, "%s", buf); fputc('\n', fi.access.fp); fflush(fi.access.fp); funlockfile(fi.access.fp); (void)mg_fclose(&fi.access); /* Ignore errors. We can't call * mg_cry here anyway ;-) */ } } } #else #error Must either enable filesystems or provide a custom mg_cry_internal_impl implementation #endif /* Externally provided function */ /* Construct fake connection structure. Used for logging, if connection * is not applicable at the moment of logging. */ static struct mg_connection * fake_connection(struct mg_connection *fc, struct mg_context *ctx) { static const struct mg_connection conn_zero = {0}; *fc = conn_zero; fc->phys_ctx = ctx; fc->dom_ctx = &(ctx->dd); return fc; } static void mg_cry_internal_wrap(const struct mg_connection *conn, struct mg_context *ctx, const char *func, unsigned line, const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (!conn && ctx) { struct mg_connection fc; mg_cry_internal_impl(fake_connection(&fc, ctx), func, line, fmt, ap); } else { mg_cry_internal_impl(conn, func, line, fmt, ap); } va_end(ap); } void mg_cry(const struct mg_connection *conn, const char *fmt, ...) { va_list ap; va_start(ap, fmt); mg_cry_internal_impl(conn, "user", 0, fmt, ap); va_end(ap); } #define mg_cry DO_NOT_USE_THIS_FUNCTION__USE_mg_cry_internal const char * mg_version(void) { return CIVETWEB_VERSION; } const struct mg_request_info * mg_get_request_info(const struct mg_connection *conn) { if (!conn) { return NULL; } #if defined(MG_ALLOW_USING_GET_REQUEST_INFO_FOR_RESPONSE) if (conn->connection_type == CONNECTION_TYPE_RESPONSE) { char txt[16]; struct mg_workerTLS *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); snprintf(txt, sizeof(txt), "%03i", conn->response_info.status_code); if (strlen(txt) == 3) { memcpy(tls->txtbuf, txt, 4); } else { strcpy(tls->txtbuf, "ERR"); } ((struct mg_connection *)conn)->request_info.local_uri = ((struct mg_connection *)conn)->request_info.request_uri = tls->txtbuf; /* use thread safe buffer */ ((struct mg_connection *)conn)->request_info.num_headers = conn->response_info.num_headers; memcpy(((struct mg_connection *)conn)->request_info.http_headers, conn->response_info.http_headers, sizeof(conn->response_info.http_headers)); } else #endif if (conn->connection_type != CONNECTION_TYPE_REQUEST) { return NULL; } return &conn->request_info; } const struct mg_response_info * mg_get_response_info(const struct mg_connection *conn) { if (!conn) { return NULL; } if (conn->connection_type != CONNECTION_TYPE_RESPONSE) { return NULL; } return &conn->response_info; } static const char * get_proto_name(const struct mg_connection *conn) { #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunreachable-code" /* Depending on USE_WEBSOCKET and NO_SSL, some oft the protocols might be * not supported. Clang raises an "unreachable code" warning for parts of ?: * unreachable, but splitting into four different #ifdef clauses here is more * complicated. */ #endif const struct mg_request_info *ri = &conn->request_info; const char *proto = (is_websocket_protocol(conn) ? (ri->is_ssl ? "wss" : "ws") : (ri->is_ssl ? "https" : "http")); return proto; #if defined(__clang__) # pragma clang diagnostic pop #endif } int mg_get_request_link(const struct mg_connection *conn, char *buf, size_t buflen) { if ((buflen < 1) || (buf == 0) || (conn == 0)) { return -1; } else { int truncated = 0; const struct mg_request_info *ri = &conn->request_info; const char *proto = get_proto_name(conn); if (ri->local_uri == NULL) { return -1; } if ((ri->request_uri != NULL) && (0 != strcmp(ri->local_uri, ri->request_uri))) { /* The request uri is different from the local uri. * This is usually if an absolute URI, including server * name has been provided. */ mg_snprintf(conn, &truncated, buf, buflen, "%s://%s", proto, ri->request_uri); if (truncated) { return -1; } return 0; } else { /* The common case is a relative URI, so we have to * construct an absolute URI from server name and port */ #if defined(USE_IPV6) int is_ipv6 = (conn->client.lsa.sa.sa_family == AF_INET6); int port = is_ipv6 ? htons(conn->client.lsa.sin6.sin6_port) : htons(conn->client.lsa.sin.sin_port); #else int port = htons(conn->client.lsa.sin.sin_port); #endif int def_port = ri->is_ssl ? 443 : 80; int auth_domain_check_enabled = conn->dom_ctx->config[ENABLE_AUTH_DOMAIN_CHECK] && (!mg_strcasecmp( conn->dom_ctx->config[ENABLE_AUTH_DOMAIN_CHECK], "yes")); const char *server_domain = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; char portstr[16]; char server_ip[48]; if (port != def_port) { (snprintf)(portstr, sizeof(portstr), ":%u", (unsigned)port); } else { portstr[0] = 0; } if (!auth_domain_check_enabled || !server_domain) { sockaddr_to_string(server_ip, sizeof(server_ip), &conn->client.lsa); server_domain = server_ip; } mg_snprintf(conn, &truncated, buf, buflen, "%s://%s%s%s", proto, server_domain, portstr, ri->local_uri); if (truncated) { return -1; } return 0; } } } /* Skip the characters until one of the delimiters characters found. * 0-terminate resulting word. Skip the delimiter and following whitespaces. * Advance pointer to buffer to the next word. Return found 0-terminated * word. * Delimiters can be quoted with quotechar. */ static char * skip_quoted(char **buf, const char *delimiters, const char *whitespace, char quotechar) { char *p, *begin_word, *end_word, *end_whitespace; begin_word = *buf; end_word = begin_word + strcspn(begin_word, delimiters); /* Check for quotechar */ if (end_word > begin_word) { p = end_word - 1; while (*p == quotechar) { /* While the delimiter is quoted, look for the next delimiter. */ /* This happens, e.g., in calls from parse_auth_header, * if the user name contains a " character. */ /* If there is anything beyond end_word, copy it. */ if (*end_word != '\0') { size_t end_off = strcspn(end_word + 1, delimiters); memmove(p, end_word, end_off + 1); p += end_off; /* p must correspond to end_word - 1 */ end_word += end_off + 1; } else { *p = '\0'; break; } } for (p++; p < end_word; p++) { *p = '\0'; } } if (*end_word == '\0') { *buf = end_word; } else { #if defined(GCC_DIAGNOSTIC) /* Disable spurious conversion warning for GCC */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wsign-conversion" #endif /* defined(GCC_DIAGNOSTIC) */ end_whitespace = end_word + strspn(&end_word[1], whitespace) + 1; #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ for (p = end_word; p < end_whitespace; p++) { *p = '\0'; } *buf = end_whitespace; } return begin_word; } /* Return HTTP header value, or NULL if not found. */ static const char * get_header(const struct mg_header *hdr, int num_hdr, const char *name) { int i; for (i = 0; i < num_hdr; i++) { if (!mg_strcasecmp(name, hdr[i].name)) { return hdr[i].value; } } return NULL; } #if defined(USE_WEBSOCKET) /* Retrieve requested HTTP header multiple values, and return the number of * found occurrences */ static int get_req_headers(const struct mg_request_info *ri, const char *name, const char **output, int output_max_size) { int i; int cnt = 0; if (ri) { for (i = 0; i < ri->num_headers && cnt < output_max_size; i++) { if (!mg_strcasecmp(name, ri->http_headers[i].name)) { output[cnt++] = ri->http_headers[i].value; } } } return cnt; } #endif const char * mg_get_header(const struct mg_connection *conn, const char *name) { if (!conn) { return NULL; } if (conn->connection_type == CONNECTION_TYPE_REQUEST) { return get_header(conn->request_info.http_headers, conn->request_info.num_headers, name); } if (conn->connection_type == CONNECTION_TYPE_RESPONSE) { return get_header(conn->response_info.http_headers, conn->response_info.num_headers, name); } return NULL; } static const char * get_http_version(const struct mg_connection *conn) { if (!conn) { return NULL; } if (conn->connection_type == CONNECTION_TYPE_REQUEST) { return conn->request_info.http_version; } if (conn->connection_type == CONNECTION_TYPE_RESPONSE) { return conn->response_info.http_version; } return NULL; } /* A helper function for traversing a comma separated list of values. * It returns a list pointer shifted to the next value, or NULL if the end * of the list found. * Value is stored in val vector. If value has form "x=y", then eq_val * vector is initialized to point to the "y" part, and val vector length * is adjusted to point only to "x". */ static const char * next_option(const char *list, struct vec *val, struct vec *eq_val) { int end; reparse: if (val == NULL || list == NULL || *list == '\0') { /* End of the list */ return NULL; } /* Skip over leading LWS */ while (*list == ' ' || *list == '\t') list++; val->ptr = list; if ((list = strchr(val->ptr, ',')) != NULL) { /* Comma found. Store length and shift the list ptr */ val->len = ((size_t)(list - val->ptr)); list++; } else { /* This value is the last one */ list = val->ptr + strlen(val->ptr); val->len = ((size_t)(list - val->ptr)); } /* Adjust length for trailing LWS */ end = (int)val->len - 1; while (end >= 0 && ((val->ptr[end] == ' ') || (val->ptr[end] == '\t'))) end--; val->len = (size_t)(end) + (size_t)(1); if (val->len == 0) { /* Ignore any empty entries. */ goto reparse; } if (eq_val != NULL) { /* Value has form "x=y", adjust pointers and lengths * so that val points to "x", and eq_val points to "y". */ eq_val->len = 0; eq_val->ptr = (const char *)memchr(val->ptr, '=', val->len); if (eq_val->ptr != NULL) { eq_val->ptr++; /* Skip over '=' character */ eq_val->len = ((size_t)(val->ptr - eq_val->ptr)) + val->len; val->len = ((size_t)(eq_val->ptr - val->ptr)) - 1; } } return list; } /* A helper function for checking if a comma separated list of values * contains * the given option (case insensitvely). * 'header' can be NULL, in which case false is returned. */ static int header_has_option(const char *header, const char *option) { struct vec opt_vec; struct vec eq_vec; DEBUG_ASSERT(option != NULL); DEBUG_ASSERT(option[0] != '\0'); while ((header = next_option(header, &opt_vec, &eq_vec)) != NULL) { if (mg_strncasecmp(option, opt_vec.ptr, opt_vec.len) == 0) return 1; } return 0; } /* Perform case-insensitive match of string against pattern */ static ptrdiff_t match_prefix(const char *pattern, size_t pattern_len, const char *str) { const char *or_str; ptrdiff_t i, j, len, res; if ((or_str = (const char *)memchr(pattern, '|', pattern_len)) != NULL) { res = match_prefix(pattern, (size_t)(or_str - pattern), str); return (res > 0) ? res : match_prefix(or_str + 1, (size_t)((pattern + pattern_len) - (or_str + 1)), str); } for (i = 0, j = 0; (i < (ptrdiff_t)pattern_len); i++, j++) { if ((pattern[i] == '?') && (str[j] != '\0')) { continue; } else if (pattern[i] == '$') { return (str[j] == '\0') ? j : -1; } else if (pattern[i] == '*') { i++; if (pattern[i] == '*') { i++; len = strlen(str + j); } else { len = strcspn(str + j, "/"); } if (i == (ptrdiff_t)pattern_len) { return j + len; } do { res = match_prefix(pattern + i, pattern_len - i, str + j + len); } while (res == -1 && len-- > 0); return (res == -1) ? -1 : j + res + len; } else if (lowercase(&pattern[i]) != lowercase(&str[j])) { return -1; } } return (ptrdiff_t)j; } /* HTTP 1.1 assumes keep alive if "Connection:" header is not set * This function must tolerate situations when connection info is not * set up, for example if request parsing failed. */ static int should_keep_alive(const struct mg_connection *conn) { const char *http_version; const char *header; /* First satisfy needs of the server */ if ((conn == NULL) || conn->must_close) { /* Close, if civetweb framework needs to close */ return 0; } if (mg_strcasecmp(conn->dom_ctx->config[ENABLE_KEEP_ALIVE], "yes") != 0) { /* Close, if keep alive is not enabled */ return 0; } /* Check explicit wish of the client */ header = mg_get_header(conn, "Connection"); if (header) { /* If there is a connection header from the client, obey */ if (header_has_option(header, "keep-alive")) { return 1; } return 0; } /* Use default of the standard */ http_version = get_http_version(conn); if (http_version && (0 == strcmp(http_version, "1.1"))) { /* HTTP 1.1 default is keep alive */ return 1; } /* HTTP 1.0 (and earlier) default is to close the connection */ return 0; } static int should_decode_url(const struct mg_connection *conn) { if (!conn || !conn->dom_ctx) { return 0; } return (mg_strcasecmp(conn->dom_ctx->config[DECODE_URL], "yes") == 0); } static const char * suggest_connection_header(const struct mg_connection *conn) { return should_keep_alive(conn) ? "keep-alive" : "close"; } static int send_no_cache_header(struct mg_connection *conn) { /* Send all current and obsolete cache opt-out directives. */ return mg_printf(conn, "Cache-Control: no-cache, no-store, " "must-revalidate, private, max-age=0\r\n" "Pragma: no-cache\r\n" "Expires: 0\r\n"); } static int send_static_cache_header(struct mg_connection *conn) { #if !defined(NO_CACHING) int max_age; const char *cache_control = conn->dom_ctx->config[STATIC_FILE_CACHE_CONTROL]; if (cache_control != NULL) { return mg_printf(conn, "Cache-Control: %s\r\n", cache_control); } /* Read the server config to check how long a file may be cached. * The configuration is in seconds. */ max_age = atoi(conn->dom_ctx->config[STATIC_FILE_MAX_AGE]); if (max_age <= 0) { /* 0 means "do not cache". All values <0 are reserved * and may be used differently in the future. */ /* If a file should not be cached, do not only send * max-age=0, but also pragmas and Expires headers. */ return send_no_cache_header(conn); } /* Use "Cache-Control: max-age" instead of "Expires" header. * Reason: see https://www.mnot.net/blog/2007/05/15/expires_max-age */ /* See also https://www.mnot.net/cache_docs/ */ /* According to RFC 2616, Section 14.21, caching times should not exceed * one year. A year with 365 days corresponds to 31536000 seconds, a * leap * year to 31622400 seconds. For the moment, we just send whatever has * been configured, still the behavior for >1 year should be considered * as undefined. */ return mg_printf(conn, "Cache-Control: max-age=%u\r\n", (unsigned)max_age); #else /* NO_CACHING */ return send_no_cache_header(conn); #endif /* !NO_CACHING */ } static int send_additional_header(struct mg_connection *conn) { int i = 0; const char *header = conn->dom_ctx->config[ADDITIONAL_HEADER]; #if !defined(NO_SSL) if (conn->dom_ctx->config[STRICT_HTTPS_MAX_AGE]) { int max_age = atoi(conn->dom_ctx->config[STRICT_HTTPS_MAX_AGE]); if (max_age >= 0) { i += mg_printf(conn, "Strict-Transport-Security: max-age=%u\r\n", (unsigned)max_age); } } #endif if (header && header[0]) { i += mg_printf(conn, "%s\r\n", header); } return i; } #if !defined(NO_FILESYSTEMS) static void handle_file_based_request(struct mg_connection *conn, const char *path, struct mg_file *filep); #endif /* NO_FILESYSTEMS */ const char * mg_get_response_code_text(const struct mg_connection *conn, int response_code) { /* See IANA HTTP status code assignment: * http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml */ switch (response_code) { /* RFC2616 Section 10.1 - Informational 1xx */ case 100: return "Continue"; /* RFC2616 Section 10.1.1 */ case 101: return "Switching Protocols"; /* RFC2616 Section 10.1.2 */ case 102: return "Processing"; /* RFC2518 Section 10.1 */ /* RFC2616 Section 10.2 - Successful 2xx */ case 200: return "OK"; /* RFC2616 Section 10.2.1 */ case 201: return "Created"; /* RFC2616 Section 10.2.2 */ case 202: return "Accepted"; /* RFC2616 Section 10.2.3 */ case 203: return "Non-Authoritative Information"; /* RFC2616 Section 10.2.4 */ case 204: return "No Content"; /* RFC2616 Section 10.2.5 */ case 205: return "Reset Content"; /* RFC2616 Section 10.2.6 */ case 206: return "Partial Content"; /* RFC2616 Section 10.2.7 */ case 207: return "Multi-Status"; /* RFC2518 Section 10.2, RFC4918 Section 11.1 */ case 208: return "Already Reported"; /* RFC5842 Section 7.1 */ case 226: return "IM used"; /* RFC3229 Section 10.4.1 */ /* RFC2616 Section 10.3 - Redirection 3xx */ case 300: return "Multiple Choices"; /* RFC2616 Section 10.3.1 */ case 301: return "Moved Permanently"; /* RFC2616 Section 10.3.2 */ case 302: return "Found"; /* RFC2616 Section 10.3.3 */ case 303: return "See Other"; /* RFC2616 Section 10.3.4 */ case 304: return "Not Modified"; /* RFC2616 Section 10.3.5 */ case 305: return "Use Proxy"; /* RFC2616 Section 10.3.6 */ case 307: return "Temporary Redirect"; /* RFC2616 Section 10.3.8 */ case 308: return "Permanent Redirect"; /* RFC7238 Section 3 */ /* RFC2616 Section 10.4 - Client Error 4xx */ case 400: return "Bad Request"; /* RFC2616 Section 10.4.1 */ case 401: return "Unauthorized"; /* RFC2616 Section 10.4.2 */ case 402: return "Payment Required"; /* RFC2616 Section 10.4.3 */ case 403: return "Forbidden"; /* RFC2616 Section 10.4.4 */ case 404: return "Not Found"; /* RFC2616 Section 10.4.5 */ case 405: return "Method Not Allowed"; /* RFC2616 Section 10.4.6 */ case 406: return "Not Acceptable"; /* RFC2616 Section 10.4.7 */ case 407: return "Proxy Authentication Required"; /* RFC2616 Section 10.4.8 */ case 408: return "Request Time-out"; /* RFC2616 Section 10.4.9 */ case 409: return "Conflict"; /* RFC2616 Section 10.4.10 */ case 410: return "Gone"; /* RFC2616 Section 10.4.11 */ case 411: return "Length Required"; /* RFC2616 Section 10.4.12 */ case 412: return "Precondition Failed"; /* RFC2616 Section 10.4.13 */ case 413: return "Request Entity Too Large"; /* RFC2616 Section 10.4.14 */ case 414: return "Request-URI Too Large"; /* RFC2616 Section 10.4.15 */ case 415: return "Unsupported Media Type"; /* RFC2616 Section 10.4.16 */ case 416: return "Requested range not satisfiable"; /* RFC2616 Section 10.4.17 */ case 417: return "Expectation Failed"; /* RFC2616 Section 10.4.18 */ case 421: return "Misdirected Request"; /* RFC7540 Section 9.1.2 */ case 422: return "Unproccessable entity"; /* RFC2518 Section 10.3, RFC4918 * Section 11.2 */ case 423: return "Locked"; /* RFC2518 Section 10.4, RFC4918 Section 11.3 */ case 424: return "Failed Dependency"; /* RFC2518 Section 10.5, RFC4918 * Section 11.4 */ case 426: return "Upgrade Required"; /* RFC 2817 Section 4 */ case 428: return "Precondition Required"; /* RFC 6585, Section 3 */ case 429: return "Too Many Requests"; /* RFC 6585, Section 4 */ case 431: return "Request Header Fields Too Large"; /* RFC 6585, Section 5 */ case 451: return "Unavailable For Legal Reasons"; /* draft-tbray-http-legally-restricted-status-05, * Section 3 */ /* RFC2616 Section 10.5 - Server Error 5xx */ case 500: return "Internal Server Error"; /* RFC2616 Section 10.5.1 */ case 501: return "Not Implemented"; /* RFC2616 Section 10.5.2 */ case 502: return "Bad Gateway"; /* RFC2616 Section 10.5.3 */ case 503: return "Service Unavailable"; /* RFC2616 Section 10.5.4 */ case 504: return "Gateway Time-out"; /* RFC2616 Section 10.5.5 */ case 505: return "HTTP Version not supported"; /* RFC2616 Section 10.5.6 */ case 506: return "Variant Also Negotiates"; /* RFC 2295, Section 8.1 */ case 507: return "Insufficient Storage"; /* RFC2518 Section 10.6, RFC4918 * Section 11.5 */ case 508: return "Loop Detected"; /* RFC5842 Section 7.1 */ case 510: return "Not Extended"; /* RFC 2774, Section 7 */ case 511: return "Network Authentication Required"; /* RFC 6585, Section 6 */ /* Other status codes, not shown in the IANA HTTP status code * assignment. * E.g., "de facto" standards due to common use, ... */ case 418: return "I am a teapot"; /* RFC2324 Section 2.3.2 */ case 419: return "Authentication Timeout"; /* common use */ case 420: return "Enhance Your Calm"; /* common use */ case 440: return "Login Timeout"; /* common use */ case 509: return "Bandwidth Limit Exceeded"; /* common use */ default: /* This error code is unknown. This should not happen. */ if (conn) { mg_cry_internal(conn, "Unknown HTTP response code: %u", response_code); } /* Return at least a category according to RFC 2616 Section 10. */ if (response_code >= 100 && response_code < 200) { /* Unknown informational status code */ return "Information"; } if (response_code >= 200 && response_code < 300) { /* Unknown success code */ return "Success"; } if (response_code >= 300 && response_code < 400) { /* Unknown redirection code */ return "Redirection"; } if (response_code >= 400 && response_code < 500) { /* Unknown request error code */ return "Client Error"; } if (response_code >= 500 && response_code < 600) { /* Unknown server error code */ return "Server Error"; } /* Response code not even within reasonable range */ return ""; } } static int mg_send_http_error_impl(struct mg_connection *conn, int status, const char *fmt, va_list args) { char errmsg_buf[MG_BUF_LEN]; va_list ap; int has_body; char date[64]; time_t curtime = time(NULL); #if !defined(NO_FILESYSTEMS) char path_buf[PATH_MAX]; int len, i, page_handler_found, scope, truncated; const char *error_handler = NULL; struct mg_file error_page_file = STRUCT_FILE_INITIALIZER; const char *error_page_file_ext, *tstr; #endif /* NO_FILESYSTEMS */ int handled_by_callback = 0; const char *status_text = mg_get_response_code_text(conn, status); if ((conn == NULL) || (fmt == NULL)) { return -2; } /* Set status (for log) */ conn->status_code = status; /* Errors 1xx, 204 and 304 MUST NOT send a body */ has_body = ((status > 199) && (status != 204) && (status != 304)); /* Prepare message in buf, if required */ if (has_body || (!conn->in_error_handler && (conn->phys_ctx->callbacks.http_error != NULL))) { /* Store error message in errmsg_buf */ va_copy(ap, args); mg_vsnprintf(conn, NULL, errmsg_buf, sizeof(errmsg_buf), fmt, ap); va_end(ap); /* In a debug build, print all html errors */ DEBUG_TRACE("Error %i - [%s]", status, errmsg_buf); } /* If there is a http_error callback, call it. * But don't do it recursively, if callback calls mg_send_http_error again. */ if (!conn->in_error_handler && (conn->phys_ctx->callbacks.http_error != NULL)) { /* Mark in_error_handler to avoid recursion and call user callback. */ conn->in_error_handler = 1; handled_by_callback = (conn->phys_ctx->callbacks.http_error(conn, status, errmsg_buf) == 0); conn->in_error_handler = 0; } if (!handled_by_callback) { /* Check for recursion */ if (conn->in_error_handler) { DEBUG_TRACE( "Recursion when handling error %u - fall back to default", status); #if !defined(NO_FILESYSTEMS) } else { /* Send user defined error pages, if defined */ error_handler = conn->dom_ctx->config[ERROR_PAGES]; error_page_file_ext = conn->dom_ctx->config[INDEX_FILES]; page_handler_found = 0; if (error_handler != NULL) { for (scope = 1; (scope <= 3) && !page_handler_found; scope++) { switch (scope) { case 1: /* Handler for specific error, e.g. 404 error */ mg_snprintf(conn, &truncated, path_buf, sizeof(path_buf) - 32, "%serror%03u.", error_handler, status); break; case 2: /* Handler for error group, e.g., 5xx error * handler * for all server errors (500-599) */ mg_snprintf(conn, &truncated, path_buf, sizeof(path_buf) - 32, "%serror%01uxx.", error_handler, status / 100); break; default: /* Handler for all errors */ mg_snprintf(conn, &truncated, path_buf, sizeof(path_buf) - 32, "%serror.", error_handler); break; } /* String truncation in buf may only occur if * error_handler is too long. This string is * from the config, not from a client. */ (void)truncated; len = (int)strlen(path_buf); tstr = strchr(error_page_file_ext, '.'); while (tstr) { for (i = 1; (i < 32) && (tstr[i] != 0) && (tstr[i] != ','); i++) { /* buffer overrun is not possible here, since * (i < 32) && (len < sizeof(path_buf) - 32) * ==> (i + len) < sizeof(path_buf) */ path_buf[len + i - 1] = tstr[i]; } /* buffer overrun is not possible here, since * (i <= 32) && (len < sizeof(path_buf) - 32) * ==> (i + len) <= sizeof(path_buf) */ path_buf[len + i - 1] = 0; if (mg_stat(conn, path_buf, &error_page_file.stat)) { DEBUG_TRACE("Check error page %s - found", path_buf); page_handler_found = 1; break; } DEBUG_TRACE("Check error page %s - not found", path_buf); tstr = strchr(tstr + i, '.'); } } } if (page_handler_found) { conn->in_error_handler = 1; handle_file_based_request(conn, path_buf, &error_page_file); conn->in_error_handler = 0; return 0; } #endif /* NO_FILESYSTEMS */ } /* No custom error page. Send default error page. */ gmt_time_string(date, sizeof(date), &curtime); conn->must_close = 1; mg_printf(conn, "HTTP/1.1 %d %s\r\n", status, status_text); send_no_cache_header(conn); send_additional_header(conn); if (has_body) { mg_printf(conn, "%s", "Content-Type: text/plain; charset=utf-8\r\n"); } mg_printf(conn, "Date: %s\r\n" "Connection: close\r\n\r\n", date); /* HTTP responses 1xx, 204 and 304 MUST NOT send a body */ if (has_body) { /* For other errors, send a generic error message. */ mg_printf(conn, "Error %d: %s\n", status, status_text); mg_write(conn, errmsg_buf, strlen(errmsg_buf)); } else { /* No body allowed. Close the connection. */ DEBUG_TRACE("Error %i", status); } } return 0; } int mg_send_http_error(struct mg_connection *conn, int status, const char *fmt, ...) { va_list ap; int ret; va_start(ap, fmt); ret = mg_send_http_error_impl(conn, status, fmt, ap); va_end(ap); return ret; } int mg_send_http_ok(struct mg_connection *conn, const char *mime_type, long long content_length) { char date[64]; time_t curtime = time(NULL); if ((mime_type == NULL) || (*mime_type == 0)) { /* No content type defined: default to text/html */ mime_type = "text/html"; } gmt_time_string(date, sizeof(date), &curtime); mg_printf(conn, "HTTP/1.1 200 OK\r\n" "Content-Type: %s\r\n" "Date: %s\r\n" "Connection: %s\r\n", mime_type, date, suggest_connection_header(conn)); send_no_cache_header(conn); send_additional_header(conn); if (content_length < 0) { mg_printf(conn, "Transfer-Encoding: chunked\r\n\r\n"); } else { mg_printf(conn, "Content-Length: %" UINT64_FMT "\r\n\r\n", (uint64_t)content_length); } return 0; } int mg_send_http_redirect(struct mg_connection *conn, const char *target_url, int redirect_code) { /* Send a 30x redirect response. * * Redirect types (status codes): * * Status | Perm/Temp | Method | Version * 301 | permanent | POST->GET undefined | HTTP/1.0 * 302 | temporary | POST->GET undefined | HTTP/1.0 * 303 | temporary | always use GET | HTTP/1.1 * 307 | temporary | always keep method | HTTP/1.1 * 308 | permanent | always keep method | HTTP/1.1 */ const char *redirect_text; int ret; size_t content_len = 0; char reply[MG_BUF_LEN]; /* In case redirect_code=0, use 307. */ if (redirect_code == 0) { redirect_code = 307; } /* In case redirect_code is none of the above, return error. */ if ((redirect_code != 301) && (redirect_code != 302) && (redirect_code != 303) && (redirect_code != 307) && (redirect_code != 308)) { /* Parameter error */ return -2; } /* Get proper text for response code */ redirect_text = mg_get_response_code_text(conn, redirect_code); /* If target_url is not defined, redirect to "/". */ if ((target_url == NULL) || (*target_url == 0)) { target_url = "/"; } #if defined(MG_SEND_REDIRECT_BODY) /* TODO: condition name? */ /* Prepare a response body with a hyperlink. * * According to RFC2616 (and RFC1945 before): * Unless the request method was HEAD, the entity of the * response SHOULD contain a short hypertext note with a hyperlink to * the new URI(s). * * However, this response body is not useful in M2M communication. * Probably the original reason in the RFC was, clients not supporting * a 30x HTTP redirect could still show the HTML page and let the user * press the link. Since current browsers support 30x HTTP, the additional * HTML body does not seem to make sense anymore. * * The new RFC7231 (Section 6.4) does no longer recommend it ("SHOULD"), * but it only notes: * The server's response payload usually contains a short * hypertext note with a hyperlink to the new URI(s). * * Deactivated by default. If you need the 30x body, set the define. */ mg_snprintf( conn, NULL /* ignore truncation */, reply, sizeof(reply), "%s%s", redirect_text, target_url, target_url); content_len = strlen(reply); #else reply[0] = 0; #endif /* Do not send any additional header. For all other options, * including caching, there are suitable defaults. */ ret = mg_printf(conn, "HTTP/1.1 %i %s\r\n" "Location: %s\r\n" "Content-Length: %u\r\n" "Connection: %s\r\n\r\n", redirect_code, redirect_text, target_url, (unsigned int)content_len, suggest_connection_header(conn)); /* Send response body */ if (ret > 0) { /* ... unless it is a HEAD request */ if (0 != strcmp(conn->request_info.request_method, "HEAD")) { ret = mg_write(conn, reply, content_len); } } return (ret > 0) ? ret : -1; } #if defined(_WIN32) /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif static int pthread_mutex_init(pthread_mutex_t *mutex, void *unused) { (void)unused; /* Always initialize as PTHREAD_MUTEX_RECURSIVE */ InitializeCriticalSection(&mutex->sec); return 0; } static int pthread_mutex_destroy(pthread_mutex_t *mutex) { DeleteCriticalSection(&mutex->sec); return 0; } static int pthread_mutex_lock(pthread_mutex_t *mutex) { EnterCriticalSection(&mutex->sec); return 0; } static int pthread_mutex_unlock(pthread_mutex_t *mutex) { LeaveCriticalSection(&mutex->sec); return 0; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_init(pthread_cond_t *cv, const void *unused) { (void)unused; (void)pthread_mutex_init(&cv->threadIdSec, &pthread_mutex_attr); cv->waiting_thread = NULL; return 0; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *mutex, FUNCTION_MAY_BE_UNUSED const struct timespec *abstime) { struct mg_workerTLS **ptls, *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); int ok; int64_t nsnow, nswaitabs, nswaitrel; DWORD mswaitrel; pthread_mutex_lock(&cv->threadIdSec); /* Add this thread to cv's waiting list */ ptls = &cv->waiting_thread; for (; *ptls != NULL; ptls = &(*ptls)->next_waiting_thread) ; tls->next_waiting_thread = NULL; *ptls = tls; pthread_mutex_unlock(&cv->threadIdSec); if (abstime) { nsnow = mg_get_current_time_ns(); nswaitabs = (((int64_t)abstime->tv_sec) * 1000000000) + abstime->tv_nsec; nswaitrel = nswaitabs - nsnow; if (nswaitrel < 0) { nswaitrel = 0; } mswaitrel = (DWORD)(nswaitrel / 1000000); } else { mswaitrel = (DWORD)INFINITE; } pthread_mutex_unlock(mutex); ok = (WAIT_OBJECT_0 == WaitForSingleObject(tls->pthread_cond_helper_mutex, mswaitrel)); if (!ok) { ok = 1; pthread_mutex_lock(&cv->threadIdSec); ptls = &cv->waiting_thread; for (; *ptls != NULL; ptls = &(*ptls)->next_waiting_thread) { if (*ptls == tls) { *ptls = tls->next_waiting_thread; ok = 0; break; } } pthread_mutex_unlock(&cv->threadIdSec); if (ok) { WaitForSingleObject(tls->pthread_cond_helper_mutex, (DWORD)INFINITE); } } /* This thread has been removed from cv's waiting list */ pthread_mutex_lock(mutex); return ok ? 0 : -1; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex) { return pthread_cond_timedwait(cv, mutex, NULL); } FUNCTION_MAY_BE_UNUSED static int pthread_cond_signal(pthread_cond_t *cv) { HANDLE wkup = NULL; BOOL ok = FALSE; pthread_mutex_lock(&cv->threadIdSec); if (cv->waiting_thread) { wkup = cv->waiting_thread->pthread_cond_helper_mutex; cv->waiting_thread = cv->waiting_thread->next_waiting_thread; ok = SetEvent(wkup); DEBUG_ASSERT(ok); } pthread_mutex_unlock(&cv->threadIdSec); return ok ? 0 : 1; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_broadcast(pthread_cond_t *cv) { pthread_mutex_lock(&cv->threadIdSec); while (cv->waiting_thread) { pthread_cond_signal(cv); } pthread_mutex_unlock(&cv->threadIdSec); return 0; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_destroy(pthread_cond_t *cv) { pthread_mutex_lock(&cv->threadIdSec); DEBUG_ASSERT(cv->waiting_thread == NULL); pthread_mutex_unlock(&cv->threadIdSec); pthread_mutex_destroy(&cv->threadIdSec); return 0; } #if defined(ALTERNATIVE_QUEUE) FUNCTION_MAY_BE_UNUSED static void * event_create(void) { return (void *)CreateEvent(NULL, FALSE, FALSE, NULL); } FUNCTION_MAY_BE_UNUSED static int event_wait(void *eventhdl) { int res = WaitForSingleObject((HANDLE)eventhdl, (DWORD)INFINITE); return (res == WAIT_OBJECT_0); } FUNCTION_MAY_BE_UNUSED static int event_signal(void *eventhdl) { return (int)SetEvent((HANDLE)eventhdl); } FUNCTION_MAY_BE_UNUSED static void event_destroy(void *eventhdl) { CloseHandle((HANDLE)eventhdl); } #endif #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif /* For Windows, change all slashes to backslashes in path names. */ static void change_slashes_to_backslashes(char *path) { int i; for (i = 0; path[i] != '\0'; i++) { if (path[i] == '/') { path[i] = '\\'; } /* remove double backslash (check i > 0 to preserve UNC paths, * like \\server\file.txt) */ if ((path[i] == '\\') && (i > 0)) { while ((path[i + 1] == '\\') || (path[i + 1] == '/')) { (void)memmove(path + i + 1, path + i + 2, strlen(path + i + 1)); } } } } static int mg_wcscasecmp(const wchar_t *s1, const wchar_t *s2) { int diff; do { diff = ((*s1 >= L'A') && (*s1 <= L'Z') ? (*s1 - L'A' + L'a') : *s1) - ((*s2 >= L'A') && (*s2 <= L'Z') ? (*s2 - L'A' + L'a') : *s2); s1++; s2++; } while ((diff == 0) && (s1[-1] != L'\0')); return diff; } /* Encode 'path' which is assumed UTF-8 string, into UNICODE string. * wbuf and wbuf_len is a target buffer and its length. */ static void path_to_unicode(const struct mg_connection *conn, const char *path, wchar_t *wbuf, size_t wbuf_len) { char buf[PATH_MAX], buf2[PATH_MAX]; wchar_t wbuf2[W_PATH_MAX + 1]; DWORD long_len, err; int (*fcompare)(const wchar_t *, const wchar_t *) = mg_wcscasecmp; mg_strlcpy(buf, path, sizeof(buf)); change_slashes_to_backslashes(buf); /* Convert to Unicode and back. If doubly-converted string does not * match the original, something is fishy, reject. */ memset(wbuf, 0, wbuf_len * sizeof(wchar_t)); MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, (int)wbuf_len); WideCharToMultiByte( CP_UTF8, 0, wbuf, (int)wbuf_len, buf2, sizeof(buf2), NULL, NULL); if (strcmp(buf, buf2) != 0) { wbuf[0] = L'\0'; } /* Windows file systems are not case sensitive, but you can still use * uppercase and lowercase letters (on all modern file systems). * The server can check if the URI uses the same upper/lowercase * letters an the file system, effectively making Windows servers * case sensitive (like Linux servers are). It is still not possible * to use two files with the same name in different cases on Windows * (like /a and /A) - this would be possible in Linux. * As a default, Windows is not case sensitive, but the case sensitive * file name check can be activated by an additional configuration. */ if (conn) { if (conn->dom_ctx->config[CASE_SENSITIVE_FILES] && !mg_strcasecmp(conn->dom_ctx->config[CASE_SENSITIVE_FILES], "yes")) { /* Use case sensitive compare function */ fcompare = wcscmp; } } (void)conn; /* conn is currently unused */ #if !defined(_WIN32_WCE) /* Only accept a full file path, not a Windows short (8.3) path. */ memset(wbuf2, 0, ARRAY_SIZE(wbuf2) * sizeof(wchar_t)); long_len = GetLongPathNameW(wbuf, wbuf2, ARRAY_SIZE(wbuf2) - 1); if (long_len == 0) { err = GetLastError(); if (err == ERROR_FILE_NOT_FOUND) { /* File does not exist. This is not always a problem here. */ return; } } if ((long_len >= ARRAY_SIZE(wbuf2)) || (fcompare(wbuf, wbuf2) != 0)) { /* Short name is used. */ wbuf[0] = L'\0'; } #else (void)long_len; (void)wbuf2; (void)err; if (strchr(path, '~')) { wbuf[0] = L'\0'; } #endif } #if !defined(NO_FILESYSTEMS) static int mg_stat(const struct mg_connection *conn, const char *path, struct mg_file_stat *filep) { wchar_t wbuf[W_PATH_MAX]; WIN32_FILE_ATTRIBUTE_DATA info; time_t creation_time; size_t len; if (!filep) { return 0; } memset(filep, 0, sizeof(*filep)); if (conn && is_file_in_memory(conn, path)) { /* filep->is_directory = 0; filep->gzipped = 0; .. already done by * memset */ /* Quick fix (for 1.9.x): */ /* mg_stat must fill all fields, also for files in memory */ struct mg_file tmp_file = STRUCT_FILE_INITIALIZER; open_file_in_memory(conn, path, &tmp_file, MG_FOPEN_MODE_NONE); filep->size = tmp_file.stat.size; filep->location = 2; /* TODO: for 1.10: restructure how files in memory are handled */ /* The "file in memory" feature is a candidate for deletion. * Please join the discussion at * https://groups.google.com/forum/#!topic/civetweb/h9HT4CmeYqI */ filep->last_modified = time(NULL); /* TODO */ /* last_modified = now ... assumes the file may change during * runtime, * so every mg_fopen call may return different data */ /* last_modified = conn->phys_ctx.start_time; * May be used it the data does not change during runtime. This * allows * browser caching. Since we do not know, we have to assume the file * in memory may change. */ return 1; } path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); /* Windows happily opens files with some garbage at the end of file name. * For example, fopen("a.cgi ", "r") on Windows successfully opens * "a.cgi", despite one would expect an error back. */ len = strlen(path); if ((len > 0) && (path[len - 1] != ' ') && (path[len - 1] != '.') && (GetFileAttributesExW(wbuf, GetFileExInfoStandard, &info) != 0)) { filep->size = MAKEUQUAD(info.nFileSizeLow, info.nFileSizeHigh); filep->last_modified = SYS2UNIX_TIME(info.ftLastWriteTime.dwLowDateTime, info.ftLastWriteTime.dwHighDateTime); /* On Windows, the file creation time can be higher than the * modification time, e.g. when a file is copied. * Since the Last-Modified timestamp is used for caching * it should be based on the most recent timestamp. */ creation_time = SYS2UNIX_TIME(info.ftCreationTime.dwLowDateTime, info.ftCreationTime.dwHighDateTime); if (creation_time > filep->last_modified) { filep->last_modified = creation_time; } filep->is_directory = info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; return 1; } return 0; } #endif static int mg_remove(const struct mg_connection *conn, const char *path) { wchar_t wbuf[W_PATH_MAX]; path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); return DeleteFileW(wbuf) ? 0 : -1; } static int mg_mkdir(const struct mg_connection *conn, const char *path, int mode) { wchar_t wbuf[W_PATH_MAX]; (void)mode; path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); return CreateDirectoryW(wbuf, NULL) ? 0 : -1; } /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif /* Implementation of POSIX opendir/closedir/readdir for Windows. */ FUNCTION_MAY_BE_UNUSED static DIR * mg_opendir(const struct mg_connection *conn, const char *name) { DIR *dir = NULL; wchar_t wpath[W_PATH_MAX]; DWORD attrs; if (name == NULL) { SetLastError(ERROR_BAD_ARGUMENTS); } else if ((dir = (DIR *)mg_malloc(sizeof(*dir))) == NULL) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); } else { path_to_unicode(conn, name, wpath, ARRAY_SIZE(wpath)); attrs = GetFileAttributesW(wpath); if ((wcslen(wpath) + 2 < ARRAY_SIZE(wpath)) && (attrs != 0xFFFFFFFF) && ((attrs & FILE_ATTRIBUTE_DIRECTORY) != 0)) { (void)wcscat(wpath, L"\\*"); dir->handle = FindFirstFileW(wpath, &dir->info); dir->result.d_name[0] = '\0'; } else { mg_free(dir); dir = NULL; } } return dir; } FUNCTION_MAY_BE_UNUSED static int mg_closedir(DIR *dir) { int result = 0; if (dir != NULL) { if (dir->handle != INVALID_HANDLE_VALUE) result = FindClose(dir->handle) ? 0 : -1; mg_free(dir); } else { result = -1; SetLastError(ERROR_BAD_ARGUMENTS); } return result; } FUNCTION_MAY_BE_UNUSED static struct dirent * mg_readdir(DIR *dir) { struct dirent *result = 0; if (dir) { if (dir->handle != INVALID_HANDLE_VALUE) { result = &dir->result; (void)WideCharToMultiByte(CP_UTF8, 0, dir->info.cFileName, -1, result->d_name, sizeof(result->d_name), NULL, NULL); if (!FindNextFileW(dir->handle, &dir->info)) { (void)FindClose(dir->handle); dir->handle = INVALID_HANDLE_VALUE; } } else { SetLastError(ERROR_FILE_NOT_FOUND); } } else { SetLastError(ERROR_BAD_ARGUMENTS); } return result; } #if !defined(HAVE_POLL) #undef POLLIN #undef POLLPRI #undef POLLOUT #define POLLIN (1) /* Data ready - read will not block. */ #define POLLPRI (2) /* Priority data ready. */ #define POLLOUT (4) /* Send queue not full - write will not block. */ FUNCTION_MAY_BE_UNUSED static int poll(struct mg_pollfd *pfd, unsigned int n, int milliseconds) { struct timeval tv; fd_set rset; fd_set wset; unsigned int i; int result; SOCKET maxfd = 0; memset(&tv, 0, sizeof(tv)); tv.tv_sec = milliseconds / 1000; tv.tv_usec = (milliseconds % 1000) * 1000; FD_ZERO(&rset); FD_ZERO(&wset); for (i = 0; i < n; i++) { if (pfd[i].events & POLLIN) { FD_SET(pfd[i].fd, &rset); } if (pfd[i].events & POLLOUT) { FD_SET(pfd[i].fd, &wset); } pfd[i].revents = 0; if (pfd[i].fd > maxfd) { maxfd = pfd[i].fd; } } if ((result = select((int)maxfd + 1, &rset, &wset, NULL, &tv)) > 0) { for (i = 0; i < n; i++) { if (FD_ISSET(pfd[i].fd, &rset)) { pfd[i].revents |= POLLIN; } if (FD_ISSET(pfd[i].fd, &wset)) { pfd[i].revents |= POLLOUT; } } } /* We should subtract the time used in select from remaining * "milliseconds", in particular if called from mg_poll with a * timeout quantum. * Unfortunately, the remaining time is not stored in "tv" in all * implementations, so the result in "tv" must be considered undefined. * See http://man7.org/linux/man-pages/man2/select.2.html */ return result; } #endif /* HAVE_POLL */ #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif static void set_close_on_exec(SOCKET sock, const struct mg_connection *conn /* may be null */, struct mg_context *ctx /* may be null */) { (void)conn; /* Unused. */ (void)ctx; #if defined(_WIN32_WCE) (void)sock; #else (void)SetHandleInformation((HANDLE)(intptr_t)sock, HANDLE_FLAG_INHERIT, 0); #endif } int mg_start_thread(mg_thread_func_t f, void *p) { #if defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) /* Compile-time option to control stack size, e.g. * -DUSE_STACK_SIZE=16384 */ return ((_beginthread((void(__cdecl *)(void *))f, USE_STACK_SIZE, p) == ((uintptr_t)(-1L))) ? -1 : 0); #else return ( (_beginthread((void(__cdecl *)(void *))f, 0, p) == ((uintptr_t)(-1L))) ? -1 : 0); #endif /* defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) */ } /* Start a thread storing the thread context. */ static int mg_start_thread_with_id(unsigned(__stdcall *f)(void *), void *p, pthread_t *threadidptr) { uintptr_t uip; HANDLE threadhandle; int result = -1; uip = _beginthreadex(NULL, 0, f, p, 0, NULL); threadhandle = (HANDLE)uip; if ((uip != 0) && (threadidptr != NULL)) { *threadidptr = threadhandle; result = 0; } return result; } /* Wait for a thread to finish. */ static int mg_join_thread(pthread_t threadid) { int result; DWORD dwevent; result = -1; dwevent = WaitForSingleObject(threadid, (DWORD)INFINITE); if (dwevent == WAIT_FAILED) { DEBUG_TRACE("WaitForSingleObject() failed, error %d", ERRNO); } else { if (dwevent == WAIT_OBJECT_0) { CloseHandle(threadid); result = 0; } } return result; } #if !defined(NO_SSL_DL) && !defined(NO_SSL) /* If SSL is loaded dynamically, dlopen/dlclose is required. */ /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif FUNCTION_MAY_BE_UNUSED static HANDLE dlopen(const char *dll_name, int flags) { wchar_t wbuf[W_PATH_MAX]; (void)flags; path_to_unicode(NULL, dll_name, wbuf, ARRAY_SIZE(wbuf)); return LoadLibraryW(wbuf); } FUNCTION_MAY_BE_UNUSED static int dlclose(void *handle) { int result; if (FreeLibrary((HMODULE)handle) != 0) { result = 0; } else { result = -1; } return result; } #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif #endif #if !defined(NO_CGI) #define SIGKILL (0) static int kill(pid_t pid, int sig_num) { (void)TerminateProcess((HANDLE)pid, (UINT)sig_num); (void)CloseHandle((HANDLE)pid); return 0; } #if !defined(WNOHANG) #define WNOHANG (1) #endif static pid_t waitpid(pid_t pid, int *status, int flags) { DWORD timeout = INFINITE; DWORD waitres; (void)status; /* Currently not used by any client here */ if ((flags | WNOHANG) == WNOHANG) { timeout = 0; } waitres = WaitForSingleObject((HANDLE)pid, timeout); if (waitres == WAIT_OBJECT_0) { return pid; } if (waitres == WAIT_TIMEOUT) { return 0; } return (pid_t)-1; } static void trim_trailing_whitespaces(char *s) { char *e = s + strlen(s); while ((e > s) && isspace((unsigned char)e[-1])) { *(--e) = '\0'; } } static pid_t spawn_process(struct mg_connection *conn, const char *prog, char *envblk, char *envp[], int fdin[2], int fdout[2], int fderr[2], const char *dir) { HANDLE me; char *p, *interp, full_interp[PATH_MAX], full_dir[PATH_MAX], cmdline[PATH_MAX], buf[PATH_MAX]; int truncated; struct mg_file file = STRUCT_FILE_INITIALIZER; STARTUPINFOA si; PROCESS_INFORMATION pi = {0}; (void)envp; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; me = GetCurrentProcess(); DuplicateHandle(me, (HANDLE)_get_osfhandle(fdin[0]), me, &si.hStdInput, 0, TRUE, DUPLICATE_SAME_ACCESS); DuplicateHandle(me, (HANDLE)_get_osfhandle(fdout[1]), me, &si.hStdOutput, 0, TRUE, DUPLICATE_SAME_ACCESS); DuplicateHandle(me, (HANDLE)_get_osfhandle(fderr[1]), me, &si.hStdError, 0, TRUE, DUPLICATE_SAME_ACCESS); /* Mark handles that should not be inherited. See * https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499%28v=vs.85%29.aspx */ SetHandleInformation((HANDLE)_get_osfhandle(fdin[1]), HANDLE_FLAG_INHERIT, 0); SetHandleInformation((HANDLE)_get_osfhandle(fdout[0]), HANDLE_FLAG_INHERIT, 0); SetHandleInformation((HANDLE)_get_osfhandle(fderr[0]), HANDLE_FLAG_INHERIT, 0); /* If CGI file is a script, try to read the interpreter line */ interp = conn->dom_ctx->config[CGI_INTERPRETER]; if (interp == NULL) { buf[0] = buf[1] = '\0'; /* Read the first line of the script into the buffer */ mg_snprintf( conn, &truncated, cmdline, sizeof(cmdline), "%s/%s", dir, prog); if (truncated) { pi.hProcess = (pid_t)-1; goto spawn_cleanup; } if (mg_fopen(conn, cmdline, MG_FOPEN_MODE_READ, &file)) { #if defined(MG_USE_OPEN_FILE) p = (char *)file.access.membuf; #else p = (char *)NULL; #endif mg_fgets(buf, sizeof(buf), &file, &p); (void)mg_fclose(&file.access); /* ignore error on read only file */ buf[sizeof(buf) - 1] = '\0'; } if ((buf[0] == '#') && (buf[1] == '!')) { trim_trailing_whitespaces(buf + 2); } else { buf[2] = '\0'; } interp = buf + 2; } if (interp[0] != '\0') { GetFullPathNameA(interp, sizeof(full_interp), full_interp, NULL); interp = full_interp; } GetFullPathNameA(dir, sizeof(full_dir), full_dir, NULL); if (interp[0] != '\0') { mg_snprintf(conn, &truncated, cmdline, sizeof(cmdline), "\"%s\" \"%s\\%s\"", interp, full_dir, prog); } else { mg_snprintf(conn, &truncated, cmdline, sizeof(cmdline), "\"%s\\%s\"", full_dir, prog); } if (truncated) { pi.hProcess = (pid_t)-1; goto spawn_cleanup; } DEBUG_TRACE("Running [%s]", cmdline); if (CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, CREATE_NEW_PROCESS_GROUP, envblk, NULL, &si, &pi) == 0) { mg_cry_internal( conn, "%s: CreateProcess(%s): %ld", __func__, cmdline, (long)ERRNO); pi.hProcess = (pid_t)-1; /* goto spawn_cleanup; */ } spawn_cleanup: (void)CloseHandle(si.hStdOutput); (void)CloseHandle(si.hStdError); (void)CloseHandle(si.hStdInput); if (pi.hThread != NULL) { (void)CloseHandle(pi.hThread); } return (pid_t)pi.hProcess; } #endif /* !NO_CGI */ static int set_blocking_mode(SOCKET sock) { unsigned long non_blocking = 0; return ioctlsocket(sock, (long)FIONBIO, &non_blocking); } static int set_non_blocking_mode(SOCKET sock) { unsigned long non_blocking = 1; return ioctlsocket(sock, (long)FIONBIO, &non_blocking); } #else #if !defined(NO_FILESYSTEMS) static int mg_stat(const struct mg_connection *conn, const char *path, struct mg_file_stat *filep) { struct stat st; if (!filep) { return 0; } memset(filep, 0, sizeof(*filep)); if (conn && is_file_in_memory(conn, path)) { /* Quick fix (for 1.9.x): */ /* mg_stat must fill all fields, also for files in memory */ struct mg_file tmp_file = STRUCT_FILE_INITIALIZER; open_file_in_memory(conn, path, &tmp_file, MG_FOPEN_MODE_NONE); filep->size = tmp_file.stat.size; filep->last_modified = time(NULL); filep->location = 2; /* TODO: remove legacy "files in memory" feature */ return 1; } if (0 == stat(path, &st)) { filep->size = (uint64_t)(st.st_size); filep->last_modified = st.st_mtime; filep->is_directory = S_ISDIR(st.st_mode); return 1; } return 0; } #endif /* NO_FILESYSTEMS */ static void set_close_on_exec(int fd, const struct mg_connection *conn /* may be null */, struct mg_context *ctx /* may be null */) { #if defined(__ZEPHYR__) (void)fd; (void)conn; (void)ctx; #else if (fcntl(fd, F_SETFD, FD_CLOEXEC) != 0) { if (conn || ctx) { struct mg_connection fc; mg_cry_internal((conn ? conn : fake_connection(&fc, ctx)), "%s: fcntl(F_SETFD FD_CLOEXEC) failed: %s", __func__, strerror(ERRNO)); } } #endif } int mg_start_thread(mg_thread_func_t func, void *param) { pthread_t thread_id; pthread_attr_t attr; int result; (void)pthread_attr_init(&attr); (void)pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); #if defined(__ZEPHYR__) pthread_attr_setstack(&attr, &civetweb_main_stack, ZEPHYR_STACK_SIZE); #elif defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) /* Compile-time option to control stack size, * e.g. -DUSE_STACK_SIZE=16384 */ (void)pthread_attr_setstacksize(&attr, USE_STACK_SIZE); #endif /* defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) */ result = pthread_create(&thread_id, &attr, func, param); pthread_attr_destroy(&attr); return result; } /* Start a thread storing the thread context. */ static int mg_start_thread_with_id(mg_thread_func_t func, void *param, pthread_t *threadidptr) { pthread_t thread_id; pthread_attr_t attr; int result; (void)pthread_attr_init(&attr); #if defined(__ZEPHYR__) pthread_attr_setstack(&attr, &civetweb_worker_stacks[zephyr_worker_stack_index++], ZEPHYR_STACK_SIZE); #elif defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) /* Compile-time option to control stack size, * e.g. -DUSE_STACK_SIZE=16384 */ (void)pthread_attr_setstacksize(&attr, USE_STACK_SIZE); #endif /* defined(USE_STACK_SIZE) && USE_STACK_SIZE > 1 */ result = pthread_create(&thread_id, &attr, func, param); pthread_attr_destroy(&attr); if ((result == 0) && (threadidptr != NULL)) { *threadidptr = thread_id; } return result; } /* Wait for a thread to finish. */ static int mg_join_thread(pthread_t threadid) { int result; result = pthread_join(threadid, NULL); return result; } #if !defined(NO_CGI) static pid_t spawn_process(struct mg_connection *conn, const char *prog, char *envblk, char *envp[], int fdin[2], int fdout[2], int fderr[2], const char *dir) { pid_t pid; const char *interp; (void)envblk; if ((pid = fork()) == -1) { /* Parent */ mg_cry_internal(conn, "%s: fork(): %s", __func__, strerror(ERRNO)); } else if (pid != 0) { /* Make sure children close parent-side descriptors. * The caller will close the child-side immediately. */ set_close_on_exec(fdin[1], conn, NULL); /* stdin write */ set_close_on_exec(fdout[0], conn, NULL); /* stdout read */ set_close_on_exec(fderr[0], conn, NULL); /* stderr read */ } else { /* Child */ if (chdir(dir) != 0) { mg_cry_internal( conn, "%s: chdir(%s): %s", __func__, dir, strerror(ERRNO)); } else if (dup2(fdin[0], 0) == -1) { mg_cry_internal(conn, "%s: dup2(%d, 0): %s", __func__, fdin[0], strerror(ERRNO)); } else if (dup2(fdout[1], 1) == -1) { mg_cry_internal(conn, "%s: dup2(%d, 1): %s", __func__, fdout[1], strerror(ERRNO)); } else if (dup2(fderr[1], 2) == -1) { mg_cry_internal(conn, "%s: dup2(%d, 2): %s", __func__, fderr[1], strerror(ERRNO)); } else { struct sigaction sa; /* Keep stderr and stdout in two different pipes. * Stdout will be sent back to the client, * stderr should go into a server error log. */ (void)close(fdin[0]); (void)close(fdout[1]); (void)close(fderr[1]); /* Close write end fdin and read end fdout and fderr */ (void)close(fdin[1]); (void)close(fdout[0]); (void)close(fderr[0]); /* After exec, all signal handlers are restored to their default * values, with one exception of SIGCHLD. According to * POSIX.1-2001 and Linux's implementation, SIGCHLD's handler * will leave unchanged after exec if it was set to be ignored. * Restore it to default action. */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGCHLD, &sa, NULL); interp = conn->dom_ctx->config[CGI_INTERPRETER]; if (interp == NULL) { (void)execle(prog, prog, NULL, envp); mg_cry_internal(conn, "%s: execle(%s): %s", __func__, prog, strerror(ERRNO)); } else { (void)execle(interp, interp, prog, NULL, envp); mg_cry_internal(conn, "%s: execle(%s %s): %s", __func__, interp, prog, strerror(ERRNO)); } } exit(EXIT_FAILURE); } return pid; } #endif /* !NO_CGI */ static int set_non_blocking_mode(SOCKET sock) { int flags = fcntl(sock, F_GETFL, 0); if (flags < 0) { return -1; } if (fcntl(sock, F_SETFL, (flags | O_NONBLOCK)) < 0) { return -1; } return 0; } static int set_blocking_mode(SOCKET sock) { int flags = fcntl(sock, F_GETFL, 0); if (flags < 0) { return -1; } if (fcntl(sock, F_SETFL, flags & (~(int)(O_NONBLOCK))) < 0) { return -1; } return 0; } #endif /* _WIN32 / else */ /* End of initial operating system specific define block. */ /* Get a random number (independent of C rand function) */ static uint64_t get_random(void) { static uint64_t lfsr = 0; /* Linear feedback shift register */ static uint64_t lcg = 0; /* Linear congruential generator */ uint64_t now = mg_get_current_time_ns(); if (lfsr == 0) { /* lfsr will be only 0 if has not been initialized, * so this code is called only once. */ lfsr = mg_get_current_time_ns(); lcg = mg_get_current_time_ns(); } else { /* Get the next step of both random number generators. */ lfsr = (lfsr >> 1) | ((((lfsr >> 0) ^ (lfsr >> 1) ^ (lfsr >> 3) ^ (lfsr >> 4)) & 1) << 63); lcg = lcg * 6364136223846793005LL + 1442695040888963407LL; } /* Combining two pseudo-random number generators and a high resolution * part * of the current server time will make it hard (impossible?) to guess * the * next number. */ return (lfsr ^ lcg ^ now); } static int mg_poll(struct mg_pollfd *pfd, unsigned int n, int milliseconds, volatile int *stop_server) { /* Call poll, but only for a maximum time of a few seconds. * This will allow to stop the server after some seconds, instead * of having to wait for a long socket timeout. */ int ms_now = SOCKET_TIMEOUT_QUANTUM; /* Sleep quantum in ms */ do { int result; if (*stop_server) { /* Shut down signal */ return -2; } if ((milliseconds >= 0) && (milliseconds < ms_now)) { ms_now = milliseconds; } result = poll(pfd, n, ms_now); if (result != 0) { /* Poll returned either success (1) or error (-1). * Forward both to the caller. */ return result; } /* Poll returned timeout (0). */ if (milliseconds > 0) { milliseconds -= ms_now; } } while (milliseconds != 0); /* timeout: return 0 */ return 0; } /* Write data to the IO channel - opened file descriptor, socket or SSL * descriptor. * Return value: * >=0 .. number of bytes successfully written * -1 .. timeout * -2 .. error */ static int push_inner(struct mg_context *ctx, FILE *fp, SOCKET sock, SSL *ssl, const char *buf, int len, double timeout) { uint64_t start = 0, now = 0, timeout_ns = 0; int n, err; unsigned ms_wait = SOCKET_TIMEOUT_QUANTUM; /* Sleep quantum in ms */ #if defined(_WIN32) typedef int len_t; #else typedef size_t len_t; #endif if (timeout > 0) { now = mg_get_current_time_ns(); start = now; timeout_ns = (uint64_t)(timeout * 1.0E9); } if (ctx == NULL) { return -2; } #if defined(NO_SSL) if (ssl) { return -2; } #endif /* Try to read until it succeeds, fails, times out, or the server * shuts down. */ for (;;) { #if !defined(NO_SSL) if (ssl != NULL) { n = SSL_write(ssl, buf, len); if (n <= 0) { err = SSL_get_error(ssl, n); if ((err == SSL_ERROR_SYSCALL) && (n == -1)) { err = ERRNO; } else if ((err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE)) { n = 0; } else { DEBUG_TRACE("SSL_write() failed, error %d", err); return -2; } } else { err = 0; } } else #endif if (fp != NULL) { n = (int)fwrite(buf, 1, (size_t)len, fp); if (ferror(fp)) { n = -1; err = ERRNO; } else { err = 0; } } else { n = (int)send(sock, buf, (len_t)len, MSG_NOSIGNAL); err = (n < 0) ? ERRNO : 0; #if defined(_WIN32) if (err == WSAEWOULDBLOCK) { err = 0; n = 0; } #else if (err == EWOULDBLOCK) { err = 0; n = 0; } #endif if (n < 0) { /* shutdown of the socket at client side */ return -2; } } if (ctx->stop_flag) { return -2; } if ((n > 0) || ((n == 0) && (len == 0))) { /* some data has been read, or no data was requested */ return n; } if (n < 0) { /* socket error - check errno */ DEBUG_TRACE("send() failed, error %d", err); /* TODO (mid): error handling depending on the error code. * These codes are different between Windows and Linux. * Currently there is no problem with failing send calls, * if there is a reproducible situation, it should be * investigated in detail. */ return -2; } /* Only in case n=0 (timeout), repeat calling the write function */ /* If send failed, wait before retry */ if (fp != NULL) { /* For files, just wait a fixed time. * Maybe it helps, maybe not. */ mg_sleep(5); } else { /* For sockets, wait for the socket using poll */ struct mg_pollfd pfd[1]; int pollres; pfd[0].fd = sock; pfd[0].events = POLLOUT; pollres = mg_poll(pfd, 1, (int)(ms_wait), &(ctx->stop_flag)); if (ctx->stop_flag) { return -2; } if (pollres > 0) { continue; } } if (timeout > 0) { now = mg_get_current_time_ns(); if ((now - start) > timeout_ns) { /* Timeout */ break; } } } (void)err; /* Avoid unused warning if NO_SSL is set and DEBUG_TRACE is not used */ return -1; } static int push_all(struct mg_context *ctx, FILE *fp, SOCKET sock, SSL *ssl, const char *buf, int len) { double timeout = -1.0; int n, nwritten = 0; if (ctx == NULL) { return -1; } if (ctx->dd.config[REQUEST_TIMEOUT]) { timeout = atoi(ctx->dd.config[REQUEST_TIMEOUT]) / 1000.0; } while ((len > 0) && (ctx->stop_flag == 0)) { n = push_inner(ctx, fp, sock, ssl, buf + nwritten, len, timeout); if (n < 0) { if (nwritten == 0) { nwritten = -1; /* Propagate the error */ } break; } else if (n == 0) { break; /* No more data to write */ } else { nwritten += n; len -= n; } } return nwritten; } /* Read from IO channel - opened file descriptor, socket, or SSL descriptor. * Return value: * >=0 .. number of bytes successfully read * -1 .. timeout * -2 .. error */ static int pull_inner(FILE *fp, struct mg_connection *conn, char *buf, int len, double timeout) { int nread, err = 0; #if defined(_WIN32) typedef int len_t; #else typedef size_t len_t; #endif #if !defined(NO_SSL) int ssl_pending; #endif /* We need an additional wait loop around this, because in some cases * with TLSwe may get data from the socket but not from SSL_read. * In this case we need to repeat at least once. */ if (fp != NULL) { #if !defined(_WIN32_WCE) /* Use read() instead of fread(), because if we're reading from the * CGI pipe, fread() may block until IO buffer is filled up. We * cannot afford to block and must pass all read bytes immediately * to the client. */ nread = (int)read(fileno(fp), buf, (size_t)len); #else /* WinCE does not support CGI pipes */ nread = (int)fread(buf, 1, (size_t)len, fp); #endif err = (nread < 0) ? ERRNO : 0; if ((nread == 0) && (len > 0)) { /* Should get data, but got EOL */ return -2; } #if !defined(NO_SSL) } else if ((conn->ssl != NULL) && ((ssl_pending = SSL_pending(conn->ssl)) > 0)) { /* We already know there is no more data buffered in conn->buf * but there is more available in the SSL layer. So don't poll * conn->client.sock yet. */ if (ssl_pending > len) { ssl_pending = len; } nread = SSL_read(conn->ssl, buf, ssl_pending); if (nread <= 0) { err = SSL_get_error(conn->ssl, nread); if ((err == SSL_ERROR_SYSCALL) && (nread == -1)) { err = ERRNO; } else if ((err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE)) { nread = 0; } else { /* All errors should return -2 */ DEBUG_TRACE("SSL_read() failed, error %d", err); return -2; } ERR_clear_error(); } else { err = 0; } } else if (conn->ssl != NULL) { struct mg_pollfd pfd[1]; int pollres; pfd[0].fd = conn->client.sock; pfd[0].events = POLLIN; pollres = mg_poll(pfd, 1, (int)(timeout * 1000.0), &(conn->phys_ctx->stop_flag)); if (conn->phys_ctx->stop_flag) { return -2; } if (pollres > 0) { nread = SSL_read(conn->ssl, buf, len); if (nread <= 0) { err = SSL_get_error(conn->ssl, nread); if ((err == SSL_ERROR_SYSCALL) && (nread == -1)) { err = ERRNO; } else if ((err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE)) { nread = 0; } else { DEBUG_TRACE("SSL_read() failed, error %d", err); return -2; } } else { err = 0; } ERR_clear_error(); } else if (pollres < 0) { /* Error */ return -2; } else { /* pollres = 0 means timeout */ nread = 0; } #endif } else { struct mg_pollfd pfd[1]; int pollres; pfd[0].fd = conn->client.sock; pfd[0].events = POLLIN; pollres = mg_poll(pfd, 1, (int)(timeout * 1000.0), &(conn->phys_ctx->stop_flag)); if (conn->phys_ctx->stop_flag) { return -2; } if (pollres > 0) { nread = (int)recv(conn->client.sock, buf, (len_t)len, 0); err = (nread < 0) ? ERRNO : 0; if (nread <= 0) { /* shutdown of the socket at client side */ return -2; } } else if (pollres < 0) { /* error callint poll */ return -2; } else { /* pollres = 0 means timeout */ nread = 0; } } if (conn->phys_ctx->stop_flag) { return -2; } if ((nread > 0) || ((nread == 0) && (len == 0))) { /* some data has been read, or no data was requested */ return nread; } if (nread < 0) { /* socket error - check errno */ #if defined(_WIN32) if (err == WSAEWOULDBLOCK) { /* TODO (low): check if this is still required */ /* standard case if called from close_socket_gracefully */ return -2; } else if (err == WSAETIMEDOUT) { /* TODO (low): check if this is still required */ /* timeout is handled by the while loop */ return 0; } else if (err == WSAECONNABORTED) { /* See https://www.chilkatsoft.com/p/p_299.asp */ return -2; } else { DEBUG_TRACE("recv() failed, error %d", err); return -2; } #else /* TODO: POSIX returns either EAGAIN or EWOULDBLOCK in both cases, * if the timeout is reached and if the socket was set to non- * blocking in close_socket_gracefully, so we can not distinguish * here. We have to wait for the timeout in both cases for now. */ if ((err == EAGAIN) || (err == EWOULDBLOCK) || (err == EINTR)) { /* TODO (low): check if this is still required */ /* EAGAIN/EWOULDBLOCK: * standard case if called from close_socket_gracefully * => should return -1 */ /* or timeout occurred * => the code must stay in the while loop */ /* EINTR can be generated on a socket with a timeout set even * when SA_RESTART is effective for all relevant signals * (see signal(7)). * => stay in the while loop */ } else { DEBUG_TRACE("recv() failed, error %d", err); return -2; } #endif } /* Timeout occurred, but no data available. */ return -1; } static int pull_all(FILE *fp, struct mg_connection *conn, char *buf, int len) { int n, nread = 0; double timeout = -1.0; uint64_t start_time = 0, now = 0, timeout_ns = 0; if (conn->dom_ctx->config[REQUEST_TIMEOUT]) { timeout = atoi(conn->dom_ctx->config[REQUEST_TIMEOUT]) / 1000.0; } if (timeout >= 0.0) { start_time = mg_get_current_time_ns(); timeout_ns = (uint64_t)(timeout * 1.0E9); } while ((len > 0) && (conn->phys_ctx->stop_flag == 0)) { n = pull_inner(fp, conn, buf + nread, len, timeout); if (n == -2) { if (nread == 0) { nread = -1; /* Propagate the error */ } break; } else if (n == -1) { /* timeout */ if (timeout >= 0.0) { now = mg_get_current_time_ns(); if ((now - start_time) <= timeout_ns) { continue; } } break; } else if (n == 0) { break; /* No more data to read */ } else { nread += n; len -= n; } } return nread; } static void discard_unread_request_data(struct mg_connection *conn) { char buf[MG_BUF_LEN]; while (mg_read(conn, buf, sizeof(buf)) > 0) ; } static int mg_read_inner(struct mg_connection *conn, void *buf, size_t len) { int64_t content_len, n, buffered_len, nread; int64_t len64 = (int64_t)((len > INT_MAX) ? INT_MAX : len); /* since the return value is * int, we may not read more * bytes */ const char *body; if (conn == NULL) { return 0; } /* If Content-Length is not set for a response with body data, * we do not know in advance how much data should be read. */ content_len = conn->content_len; if (content_len < 0) { /* The body data is completed when the connection is closed. */ content_len = INT64_MAX; } nread = 0; if (conn->consumed_content < content_len) { /* Adjust number of bytes to read. */ int64_t left_to_read = content_len - conn->consumed_content; if (left_to_read < len64) { /* Do not read more than the total content length of the * request. */ len64 = left_to_read; } /* Return buffered data */ buffered_len = (int64_t)(conn->data_len) - (int64_t)conn->request_len - conn->consumed_content; if (buffered_len > 0) { if (len64 < buffered_len) { buffered_len = len64; } body = conn->buf + conn->request_len + conn->consumed_content; memcpy(buf, body, (size_t)buffered_len); len64 -= buffered_len; conn->consumed_content += buffered_len; nread += buffered_len; buf = (char *)buf + buffered_len; } /* We have returned all buffered data. Read new data from the remote * socket. */ if ((n = pull_all(NULL, conn, (char *)buf, (int)len64)) >= 0) { conn->consumed_content += n; nread += n; } else { nread = ((nread > 0) ? nread : n); } } return (int)nread; } int mg_read(struct mg_connection *conn, void *buf, size_t len) { if (len > INT_MAX) { len = INT_MAX; } if (conn == NULL) { return 0; } if (conn->is_chunked) { size_t all_read = 0; while (len > 0) { if (conn->is_chunked >= 3) { /* No more data left to read */ return 0; } if (conn->is_chunked != 1) { /* Has error */ return -1; } if (conn->consumed_content != conn->content_len) { /* copy from the current chunk */ int read_ret = mg_read_inner(conn, (char *)buf + all_read, len); if (read_ret < 1) { /* read error */ conn->is_chunked = 2; return -1; } all_read += (size_t)read_ret; len -= (size_t)read_ret; if (conn->consumed_content == conn->content_len) { /* Add data bytes in the current chunk have been read, * so we are expecting \r\n now. */ char x[2]; conn->content_len += 2; if ((mg_read_inner(conn, x, 2) != 2) || (x[0] != '\r') || (x[1] != '\n')) { /* Protocol violation */ conn->is_chunked = 2; return -1; } } } else { /* fetch a new chunk */ size_t i; char lenbuf[64]; char *end = NULL; unsigned long chunkSize = 0; for (i = 0; i < (sizeof(lenbuf) - 1); i++) { conn->content_len++; if (mg_read_inner(conn, lenbuf + i, 1) != 1) { lenbuf[i] = 0; } if ((i > 0) && (lenbuf[i] == '\r') && (lenbuf[i - 1] != '\r')) { continue; } if ((i > 1) && (lenbuf[i] == '\n') && (lenbuf[i - 1] == '\r')) { lenbuf[i + 1] = 0; chunkSize = strtoul(lenbuf, &end, 16); if (chunkSize == 0) { /* regular end of content */ conn->is_chunked = 3; } break; } if (!isxdigit((unsigned char)lenbuf[i])) { /* illegal character for chunk length */ conn->is_chunked = 2; return -1; } } if ((end == NULL) || (*end != '\r')) { /* chunksize not set correctly */ conn->is_chunked = 2; return -1; } if (chunkSize == 0) { /* try discarding trailer for keep-alive */ conn->content_len += 2; if ((mg_read_inner(conn, lenbuf, 2) == 2) && (lenbuf[0] == '\r') && (lenbuf[1] == '\n')) { conn->is_chunked = 4; } break; } /* append a new chunk */ conn->content_len += chunkSize; } } return (int)all_read; } return mg_read_inner(conn, buf, len); } int mg_write(struct mg_connection *conn, const void *buf, size_t len) { time_t now; int n, total, allowed; if (conn == NULL) { return 0; } if (len > INT_MAX) { return -1; } if (conn->throttle > 0) { if ((now = time(NULL)) != conn->last_throttle_time) { conn->last_throttle_time = now; conn->last_throttle_bytes = 0; } allowed = conn->throttle - conn->last_throttle_bytes; if (allowed > (int)len) { allowed = (int)len; } if ((total = push_all(conn->phys_ctx, NULL, conn->client.sock, conn->ssl, (const char *)buf, allowed)) == allowed) { buf = (const char *)buf + total; conn->last_throttle_bytes += total; while ((total < (int)len) && (conn->phys_ctx->stop_flag == 0)) { allowed = (conn->throttle > ((int)len - total)) ? (int)len - total : conn->throttle; if ((n = push_all(conn->phys_ctx, NULL, conn->client.sock, conn->ssl, (const char *)buf, allowed)) != allowed) { break; } sleep(1); conn->last_throttle_bytes = allowed; conn->last_throttle_time = time(NULL); buf = (const char *)buf + n; total += n; } } } else { total = push_all(conn->phys_ctx, NULL, conn->client.sock, conn->ssl, (const char *)buf, (int)len); } if (total > 0) { conn->num_bytes_sent += total; } return total; } /* Send a chunk, if "Transfer-Encoding: chunked" is used */ int mg_send_chunk(struct mg_connection *conn, const char *chunk, unsigned int chunk_len) { char lenbuf[16]; size_t lenbuf_len; int ret; int t; /* First store the length information in a text buffer. */ snprintf(lenbuf, sizeof(lenbuf), "%x\r\n", chunk_len); lenbuf_len = strlen(lenbuf); /* Then send length information, chunk and terminating \r\n. */ ret = mg_write(conn, lenbuf, lenbuf_len); if (ret != (int)lenbuf_len) { return -1; } t = ret; ret = mg_write(conn, chunk, chunk_len); if (ret != (int)chunk_len) { return -1; } t += ret; ret = mg_write(conn, "\r\n", 2); if (ret != 2) { return -1; } t += ret; return t; } #if defined(GCC_DIAGNOSTIC) /* This block forwards format strings to printf implementations, * so we need to disable the format-nonliteral warning. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif /* Alternative alloc_vprintf() for non-compliant C runtimes */ static int alloc_vprintf2(char **buf, const char *fmt, va_list ap) { va_list ap_copy; size_t size = MG_BUF_LEN / 4; int len = -1; *buf = NULL; while (len < 0) { if (*buf) { mg_free(*buf); } size *= 4; *buf = (char *)mg_malloc(size); if (!*buf) { break; } va_copy(ap_copy, ap); len = vsnprintf_impl(*buf, size - 1, fmt, ap_copy); va_end(ap_copy); (*buf)[size - 1] = 0; } return len; } /* Print message to buffer. If buffer is large enough to hold the message, * return buffer. If buffer is to small, allocate large enough buffer on * heap, * and return allocated buffer. */ static int alloc_vprintf(char **out_buf, char *prealloc_buf, size_t prealloc_size, const char *fmt, va_list ap) { va_list ap_copy; int len; /* Windows is not standard-compliant, and vsnprintf() returns -1 if * buffer is too small. Also, older versions of msvcrt.dll do not have * _vscprintf(). However, if size is 0, vsnprintf() behaves correctly. * Therefore, we make two passes: on first pass, get required message * length. * On second pass, actually print the message. */ va_copy(ap_copy, ap); len = vsnprintf_impl(NULL, 0, fmt, ap_copy); va_end(ap_copy); if (len < 0) { /* C runtime is not standard compliant, vsnprintf() returned -1. * Switch to alternative code path that uses incremental * allocations. */ va_copy(ap_copy, ap); len = alloc_vprintf2(out_buf, fmt, ap_copy); va_end(ap_copy); } else if ((size_t)(len) >= prealloc_size) { /* The pre-allocated buffer not large enough. */ /* Allocate a new buffer. */ *out_buf = (char *)mg_malloc((size_t)(len) + 1); if (!*out_buf) { /* Allocation failed. Return -1 as "out of memory" error. */ return -1; } /* Buffer allocation successful. Store the string there. */ va_copy(ap_copy, ap); IGNORE_UNUSED_RESULT( vsnprintf_impl(*out_buf, (size_t)(len) + 1, fmt, ap_copy)); va_end(ap_copy); } else { /* The pre-allocated buffer is large enough. * Use it to store the string and return the address. */ va_copy(ap_copy, ap); IGNORE_UNUSED_RESULT( vsnprintf_impl(prealloc_buf, prealloc_size, fmt, ap_copy)); va_end(ap_copy); *out_buf = prealloc_buf; } return len; } #if defined(GCC_DIAGNOSTIC) /* Enable format-nonliteral warning again. */ # pragma GCC diagnostic pop #endif static int mg_vprintf(struct mg_connection *conn, const char *fmt, va_list ap) { char mem[MG_BUF_LEN]; char *buf = NULL; int len; if ((len = alloc_vprintf(&buf, mem, sizeof(mem), fmt, ap)) > 0) { len = mg_write(conn, buf, (size_t)len); } if (buf != mem) { mg_free(buf); } return len; } int mg_printf(struct mg_connection *conn, const char *fmt, ...) { va_list ap; int result; va_start(ap, fmt); result = mg_vprintf(conn, fmt, ap); va_end(ap); return result; } int mg_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded) { int i, j, a, b; #define HEXTOI(x) (isdigit(x) ? (x - '0') : (x - 'W')) for (i = j = 0; (i < src_len) && (j < (dst_len - 1)); i++, j++) { if ((i < src_len - 2) && (src[i] == '%') && isxdigit((unsigned char)src[i + 1]) && isxdigit((unsigned char)src[i + 2])) { a = tolower((unsigned char)src[i + 1]); b = tolower((unsigned char)src[i + 2]); dst[j] = (char)((HEXTOI(a) << 4) | HEXTOI(b)); i += 2; } else if (is_form_url_encoded && (src[i] == '+')) { dst[j] = ' '; } else { dst[j] = src[i]; } } dst[j] = '\0'; /* Null-terminate the destination */ return (i >= src_len) ? j : -1; } int mg_get_var(const char *data, size_t data_len, const char *name, char *dst, size_t dst_len) { return mg_get_var2(data, data_len, name, dst, dst_len, 0); } int mg_get_var2(const char *data, size_t data_len, const char *name, char *dst, size_t dst_len, size_t occurrence) { const char *p, *e, *s; size_t name_len; int len; if ((dst == NULL) || (dst_len == 0)) { len = -2; } else if ((data == NULL) || (name == NULL) || (data_len == 0)) { len = -1; dst[0] = '\0'; } else { name_len = strlen(name); e = data + data_len; len = -1; dst[0] = '\0'; /* data is "var1=val1&var2=val2...". Find variable first */ for (p = data; p + name_len < e; p++) { if (((p == data) || (p[-1] == '&')) && (p[name_len] == '=') && !mg_strncasecmp(name, p, name_len) && 0 == occurrence--) { /* Point p to variable value */ p += name_len + 1; /* Point s to the end of the value */ s = (const char *)memchr(p, '&', (size_t)(e - p)); if (s == NULL) { s = e; } DEBUG_ASSERT(s >= p); if (s < p) { return -3; } /* Decode variable into destination buffer */ len = mg_url_decode(p, (int)(s - p), dst, (int)dst_len, 1); /* Redirect error code from -1 to -2 (destination buffer too * small). */ if (len == -1) { len = -2; } break; } } } return len; } /* HCP24: some changes to compare hole var_name */ int mg_get_cookie(const char *cookie_header, const char *var_name, char *dst, size_t dst_size) { const char *s, *p, *end; int name_len, len = -1; if ((dst == NULL) || (dst_size == 0)) { return -2; } dst[0] = '\0'; if ((var_name == NULL) || ((s = cookie_header) == NULL)) { return -1; } name_len = (int)strlen(var_name); end = s + strlen(s); for (; (s = mg_strcasestr(s, var_name)) != NULL; s += name_len) { if (s[name_len] == '=') { /* HCP24: now check is it a substring or a full cookie name */ if ((s == cookie_header) || (s[-1] == ' ')) { s += name_len + 1; if ((p = strchr(s, ' ')) == NULL) { p = end; } if (p[-1] == ';') { p--; } if ((*s == '"') && (p[-1] == '"') && (p > s + 1)) { s++; p--; } if ((size_t)(p - s) < dst_size) { len = (int)(p - s); mg_strlcpy(dst, s, (size_t)len + 1); } else { len = -3; } break; } } } return len; } #if defined(USE_WEBSOCKET) || defined(USE_LUA) static void base64_encode(const unsigned char *src, int src_len, char *dst) { static const char *b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; int i, j, a, b, c; for (i = j = 0; i < src_len; i += 3) { a = src[i]; b = ((i + 1) >= src_len) ? 0 : src[i + 1]; c = ((i + 2) >= src_len) ? 0 : src[i + 2]; dst[j++] = b64[a >> 2]; dst[j++] = b64[((a & 3) << 4) | (b >> 4)]; if (i + 1 < src_len) { dst[j++] = b64[(b & 15) << 2 | (c >> 6)]; } if (i + 2 < src_len) { dst[j++] = b64[c & 63]; } } while (j % 4 != 0) { dst[j++] = '='; } dst[j++] = '\0'; } #endif #if defined(USE_LUA) static unsigned char b64reverse(char letter) { if ((letter >= 'A') && (letter <= 'Z')) { return letter - 'A'; } if ((letter >= 'a') && (letter <= 'z')) { return letter - 'a' + 26; } if ((letter >= '0') && (letter <= '9')) { return letter - '0' + 52; } if (letter == '+') { return 62; } if (letter == '/') { return 63; } if (letter == '=') { return 255; /* normal end */ } return 254; /* error */ } static int base64_decode(const unsigned char *src, int src_len, char *dst, size_t *dst_len) { int i; unsigned char a, b, c, d; *dst_len = 0; for (i = 0; i < src_len; i += 4) { a = b64reverse(src[i]); if (a >= 254) { return i; } b = b64reverse(((i + 1) >= src_len) ? 0 : src[i + 1]); if (b >= 254) { return i + 1; } c = b64reverse(((i + 2) >= src_len) ? 0 : src[i + 2]); if (c == 254) { return i + 2; } d = b64reverse(((i + 3) >= src_len) ? 0 : src[i + 3]); if (d == 254) { return i + 3; } dst[(*dst_len)++] = (a << 2) + (b >> 4); if (c != 255) { dst[(*dst_len)++] = (b << 4) + (c >> 2); if (d != 255) { dst[(*dst_len)++] = (c << 6) + d; } } } return -1; } #endif static int is_put_or_delete_method(const struct mg_connection *conn) { if (conn) { const char *s = conn->request_info.request_method; return (s != NULL) && (!strcmp(s, "PUT") || !strcmp(s, "DELETE") || !strcmp(s, "MKCOL") || !strcmp(s, "PATCH")); } return 0; } #if !defined(NO_FILES) static int extention_matches_script( struct mg_connection *conn, /* in: request (must be valid) */ const char *filename /* in: filename (must be valid) */ ) { #if !defined(NO_CGI) if (match_prefix(conn->dom_ctx->config[CGI_EXTENSIONS], strlen(conn->dom_ctx->config[CGI_EXTENSIONS]), filename) > 0) { return 1; } #endif #if defined(USE_LUA) if (match_prefix(conn->dom_ctx->config[LUA_SCRIPT_EXTENSIONS], strlen(conn->dom_ctx->config[LUA_SCRIPT_EXTENSIONS]), filename) > 0) { return 1; } #endif #if defined(USE_DUKTAPE) if (match_prefix(conn->dom_ctx->config[DUKTAPE_SCRIPT_EXTENSIONS], strlen(conn->dom_ctx->config[DUKTAPE_SCRIPT_EXTENSIONS]), filename) > 0) { return 1; } #endif /* filename and conn could be unused, if all preocessor conditions * are false (no script language supported). */ (void)filename; (void)conn; return 0; } /* For given directory path, substitute it to valid index file. * Return 1 if index file has been found, 0 if not found. * If the file is found, it's stats is returned in stp. */ static int substitute_index_file(struct mg_connection *conn, char *path, size_t path_len, struct mg_file_stat *filestat) { const char *list = conn->dom_ctx->config[INDEX_FILES]; struct vec filename_vec; size_t n = strlen(path); int found = 0; /* The 'path' given to us points to the directory. Remove all trailing * directory separator characters from the end of the path, and * then append single directory separator character. */ while ((n > 0) && (path[n - 1] == '/')) { n--; } path[n] = '/'; /* Traverse index files list. For each entry, append it to the given * path and see if the file exists. If it exists, break the loop */ while ((list = next_option(list, &filename_vec, NULL)) != NULL) { /* Ignore too long entries that may overflow path buffer */ if ((filename_vec.len + 1) > (path_len - (n + 1))) { continue; } /* Prepare full path to the index file */ mg_strlcpy(path + n + 1, filename_vec.ptr, filename_vec.len + 1); /* Does it exist? */ if (mg_stat(conn, path, filestat)) { /* Yes it does, break the loop */ found = 1; break; } } /* If no index file exists, restore directory path */ if (!found) { path[n] = '\0'; } return found; } #endif static void interpret_uri(struct mg_connection *conn, /* in/out: request (must be valid) */ char *filename, /* out: filename */ size_t filename_buf_len, /* in: size of filename buffer */ struct mg_file_stat *filestat, /* out: file status structure */ int *is_found, /* out: file found (directly) */ int *is_script_resource, /* out: handled by a script? */ int *is_websocket_request, /* out: websocket connetion? */ int *is_put_or_delete_request /* out: put/delete a file? */ ) { char const *accept_encoding; #if !defined(NO_FILES) const char *uri = conn->request_info.local_uri; const char *root = conn->dom_ctx->config[DOCUMENT_ROOT]; const char *rewrite; struct vec a, b; ptrdiff_t match_len; char gz_path[PATH_MAX]; int truncated; #if !defined(NO_CGI) || defined(USE_LUA) || defined(USE_DUKTAPE) char *tmp_str; size_t tmp_str_len, sep_pos; int allow_substitute_script_subresources; #endif #else (void)filename_buf_len; /* unused if NO_FILES is defined */ #endif /* Step 1: Set all initially unknown outputs to zero */ memset(filestat, 0, sizeof(*filestat)); *filename = 0; *is_found = 0; *is_script_resource = 0; /* Step 2: Check if the request attempts to modify the file system */ *is_put_or_delete_request = is_put_or_delete_method(conn); /* Step 3: Check if it is a websocket request, and modify the document * root if required */ #if defined(USE_WEBSOCKET) *is_websocket_request = is_websocket_protocol(conn); #if !defined(NO_FILES) if (*is_websocket_request && conn->dom_ctx->config[WEBSOCKET_ROOT]) { root = conn->dom_ctx->config[WEBSOCKET_ROOT]; } #endif /* !NO_FILES */ #else /* USE_WEBSOCKET */ *is_websocket_request = 0; #endif /* USE_WEBSOCKET */ /* Step 4: Check if gzip encoded response is allowed */ conn->accept_gzip = 0; if ((accept_encoding = mg_get_header(conn, "Accept-Encoding")) != NULL) { if (strstr(accept_encoding, "gzip") != NULL) { conn->accept_gzip = 1; } } #if !defined(NO_FILES) /* Step 5: If there is no root directory, don't look for files. */ /* Note that root == NULL is a regular use case here. This occurs, * if all requests are handled by callbacks, so the WEBSOCKET_ROOT * config is not required. */ if (root == NULL) { /* all file related outputs have already been set to 0, just return */ return; } /* Step 6: Determine the local file path from the root path and the * request uri. */ /* Using filename_buf_len - 1 because memmove() for PATH_INFO may shift * part of the path one byte on the right. */ mg_snprintf( conn, &truncated, filename, filename_buf_len - 1, "%s%s", root, uri); if (truncated) { goto interpret_cleanup; } /* Step 7: URI rewriting */ rewrite = conn->dom_ctx->config[URL_REWRITE_PATTERN]; while ((rewrite = next_option(rewrite, &a, &b)) != NULL) { if ((match_len = match_prefix(a.ptr, a.len, uri)) > 0) { mg_snprintf(conn, &truncated, filename, filename_buf_len - 1, "%.*s%s", (int)b.len, b.ptr, uri + match_len); break; } } if (truncated) { goto interpret_cleanup; } /* Step 8: Check if the file exists at the server */ /* Local file path and name, corresponding to requested URI * is now stored in "filename" variable. */ if (mg_stat(conn, filename, filestat)) { int uri_len = (int)strlen(uri); int is_uri_end_slash = (uri_len > 0) && (uri[uri_len - 1] == '/'); /* 8.1: File exists. */ *is_found = 1; /* 8.2: Check if it is a script type. */ if (extention_matches_script(conn, filename)) { /* The request addresses a CGI resource, Lua script or * server-side javascript. * The URI corresponds to the script itself (like * /path/script.cgi), and there is no additional resource * path (like /path/script.cgi/something). * Requests that modify (replace or delete) a resource, like * PUT and DELETE requests, should replace/delete the script * file. * Requests that read or write from/to a resource, like GET and * POST requests, should call the script and return the * generated response. */ *is_script_resource = (!*is_put_or_delete_request); } /* 8.3: If the request target is a directory, there could be * a substitute file (index.html, index.cgi, ...). */ if (filestat->is_directory && is_uri_end_slash) { /* Use a local copy here, since substitute_index_file will * change the content of the file status */ struct mg_file_stat tmp_filestat; memset(&tmp_filestat, 0, sizeof(tmp_filestat)); if (substitute_index_file( conn, filename, filename_buf_len, &tmp_filestat)) { /* Substitute file found. Copy stat to the output, then * check if the file is a script file */ *filestat = tmp_filestat; if (extention_matches_script(conn, filename)) { /* Substitute file is a script file */ *is_script_resource = 1; } else { /* Substitute file is a regular file */ *is_script_resource = 0; *is_found = (mg_stat(conn, filename, filestat) ? 1 : 0); } } /* If there is no substitute file, the server could return * a directory listing in a later step */ } return; } /* Step 9: Check for zipped files: */ /* If we can't find the actual file, look for the file * with the same name but a .gz extension. If we find it, * use that and set the gzipped flag in the file struct * to indicate that the response need to have the content- * encoding: gzip header. * We can only do this if the browser declares support. */ if (conn->accept_gzip) { mg_snprintf( conn, &truncated, gz_path, sizeof(gz_path), "%s.gz", filename); if (truncated) { goto interpret_cleanup; } if (mg_stat(conn, gz_path, filestat)) { if (filestat) { filestat->is_gzipped = 1; *is_found = 1; } /* Currently gz files can not be scripts. */ return; } } #if !defined(NO_CGI) || defined(USE_LUA) || defined(USE_DUKTAPE) /* Step 10: Script resources may handle sub-resources */ /* Support PATH_INFO for CGI scripts. */ tmp_str_len = strlen(filename); tmp_str = (char *)mg_malloc_ctx(tmp_str_len + PATH_MAX + 1, conn->phys_ctx); if (!tmp_str) { /* Out of memory */ goto interpret_cleanup; } memcpy(tmp_str, filename, tmp_str_len + 1); /* Check config, if index scripts may have sub-resources */ allow_substitute_script_subresources = !mg_strcasecmp(conn->dom_ctx->config[ALLOW_INDEX_SCRIPT_SUB_RES], "yes"); sep_pos = tmp_str_len; while (sep_pos > 0) { sep_pos--; if (tmp_str[sep_pos] == '/') { int is_script = 0, does_exist = 0; tmp_str[sep_pos] = 0; if (tmp_str[0]) { is_script = extention_matches_script(conn, tmp_str); does_exist = mg_stat(conn, tmp_str, filestat); } if (does_exist && is_script) { filename[sep_pos] = 0; memmove(filename + sep_pos + 2, filename + sep_pos + 1, strlen(filename + sep_pos + 1) + 1); conn->path_info = filename + sep_pos + 1; filename[sep_pos + 1] = '/'; *is_script_resource = 1; *is_found = 1; break; } if (allow_substitute_script_subresources) { if (substitute_index_file( conn, tmp_str, tmp_str_len + PATH_MAX, filestat)) { /* some intermediate directory has an index file */ if (extention_matches_script(conn, tmp_str)) { char *tmp_str2; DEBUG_TRACE("Substitute script %s serving path %s", tmp_str, filename); /* this index file is a script */ tmp_str2 = mg_strdup_ctx(filename + sep_pos + 1, conn->phys_ctx); mg_snprintf(conn, &truncated, filename, filename_buf_len, "%s//%s", tmp_str, tmp_str2); mg_free(tmp_str2); if (truncated) { mg_free(tmp_str); goto interpret_cleanup; } sep_pos = strlen(tmp_str); filename[sep_pos] = 0; conn->path_info = filename + sep_pos + 1; *is_script_resource = 1; *is_found = 1; break; } else { DEBUG_TRACE("Substitute file %s serving path %s", tmp_str, filename); /* non-script files will not have sub-resources */ filename[sep_pos] = 0; conn->path_info = 0; *is_script_resource = 0; *is_found = 0; break; } } } tmp_str[sep_pos] = '/'; } } mg_free(tmp_str); #endif /* !defined(NO_CGI) || defined(USE_LUA) || defined(USE_DUKTAPE) */ #endif /* !defined(NO_FILES) */ return; #if !defined(NO_FILES) /* Reset all outputs */ interpret_cleanup: memset(filestat, 0, sizeof(*filestat)); *filename = 0; *is_found = 0; *is_script_resource = 0; *is_websocket_request = 0; *is_put_or_delete_request = 0; #endif /* !defined(NO_FILES) */ } /* Check whether full request is buffered. Return: * -1 if request or response is malformed * 0 if request or response is not yet fully buffered * >0 actual request length, including last \r\n\r\n */ static int get_http_header_len(const char *buf, int buflen) { int i; for (i = 0; i < buflen; i++) { /* Do an unsigned comparison in some conditions below */ const unsigned char c = (unsigned char)buf[i]; if ((c < 128) && ((char)c != '\r') && ((char)c != '\n') && !isprint(c)) { /* abort scan as soon as one malformed character is found */ return -1; } if (i < buflen - 1) { if ((buf[i] == '\n') && (buf[i + 1] == '\n')) { /* Two newline, no carriage return - not standard compliant, * but * it * should be accepted */ return i + 2; } } if (i < buflen - 3) { if ((buf[i] == '\r') && (buf[i + 1] == '\n') && (buf[i + 2] == '\r') && (buf[i + 3] == '\n')) { /* Two \r\n - standard compliant */ return i + 4; } } } return 0; } #if !defined(NO_CACHING) /* Convert month to the month number. Return -1 on error, or month number */ static int get_month_index(const char *s) { size_t i; for (i = 0; i < ARRAY_SIZE(month_names); i++) { if (!strcmp(s, month_names[i])) { return (int)i; } } return -1; } /* Parse UTC date-time string, and return the corresponding time_t value. */ static time_t parse_date_string(const char *datetime) { char month_str[32] = {0}; int second, minute, hour, day, month, year; time_t result = (time_t)0; struct tm tm; if ((sscanf(datetime, "%d/%3s/%d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6) || (sscanf(datetime, "%d %3s %d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6) || (sscanf(datetime, "%*3s, %d %3s %d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6) || (sscanf(datetime, "%d-%3s-%d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6)) { month = get_month_index(month_str); if ((month >= 0) && (year >= 1970)) { memset(&tm, 0, sizeof(tm)); tm.tm_year = year - 1900; tm.tm_mon = month; tm.tm_mday = day; tm.tm_hour = hour; tm.tm_min = minute; tm.tm_sec = second; result = timegm(&tm); } } return result; } #endif /* !NO_CACHING */ /* Pre-process URIs according to RFC + protect against directory disclosure * attacks by removing '..', excessive '/' and '\' characters */ static void remove_dot_segments(char *inout) { /* Windows backend protection * (https://tools.ietf.org/html/rfc3986#section-7.3): Replace backslash in * URI by slash */ char *in_copy = mg_strdup(inout); char *out_begin = inout; char *out_end = inout; char *in = in_copy; int replaced; while (*in) { if (*in == '\\') { *in = '/'; } in++; } /* Algorithm "remove_dot_segments" from * https://tools.ietf.org/html/rfc3986#section-5.2.4 */ /* Step 1: * The input buffer is initialized. * The output buffer is initialized to the empty string. */ in = in_copy; /* Step 2: * While the input buffer is not empty, loop as follows: */ while (*in) { /* Step 2a: * If the input buffer begins with a prefix of "../" or "./", * then remove that prefix from the input buffer; */ if (!strncmp(in, "../", 3)) { in += 3; } else if (!strncmp(in, "./", 2)) { in += 2; } /* otherwise */ /* Step 2b: * if the input buffer begins with a prefix of "/./" or "/.", * where "." is a complete path segment, then replace that * prefix with "/" in the input buffer; */ else if (!strncmp(in, "/./", 3)) { in += 2; } else if (!strcmp(in, "/.")) { in[1] = 0; } /* otherwise */ /* Step 2c: * if the input buffer begins with a prefix of "/../" or "/..", * where ".." is a complete path segment, then replace that * prefix with "/" in the input buffer and remove the last * segment and its preceding "/" (if any) from the output * buffer; */ else if (!strncmp(in, "/../", 4)) { in += 3; if (out_begin != out_end) { /* remove last segment */ do { out_end--; *out_end = 0; } while ((out_begin != out_end) && (*out_end != '/')); } } else if (!strcmp(in, "/..")) { in[1] = 0; if (out_begin != out_end) { /* remove last segment */ do { out_end--; *out_end = 0; } while ((out_begin != out_end) && (*out_end != '/')); } } /* otherwise */ /* Step 2d: * if the input buffer consists only of "." or "..", then remove * that from the input buffer; */ else if (!strcmp(in, ".") || !strcmp(in, "..")) { *in = 0; } /* otherwise */ /* Step 2e: * move the first path segment in the input buffer to the end of * the output buffer, including the initial "/" character (if * any) and any subsequent characters up to, but not including, * the next "/" character or the end of the input buffer. */ else { do { *out_end = *in; out_end++; in++; } while ((*in != 0) && (*in != '/')); } } /* Step 3: * Finally, the output buffer is returned as the result of * remove_dot_segments. */ /* Terminate output */ *out_end = 0; /* For Windows, the files/folders "x" and "x." (with a dot but without * extension) are identical. Replace all "./" by "/" and remove a "." at the * end. * Also replace all "//" by "/". * Repeat until there is no "./" or "//" anymore. */ do { replaced = 0; /* replace ./ by / */ out_end = out_begin; while (*out_end) { if ((*out_end == '.') && ((out_end[1] == '/') || (out_end[1] == 0))) { char *r = out_end; do { r[0] = r[1]; r++; replaced = 1; } while (r[0] != 0); } out_end++; } /* replace ./ by / */ out_end = out_begin; while (*out_end) { if ((out_end[0] == '/') && (out_end[1] == '/')) { char *c = out_end; while (*c) { c[0] = c[1]; c++; } replaced = 1; } out_end++; } } while (replaced); /* Free temporary copies */ mg_free(in_copy); } static const struct { const char *extension; size_t ext_len; const char *mime_type; } builtin_mime_types[] = { /* IANA registered MIME types * (http://www.iana.org/assignments/media-types) * application types */ {".doc", 4, "application/msword"}, {".eps", 4, "application/postscript"}, {".exe", 4, "application/octet-stream"}, {".js", 3, "application/javascript"}, {".json", 5, "application/json"}, {".pdf", 4, "application/pdf"}, {".ps", 3, "application/postscript"}, {".rtf", 4, "application/rtf"}, {".xhtml", 6, "application/xhtml+xml"}, {".xsl", 4, "application/xml"}, {".xslt", 5, "application/xml"}, /* fonts */ {".ttf", 4, "application/font-sfnt"}, {".cff", 4, "application/font-sfnt"}, {".otf", 4, "application/font-sfnt"}, {".aat", 4, "application/font-sfnt"}, {".sil", 4, "application/font-sfnt"}, {".pfr", 4, "application/font-tdpfr"}, {".woff", 5, "application/font-woff"}, /* audio */ {".mp3", 4, "audio/mpeg"}, {".oga", 4, "audio/ogg"}, {".ogg", 4, "audio/ogg"}, /* image */ {".gif", 4, "image/gif"}, {".ief", 4, "image/ief"}, {".jpeg", 5, "image/jpeg"}, {".jpg", 4, "image/jpeg"}, {".jpm", 4, "image/jpm"}, {".jpx", 4, "image/jpx"}, {".png", 4, "image/png"}, {".svg", 4, "image/svg+xml"}, {".tif", 4, "image/tiff"}, {".tiff", 5, "image/tiff"}, /* model */ {".wrl", 4, "model/vrml"}, /* text */ {".css", 4, "text/css"}, {".csv", 4, "text/csv"}, {".htm", 4, "text/html"}, {".html", 5, "text/html"}, {".sgm", 4, "text/sgml"}, {".shtm", 5, "text/html"}, {".shtml", 6, "text/html"}, {".txt", 4, "text/plain"}, {".xml", 4, "text/xml"}, /* video */ {".mov", 4, "video/quicktime"}, {".mp4", 4, "video/mp4"}, {".mpeg", 5, "video/mpeg"}, {".mpg", 4, "video/mpeg"}, {".ogv", 4, "video/ogg"}, {".qt", 3, "video/quicktime"}, /* not registered types * (http://reference.sitepoint.com/html/mime-types-full, * http://www.hansenb.pdx.edu/DMKB/dict/tutorials/mime_typ.php, ..) */ {".arj", 4, "application/x-arj-compressed"}, {".gz", 3, "application/x-gunzip"}, {".rar", 4, "application/x-arj-compressed"}, {".swf", 4, "application/x-shockwave-flash"}, {".tar", 4, "application/x-tar"}, {".tgz", 4, "application/x-tar-gz"}, {".torrent", 8, "application/x-bittorrent"}, {".ppt", 4, "application/x-mspowerpoint"}, {".xls", 4, "application/x-msexcel"}, {".zip", 4, "application/x-zip-compressed"}, {".aac", 4, "audio/aac"}, /* http://en.wikipedia.org/wiki/Advanced_Audio_Coding */ {".aif", 4, "audio/x-aif"}, {".m3u", 4, "audio/x-mpegurl"}, {".mid", 4, "audio/x-midi"}, {".ra", 3, "audio/x-pn-realaudio"}, {".ram", 4, "audio/x-pn-realaudio"}, {".wav", 4, "audio/x-wav"}, {".bmp", 4, "image/bmp"}, {".ico", 4, "image/x-icon"}, {".pct", 4, "image/x-pct"}, {".pict", 5, "image/pict"}, {".rgb", 4, "image/x-rgb"}, {".webm", 5, "video/webm"}, /* http://en.wikipedia.org/wiki/WebM */ {".asf", 4, "video/x-ms-asf"}, {".avi", 4, "video/x-msvideo"}, {".m4v", 4, "video/x-m4v"}, {NULL, 0, NULL}}; const char * mg_get_builtin_mime_type(const char *path) { const char *ext; size_t i, path_len; path_len = strlen(path); for (i = 0; builtin_mime_types[i].extension != NULL; i++) { ext = path + (path_len - builtin_mime_types[i].ext_len); if ((path_len > builtin_mime_types[i].ext_len) && (mg_strcasecmp(ext, builtin_mime_types[i].extension) == 0)) { return builtin_mime_types[i].mime_type; } } return "text/plain"; } /* Look at the "path" extension and figure what mime type it has. * Store mime type in the vector. */ static void get_mime_type(struct mg_connection *conn, const char *path, struct vec *vec) { struct vec ext_vec, mime_vec; const char *list, *ext; size_t path_len; path_len = strlen(path); if ((conn == NULL) || (vec == NULL)) { if (vec != NULL) { memset(vec, '\0', sizeof(struct vec)); } return; } /* Scan user-defined mime types first, in case user wants to * override default mime types. */ list = conn->dom_ctx->config[EXTRA_MIME_TYPES]; while ((list = next_option(list, &ext_vec, &mime_vec)) != NULL) { /* ext now points to the path suffix */ ext = path + path_len - ext_vec.len; if (mg_strncasecmp(ext, ext_vec.ptr, ext_vec.len) == 0) { *vec = mime_vec; return; } } vec->ptr = mg_get_builtin_mime_type(path); vec->len = strlen(vec->ptr); } /* Stringify binary data. Output buffer must be twice as big as input, * because each byte takes 2 bytes in string representation */ static void bin2str(char *to, const unsigned char *p, size_t len) { static const char *hex = "0123456789abcdef"; for (; len--; p++) { *to++ = hex[p[0] >> 4]; *to++ = hex[p[0] & 0x0f]; } *to = '\0'; } /* Return stringified MD5 hash for list of strings. Buffer must be 33 bytes. */ char * mg_md5(char buf[33], ...) { md5_byte_t hash[16]; const char *p; va_list ap; md5_state_t ctx; md5_init(&ctx); va_start(ap, buf); while ((p = va_arg(ap, const char *)) != NULL) { md5_append(&ctx, (const md5_byte_t *)p, strlen(p)); } va_end(ap); md5_finish(&ctx, hash); bin2str(buf, hash, sizeof(hash)); return buf; } /* Check the user's password, return 1 if OK */ static int check_password(const char *method, const char *ha1, const char *uri, const char *nonce, const char *nc, const char *cnonce, const char *qop, const char *response) { char ha2[32 + 1], expected_response[32 + 1]; /* Some of the parameters may be NULL */ if ((method == NULL) || (nonce == NULL) || (nc == NULL) || (cnonce == NULL) || (qop == NULL) || (response == NULL)) { return 0; } /* NOTE(lsm): due to a bug in MSIE, we do not compare the URI */ if (strlen(response) != 32) { return 0; } mg_md5(ha2, method, ":", uri, NULL); mg_md5(expected_response, ha1, ":", nonce, ":", nc, ":", cnonce, ":", qop, ":", ha2, NULL); return mg_strcasecmp(response, expected_response) == 0; } #if !defined(NO_FILESYSTEMS) /* Use the global passwords file, if specified by auth_gpass option, * or search for .htpasswd in the requested directory. */ static void open_auth_file(struct mg_connection *conn, const char *path, struct mg_file *filep) { if ((conn != NULL) && (conn->dom_ctx != NULL)) { char name[PATH_MAX]; const char *p, *e, *gpass = conn->dom_ctx->config[GLOBAL_PASSWORDS_FILE]; int truncated; if (gpass != NULL) { /* Use global passwords file */ if (!mg_fopen(conn, gpass, MG_FOPEN_MODE_READ, filep)) { #if defined(DEBUG) /* Use mg_cry_internal here, since gpass has been configured. */ mg_cry_internal(conn, "fopen(%s): %s", gpass, strerror(ERRNO)); #endif } /* Important: using local struct mg_file to test path for * is_directory flag. If filep is used, mg_stat() makes it * appear as if auth file was opened. * TODO(mid): Check if this is still required after rewriting * mg_stat */ } else if (mg_stat(conn, path, &filep->stat) && filep->stat.is_directory) { mg_snprintf(conn, &truncated, name, sizeof(name), "%s/%s", path, PASSWORDS_FILE_NAME); if (truncated || !mg_fopen(conn, name, MG_FOPEN_MODE_READ, filep)) { #if defined(DEBUG) /* Don't use mg_cry_internal here, but only a trace, since this * is * a typical case. It will occur for every directory * without a password file. */ DEBUG_TRACE("fopen(%s): %s", name, strerror(ERRNO)); #endif } } else { /* Try to find .htpasswd in requested directory. */ for (p = path, e = p + strlen(p) - 1; e > p; e--) { if (e[0] == '/') { break; } } mg_snprintf(conn, &truncated, name, sizeof(name), "%.*s/%s", (int)(e - p), p, PASSWORDS_FILE_NAME); if (truncated || !mg_fopen(conn, name, MG_FOPEN_MODE_READ, filep)) { #if defined(DEBUG) /* Don't use mg_cry_internal here, but only a trace, since this * is * a typical case. It will occur for every directory * without a password file. */ DEBUG_TRACE("fopen(%s): %s", name, strerror(ERRNO)); #endif } } } } #endif /* NO_FILESYSTEMS */ /* Parsed Authorization header */ struct ah { char *user, *uri, *cnonce, *response, *qop, *nc, *nonce; }; /* Return 1 on success. Always initializes the ah structure. */ static int parse_auth_header(struct mg_connection *conn, char *buf, size_t buf_size, struct ah *ah) { char *name, *value, *s; const char *auth_header; uint64_t nonce; if (!ah || !conn) { return 0; } (void)memset(ah, 0, sizeof(*ah)); if (((auth_header = mg_get_header(conn, "Authorization")) == NULL) || mg_strncasecmp(auth_header, "Digest ", 7) != 0) { return 0; } /* Make modifiable copy of the auth header */ (void)mg_strlcpy(buf, auth_header + 7, buf_size); s = buf; /* Parse authorization header */ for (;;) { /* Gobble initial spaces */ while (isspace((unsigned char)*s)) { s++; } name = skip_quoted(&s, "=", " ", 0); /* Value is either quote-delimited, or ends at first comma or space. */ if (s[0] == '\"') { s++; value = skip_quoted(&s, "\"", " ", '\\'); if (s[0] == ',') { s++; } } else { value = skip_quoted(&s, ", ", " ", 0); /* IE uses commas, FF uses * spaces */ } if (*name == '\0') { break; } if (!strcmp(name, "username")) { ah->user = value; } else if (!strcmp(name, "cnonce")) { ah->cnonce = value; } else if (!strcmp(name, "response")) { ah->response = value; } else if (!strcmp(name, "uri")) { ah->uri = value; } else if (!strcmp(name, "qop")) { ah->qop = value; } else if (!strcmp(name, "nc")) { ah->nc = value; } else if (!strcmp(name, "nonce")) { ah->nonce = value; } } #if !defined(NO_NONCE_CHECK) /* Read the nonce from the response. */ if (ah->nonce == NULL) { return 0; } s = NULL; nonce = strtoull(ah->nonce, &s, 10); if ((s == NULL) || (*s != 0)) { return 0; } /* Convert the nonce from the client to a number. */ nonce ^= conn->dom_ctx->auth_nonce_mask; /* The converted number corresponds to the time the nounce has been * created. This should not be earlier than the server start. */ /* Server side nonce check is valuable in all situations but one: * if the server restarts frequently, but the client should not see * that, so the server should accept nonces from previous starts. */ /* However, the reasonable default is to not accept a nonce from a * previous start, so if anyone changed the access rights between * two restarts, a new login is required. */ if (nonce < (uint64_t)conn->phys_ctx->start_time) { /* nonce is from a previous start of the server and no longer valid * (replay attack?) */ return 0; } /* Check if the nonce is too high, so it has not (yet) been used by the * server. */ if (nonce >= ((uint64_t)conn->phys_ctx->start_time + conn->dom_ctx->nonce_count)) { return 0; } #else (void)nonce; #endif /* CGI needs it as REMOTE_USER */ if (ah->user != NULL) { conn->request_info.remote_user = mg_strdup_ctx(ah->user, conn->phys_ctx); } else { return 0; } return 1; } static const char * mg_fgets(char *buf, size_t size, struct mg_file *filep, char **p) { #if defined(MG_USE_OPEN_FILE) const char *eof; size_t len; const char *memend; #else (void)p; /* parameter is unused */ #endif if (!filep) { return NULL; } #if defined(MG_USE_OPEN_FILE) if ((filep->access.membuf != NULL) && (*p != NULL)) { memend = (const char *)&filep->access.membuf[filep->stat.size]; /* Search for \n from p till the end of stream */ eof = (char *)memchr(*p, '\n', (size_t)(memend - *p)); if (eof != NULL) { eof += 1; /* Include \n */ } else { eof = memend; /* Copy remaining data */ } len = ((size_t)(eof - *p) > (size - 1)) ? (size - 1) : (size_t)(eof - *p); memcpy(buf, *p, len); buf[len] = '\0'; *p += len; return len ? eof : NULL; } else /* filep->access.fp block below */ #endif if (filep->access.fp != NULL) { return fgets(buf, (int)size, filep->access.fp); } else { return NULL; } } /* Define the initial recursion depth for procesesing htpasswd files that * include other htpasswd * (or even the same) files. It is not difficult to provide a file or files * s.t. they force civetweb * to infinitely recurse and then crash. */ #define INITIAL_DEPTH 9 #if INITIAL_DEPTH <= 0 #error Bad INITIAL_DEPTH for recursion, set to at least 1 #endif #if !defined(NO_FILESYSTEMS) struct read_auth_file_struct { struct mg_connection *conn; struct ah ah; const char *domain; char buf[256 + 256 + 40]; const char *f_user; const char *f_domain; const char *f_ha1; }; static int read_auth_file(struct mg_file *filep, struct read_auth_file_struct *workdata, int depth) { char *p = NULL /* init if MG_USE_OPEN_FILE is not set */; int is_authorized = 0; struct mg_file fp; size_t l; if (!filep || !workdata || (0 == depth)) { return 0; } /* Loop over passwords file */ #if defined(MG_USE_OPEN_FILE) p = (char *)filep->access.membuf; #endif while (mg_fgets(workdata->buf, sizeof(workdata->buf), filep, &p) != NULL) { l = strlen(workdata->buf); while (l > 0) { if (isspace((unsigned char)workdata->buf[l - 1]) || iscntrl((unsigned char)workdata->buf[l - 1])) { l--; workdata->buf[l] = 0; } else break; } if (l < 1) { continue; } workdata->f_user = workdata->buf; if (workdata->f_user[0] == ':') { /* user names may not contain a ':' and may not be empty, * so lines starting with ':' may be used for a special purpose */ if (workdata->f_user[1] == '#') { /* :# is a comment */ continue; } else if (!strncmp(workdata->f_user + 1, "include=", 8)) { if (mg_fopen(workdata->conn, workdata->f_user + 9, MG_FOPEN_MODE_READ, &fp)) { is_authorized = read_auth_file(&fp, workdata, depth - 1); (void)mg_fclose( &fp.access); /* ignore error on read only file */ /* No need to continue processing files once we have a * match, since nothing will reset it back * to 0. */ if (is_authorized) { return is_authorized; } } else { mg_cry_internal(workdata->conn, "%s: cannot open authorization file: %s", __func__, workdata->buf); } continue; } /* everything is invalid for the moment (might change in the * future) */ mg_cry_internal(workdata->conn, "%s: syntax error in authorization file: %s", __func__, workdata->buf); continue; } workdata->f_domain = strchr(workdata->f_user, ':'); if (workdata->f_domain == NULL) { mg_cry_internal(workdata->conn, "%s: syntax error in authorization file: %s", __func__, workdata->buf); continue; } *(char *)(workdata->f_domain) = 0; (workdata->f_domain)++; workdata->f_ha1 = strchr(workdata->f_domain, ':'); if (workdata->f_ha1 == NULL) { mg_cry_internal(workdata->conn, "%s: syntax error in authorization file: %s", __func__, workdata->buf); continue; } *(char *)(workdata->f_ha1) = 0; (workdata->f_ha1)++; if (!strcmp(workdata->ah.user, workdata->f_user) && !strcmp(workdata->domain, workdata->f_domain)) { return check_password(workdata->conn->request_info.request_method, workdata->f_ha1, workdata->ah.uri, workdata->ah.nonce, workdata->ah.nc, workdata->ah.cnonce, workdata->ah.qop, workdata->ah.response); } } return is_authorized; } /* Authorize against the opened passwords file. Return 1 if authorized. */ static int authorize(struct mg_connection *conn, struct mg_file *filep, const char *realm) { struct read_auth_file_struct workdata; char buf[MG_BUF_LEN]; if (!conn || !conn->dom_ctx) { return 0; } memset(&workdata, 0, sizeof(workdata)); workdata.conn = conn; if (!parse_auth_header(conn, buf, sizeof(buf), &workdata.ah)) { return 0; } if (realm) { workdata.domain = realm; } else { workdata.domain = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; } return read_auth_file(filep, &workdata, INITIAL_DEPTH); } /* Public function to check http digest authentication header */ int mg_check_digest_access_authentication(struct mg_connection *conn, const char *realm, const char *filename) { struct mg_file file = STRUCT_FILE_INITIALIZER; int auth; if (!conn || !filename) { return -1; } if (!mg_fopen(conn, filename, MG_FOPEN_MODE_READ, &file)) { return -2; } auth = authorize(conn, &file, realm); mg_fclose(&file.access); return auth; } #endif /* NO_FILESYSTEMS */ /* Return 1 if request is authorised, 0 otherwise. */ static int check_authorization(struct mg_connection *conn, const char *path) { #if !defined(NO_FILESYSTEMS) char fname[PATH_MAX]; struct vec uri_vec, filename_vec; const char *list; struct mg_file file = STRUCT_FILE_INITIALIZER; int authorized = 1, truncated; if (!conn || !conn->dom_ctx) { return 0; } list = conn->dom_ctx->config[PROTECT_URI]; while ((list = next_option(list, &uri_vec, &filename_vec)) != NULL) { if (!memcmp(conn->request_info.local_uri, uri_vec.ptr, uri_vec.len)) { mg_snprintf(conn, &truncated, fname, sizeof(fname), "%.*s", (int)filename_vec.len, filename_vec.ptr); if (truncated || !mg_fopen(conn, fname, MG_FOPEN_MODE_READ, &file)) { mg_cry_internal(conn, "%s: cannot open %s: %s", __func__, fname, strerror(errno)); } break; } } if (!is_file_opened(&file.access)) { open_auth_file(conn, path, &file); } if (is_file_opened(&file.access)) { authorized = authorize(conn, &file, NULL); (void)mg_fclose(&file.access); /* ignore error on read only file */ } return authorized; #else (void)conn; (void)path; return 1; #endif /* NO_FILESYSTEMS */ } /* Internal function. Assumes conn is valid */ static void send_authorization_request(struct mg_connection *conn, const char *realm) { char date[64]; time_t curtime = time(NULL); uint64_t nonce = (uint64_t)(conn->phys_ctx->start_time); if (!realm) { realm = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; } (void)pthread_mutex_lock(&conn->phys_ctx->nonce_mutex); nonce += conn->dom_ctx->nonce_count; ++conn->dom_ctx->nonce_count; (void)pthread_mutex_unlock(&conn->phys_ctx->nonce_mutex); nonce ^= conn->dom_ctx->auth_nonce_mask; conn->status_code = 401; conn->must_close = 1; gmt_time_string(date, sizeof(date), &curtime); mg_printf(conn, "HTTP/1.1 401 Unauthorized\r\n"); send_no_cache_header(conn); send_additional_header(conn); mg_printf(conn, "Date: %s\r\n" "Connection: %s\r\n" "Content-Length: 0\r\n" "WWW-Authenticate: Digest qop=\"auth\", realm=\"%s\", " "nonce=\"%" UINT64_FMT "\"\r\n\r\n", date, suggest_connection_header(conn), realm, nonce); } /* Interface function. Parameters are provided by the user, so do * at least some basic checks. */ int mg_send_digest_access_authentication_request(struct mg_connection *conn, const char *realm) { if (conn && conn->dom_ctx) { send_authorization_request(conn, realm); return 0; } return -1; } #if !defined(NO_FILES) static int is_authorized_for_put(struct mg_connection *conn) { if (conn) { struct mg_file file = STRUCT_FILE_INITIALIZER; const char *passfile = conn->dom_ctx->config[PUT_DELETE_PASSWORDS_FILE]; int ret = 0; if (passfile != NULL && mg_fopen(conn, passfile, MG_FOPEN_MODE_READ, &file)) { ret = authorize(conn, &file, NULL); (void)mg_fclose(&file.access); /* ignore error on read only file */ } return ret; } return 0; } #endif int mg_modify_passwords_file(const char *fname, const char *domain, const char *user, const char *pass) { int found, i; char line[512], u[512] = "", d[512] = "", ha1[33], tmp[PATH_MAX + 8]; FILE *fp, *fp2; found = 0; fp = fp2 = NULL; /* Regard empty password as no password - remove user record. */ if ((pass != NULL) && (pass[0] == '\0')) { pass = NULL; } /* Other arguments must not be empty */ if ((fname == NULL) || (domain == NULL) || (user == NULL)) { return 0; } /* Using the given file format, user name and domain must not contain * ':' */ if (strchr(user, ':') != NULL) { return 0; } if (strchr(domain, ':') != NULL) { return 0; } /* Do not allow control characters like newline in user name and domain. * Do not allow excessively long names either. */ for (i = 0; ((i < 255) && (user[i] != 0)); i++) { if (iscntrl((unsigned char)user[i])) { return 0; } } if (user[i]) { return 0; } for (i = 0; ((i < 255) && (domain[i] != 0)); i++) { if (iscntrl((unsigned char)domain[i])) { return 0; } } if (domain[i]) { return 0; } /* The maximum length of the path to the password file is limited */ if ((strlen(fname) + 4) >= PATH_MAX) { return 0; } /* Create a temporary file name. Length has been checked before. */ strcpy(tmp, fname); strcat(tmp, ".tmp"); /* Create the file if does not exist */ /* Use of fopen here is OK, since fname is only ASCII */ if ((fp = fopen(fname, "a+")) != NULL) { (void)fclose(fp); } /* Open the given file and temporary file */ if ((fp = fopen(fname, "r")) == NULL) { return 0; } else if ((fp2 = fopen(tmp, "w+")) == NULL) { fclose(fp); return 0; } /* Copy the stuff to temporary file */ while (fgets(line, sizeof(line), fp) != NULL) { if (sscanf(line, "%255[^:]:%255[^:]:%*s", u, d) != 2) { continue; } u[255] = 0; d[255] = 0; if (!strcmp(u, user) && !strcmp(d, domain)) { found++; if (pass != NULL) { mg_md5(ha1, user, ":", domain, ":", pass, NULL); fprintf(fp2, "%s:%s:%s\n", user, domain, ha1); } } else { fprintf(fp2, "%s", line); } } /* If new user, just add it */ if (!found && (pass != NULL)) { mg_md5(ha1, user, ":", domain, ":", pass, NULL); fprintf(fp2, "%s:%s:%s\n", user, domain, ha1); } /* Close files */ fclose(fp); fclose(fp2); /* Put the temp file in place of real file */ IGNORE_UNUSED_RESULT(remove(fname)); IGNORE_UNUSED_RESULT(rename(tmp, fname)); return 1; } static int is_valid_port(unsigned long port) { return (port <= 0xffff); } static int mg_inet_pton(int af, const char *src, void *dst, size_t dstlen) { struct addrinfo hints, *res, *ressave; int func_ret = 0; int gai_ret; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = af; gai_ret = getaddrinfo(src, NULL, &hints, &res); if (gai_ret != 0) { /* gai_strerror could be used to convert gai_ret to a string */ /* POSIX return values: see * http://pubs.opengroup.org/onlinepubs/9699919799/functions/freeaddrinfo.html */ /* Windows return values: see * https://msdn.microsoft.com/en-us/library/windows/desktop/ms738520%28v=vs.85%29.aspx */ return 0; } ressave = res; while (res) { if (dstlen >= (size_t)res->ai_addrlen) { memcpy(dst, res->ai_addr, res->ai_addrlen); func_ret = 1; } res = res->ai_next; } freeaddrinfo(ressave); return func_ret; } static int connect_socket(struct mg_context *ctx /* may be NULL */, const char *host, int port, int use_ssl, char *ebuf, size_t ebuf_len, SOCKET *sock /* output: socket, must not be NULL */, union usa *sa /* output: socket address, must not be NULL */ ) { int ip_ver = 0; int conn_ret = -1; int sockerr = 0; *sock = INVALID_SOCKET; memset(sa, 0, sizeof(*sa)); if (ebuf_len > 0) { *ebuf = 0; } if (host == NULL) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "NULL host"); return 0; } if ((port <= 0) || !is_valid_port((unsigned)port)) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "invalid port"); return 0; } #if !defined(NO_SSL) #if !defined(NO_SSL_DL) #if defined(OPENSSL_API_1_1) if (use_ssl && (TLS_client_method == NULL)) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "SSL is not initialized"); return 0; } #else if (use_ssl && (SSLv23_client_method == NULL)) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "SSL is not initialized"); return 0; } #endif /* OPENSSL_API_1_1 */ #else (void)use_ssl; #endif /* NO_SSL_DL */ #else (void)use_ssl; #endif /* !defined(NO_SSL) */ if (mg_inet_pton(AF_INET, host, &sa->sin, sizeof(sa->sin))) { sa->sin.sin_family = AF_INET; sa->sin.sin_port = htons((uint16_t)port); ip_ver = 4; #if defined(USE_IPV6) } else if (mg_inet_pton(AF_INET6, host, &sa->sin6, sizeof(sa->sin6))) { sa->sin6.sin6_family = AF_INET6; sa->sin6.sin6_port = htons((uint16_t)port); ip_ver = 6; } else if (host[0] == '[') { /* While getaddrinfo on Windows will work with [::1], * getaddrinfo on Linux only works with ::1 (without []). */ size_t l = strlen(host + 1); char *h = (l > 1) ? mg_strdup_ctx(host + 1, ctx) : NULL; if (h) { h[l - 1] = 0; if (mg_inet_pton(AF_INET6, h, &sa->sin6, sizeof(sa->sin6))) { sa->sin6.sin6_family = AF_INET6; sa->sin6.sin6_port = htons((uint16_t)port); ip_ver = 6; } mg_free(h); } #endif } if (ip_ver == 0) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "host not found"); return 0; } if (ip_ver == 4) { *sock = socket(PF_INET, SOCK_STREAM, 0); } #if defined(USE_IPV6) else if (ip_ver == 6) { *sock = socket(PF_INET6, SOCK_STREAM, 0); } #endif if (*sock == INVALID_SOCKET) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "socket(): %s", strerror(ERRNO)); return 0; } if (0 != set_non_blocking_mode(*sock)) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "Cannot set socket to non-blocking: %s", strerror(ERRNO)); closesocket(*sock); *sock = INVALID_SOCKET; return 0; } set_close_on_exec(*sock, NULL, ctx); if (ip_ver == 4) { /* connected with IPv4 */ conn_ret = connect(*sock, (struct sockaddr *)((void *)&sa->sin), sizeof(sa->sin)); } #if defined(USE_IPV6) else if (ip_ver == 6) { /* connected with IPv6 */ conn_ret = connect(*sock, (struct sockaddr *)((void *)&sa->sin6), sizeof(sa->sin6)); } #endif if (conn_ret != 0) { sockerr = ERRNO; } #if defined(_WIN32) if ((conn_ret != 0) && (sockerr == WSAEWOULDBLOCK)) { #else if ((conn_ret != 0) && (sockerr == EINPROGRESS)) { #endif /* Data for getsockopt */ void *psockerr = &sockerr; int ret; #if defined(_WIN32) int len = (int)sizeof(sockerr); #else socklen_t len = (socklen_t)sizeof(sockerr); #endif /* Data for poll */ struct mg_pollfd pfd[1]; int pollres; int ms_wait = 10000; /* 10 second timeout */ int nonstop = 0; /* For a non-blocking socket, the connect sequence is: * 1) call connect (will not block) * 2) wait until the socket is ready for writing (select or poll) * 3) check connection state with getsockopt */ pfd[0].fd = *sock; pfd[0].events = POLLOUT; pollres = mg_poll(pfd, 1, ms_wait, ctx ? &(ctx->stop_flag) : &nonstop); if (pollres != 1) { /* Not connected */ mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "connect(%s:%d): timeout", host, port); closesocket(*sock); *sock = INVALID_SOCKET; return 0; } #if defined(_WIN32) ret = getsockopt(*sock, SOL_SOCKET, SO_ERROR, (char *)psockerr, &len); #else ret = getsockopt(*sock, SOL_SOCKET, SO_ERROR, psockerr, &len); #endif if ((ret == 0) && (sockerr == 0)) { conn_ret = 0; } } if (conn_ret != 0) { /* Not connected */ mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "connect(%s:%d): error %s", host, port, strerror(sockerr)); closesocket(*sock); *sock = INVALID_SOCKET; return 0; } return 1; } int mg_url_encode(const char *src, char *dst, size_t dst_len) { static const char *dont_escape = "._-$,;~()"; static const char *hex = "0123456789abcdef"; char *pos = dst; const char *end = dst + dst_len - 1; for (; ((*src != '\0') && (pos < end)); src++, pos++) { if (isalnum((unsigned char)*src) || (strchr(dont_escape, *src) != NULL)) { *pos = *src; } else if (pos + 2 < end) { pos[0] = '%'; pos[1] = hex[(unsigned char)*src >> 4]; pos[2] = hex[(unsigned char)*src & 0xf]; pos += 2; } else { break; } } *pos = '\0'; return (*src == '\0') ? (int)(pos - dst) : -1; } /* Return 0 on success, non-zero if an error occurs. */ static int print_dir_entry(struct de *de) { size_t namesize, escsize, i; char *href, *esc, *p; char size[64], mod[64]; #if defined(REENTRANT_TIME) struct tm _tm; struct tm *tm = &_tm; #else struct tm *tm; #endif /* Estimate worst case size for encoding and escaping */ namesize = strlen(de->file_name) + 1; escsize = de->file_name[strcspn(de->file_name, "&<>")] ? namesize * 5 : 0; href = (char *)mg_malloc(namesize * 3 + escsize); if (href == NULL) { return -1; } mg_url_encode(de->file_name, href, namesize * 3); esc = NULL; if (escsize > 0) { /* HTML escaping needed */ esc = href + namesize * 3; for (i = 0, p = esc; de->file_name[i]; i++, p += strlen(p)) { mg_strlcpy(p, de->file_name + i, 2); if (*p == '&') { strcpy(p, "&"); } else if (*p == '<') { strcpy(p, "<"); } else if (*p == '>') { strcpy(p, ">"); } } } if (de->file.is_directory) { mg_snprintf(de->conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%s", "[DIRECTORY]"); } else { /* We use (signed) cast below because MSVC 6 compiler cannot * convert unsigned __int64 to double. Sigh. */ if (de->file.size < 1024) { mg_snprintf(de->conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%d", (int)de->file.size); } else if (de->file.size < 0x100000) { mg_snprintf(de->conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%.1fk", (double)de->file.size / 1024.0); } else if (de->file.size < 0x40000000) { mg_snprintf(de->conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%.1fM", (double)de->file.size / 1048576); } else { mg_snprintf(de->conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%.1fG", (double)de->file.size / 1073741824); } } /* Note: mg_snprintf will not cause a buffer overflow above. * So, string truncation checks are not required here. */ #if defined(REENTRANT_TIME) localtime_r(&de->file.last_modified, tm); #else tm = localtime(&de->file.last_modified); #endif if (tm != NULL) { strftime(mod, sizeof(mod), "%d-%b-%Y %H:%M", tm); } else { mg_strlcpy(mod, "01-Jan-1970 00:00", sizeof(mod)); mod[sizeof(mod) - 1] = '\0'; } mg_printf(de->conn, "%s%s" " %s  %s\n", href, de->file.is_directory ? "/" : "", esc ? esc : de->file_name, de->file.is_directory ? "/" : "", mod, size); mg_free(href); return 0; } /* This function is called from send_directory() and used for * sorting directory entries by size, or name, or modification time. * On windows, __cdecl specification is needed in case if project is built * with __stdcall convention. qsort always requires __cdels callback. */ static int WINCDECL compare_dir_entries(const void *p1, const void *p2) { if (p1 && p2) { const struct de *a = (const struct de *)p1, *b = (const struct de *)p2; const char *query_string = a->conn->request_info.query_string; int cmp_result = 0; if ((query_string == NULL) || (query_string[0] == '\0')) { query_string = "n"; } if (a->file.is_directory && !b->file.is_directory) { return -1; /* Always put directories on top */ } else if (!a->file.is_directory && b->file.is_directory) { return 1; /* Always put directories on top */ } else if (*query_string == 'n') { cmp_result = strcmp(a->file_name, b->file_name); } else if (*query_string == 's') { cmp_result = (a->file.size == b->file.size) ? 0 : ((a->file.size > b->file.size) ? 1 : -1); } else if (*query_string == 'd') { cmp_result = (a->file.last_modified == b->file.last_modified) ? 0 : ((a->file.last_modified > b->file.last_modified) ? 1 : -1); } return (query_string[1] == 'd') ? -cmp_result : cmp_result; } return 0; } static int must_hide_file(struct mg_connection *conn, const char *path) { if (conn && conn->dom_ctx) { const char *pw_pattern = "**" PASSWORDS_FILE_NAME "$"; const char *pattern = conn->dom_ctx->config[HIDE_FILES]; return (match_prefix(pw_pattern, strlen(pw_pattern), path) > 0) || ((pattern != NULL) && (match_prefix(pattern, strlen(pattern), path) > 0)); } return 0; } #if !defined(NO_FILESYSTEMS) static int scan_directory(struct mg_connection *conn, const char *dir, void *data, int (*cb)(struct de *, void *)) { char path[PATH_MAX]; struct dirent *dp; DIR *dirp; struct de de; int truncated; if ((dirp = mg_opendir(conn, dir)) == NULL) { return 0; } else { de.conn = conn; while ((dp = mg_readdir(dirp)) != NULL) { /* Do not show current dir and hidden files */ if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..") || must_hide_file(conn, dp->d_name)) { continue; } mg_snprintf( conn, &truncated, path, sizeof(path), "%s/%s", dir, dp->d_name); /* If we don't memset stat structure to zero, mtime will have * garbage and strftime() will segfault later on in * print_dir_entry(). memset is required only if mg_stat() * fails. For more details, see * http://code.google.com/p/mongoose/issues/detail?id=79 */ memset(&de.file, 0, sizeof(de.file)); if (truncated) { /* If the path is not complete, skip processing. */ continue; } if (!mg_stat(conn, path, &de.file)) { mg_cry_internal(conn, "%s: mg_stat(%s) failed: %s", __func__, path, strerror(ERRNO)); } de.file_name = dp->d_name; cb(&de, data); } (void)mg_closedir(dirp); } return 1; } #endif /* NO_FILESYSTEMS */ #if !defined(NO_FILES) static int remove_directory(struct mg_connection *conn, const char *dir) { char path[PATH_MAX]; struct dirent *dp; DIR *dirp; struct de de; int truncated; int ok = 1; if ((dirp = mg_opendir(conn, dir)) == NULL) { return 0; } else { de.conn = conn; while ((dp = mg_readdir(dirp)) != NULL) { /* Do not show current dir (but show hidden files as they will * also be removed) */ if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) { continue; } mg_snprintf( conn, &truncated, path, sizeof(path), "%s/%s", dir, dp->d_name); /* If we don't memset stat structure to zero, mtime will have * garbage and strftime() will segfault later on in * print_dir_entry(). memset is required only if mg_stat() * fails. For more details, see * http://code.google.com/p/mongoose/issues/detail?id=79 */ memset(&de.file, 0, sizeof(de.file)); if (truncated) { /* Do not delete anything shorter */ ok = 0; continue; } if (!mg_stat(conn, path, &de.file)) { mg_cry_internal(conn, "%s: mg_stat(%s) failed: %s", __func__, path, strerror(ERRNO)); ok = 0; } if (de.file.is_directory) { if (remove_directory(conn, path) == 0) { ok = 0; } } else { /* This will fail file is the file is in memory */ if (mg_remove(conn, path) == 0) { ok = 0; } } } (void)mg_closedir(dirp); IGNORE_UNUSED_RESULT(rmdir(dir)); } return ok; } #endif struct dir_scan_data { struct de *entries; unsigned int num_entries; unsigned int arr_size; }; /* Behaves like realloc(), but frees original pointer on failure */ static void * realloc2(void *ptr, size_t size) { void *new_ptr = mg_realloc(ptr, size); if ((new_ptr == NULL) && (size > 0)) { mg_free(ptr); } return new_ptr; } #if !defined(NO_FILESYSTEMS) static int dir_scan_callback(struct de *de, void *data) { struct dir_scan_data *dsd = (struct dir_scan_data *)data; if ((dsd->entries == NULL) || (dsd->num_entries >= dsd->arr_size)) { dsd->arr_size *= 2; dsd->entries = (struct de *)realloc2(dsd->entries, dsd->arr_size * sizeof(dsd->entries[0])); } if (dsd->entries == NULL) { /* TODO(lsm, low): propagate an error to the caller */ dsd->num_entries = 0; } else { dsd->entries[dsd->num_entries].file_name = mg_strdup(de->file_name); dsd->entries[dsd->num_entries].file = de->file; dsd->entries[dsd->num_entries].conn = de->conn; dsd->num_entries++; } return 0; } static void handle_directory_request(struct mg_connection *conn, const char *dir) { unsigned int i; int sort_direction; struct dir_scan_data data = {NULL, 0, 128}; char date[64], *esc, *p; const char *title; time_t curtime = time(NULL); if (!scan_directory(conn, dir, &data, dir_scan_callback)) { mg_send_http_error(conn, 500, "Error: Cannot open directory\nopendir(%s): %s", dir, strerror(ERRNO)); return; } gmt_time_string(date, sizeof(date), &curtime); if (!conn) { return; } esc = NULL; title = conn->request_info.local_uri; if (title[strcspn(title, "&<>")]) { /* HTML escaping needed */ esc = (char *)mg_malloc(strlen(title) * 5 + 1); if (esc) { for (i = 0, p = esc; title[i]; i++, p += strlen(p)) { mg_strlcpy(p, title + i, 2); if (*p == '&') { strcpy(p, "&"); } else if (*p == '<') { strcpy(p, "<"); } else if (*p == '>') { strcpy(p, ">"); } } } else { title = ""; } } sort_direction = ((conn->request_info.query_string != NULL) && (conn->request_info.query_string[0] != '\0') && (conn->request_info.query_string[1] == 'd')) ? 'a' : 'd'; conn->must_close = 1; mg_printf(conn, "HTTP/1.1 200 OK\r\n"); send_static_cache_header(conn); send_additional_header(conn); mg_printf(conn, "Date: %s\r\n" "Connection: close\r\n" "Content-Type: text/html; charset=utf-8\r\n\r\n", date); mg_printf(conn, "Index of %s" "" "

Index of %s

"
	          ""
	          ""
	          ""
	          "",
	          esc ? esc : title,
	          esc ? esc : title,
	          sort_direction,
	          sort_direction,
	          sort_direction);
	mg_free(esc);

	/* Print first entry - link to a parent directory */
	mg_printf(conn,
	          ""
	          "\n",
	          "..",
	          "Parent directory",
	          "-",
	          "-");

	/* Sort and print directory entries */
	if (data.entries != NULL) {
		qsort(data.entries,
		      (size_t)data.num_entries,
		      sizeof(data.entries[0]),
		      compare_dir_entries);
		for (i = 0; i < data.num_entries; i++) {
			print_dir_entry(&data.entries[i]);
			mg_free(data.entries[i].file_name);
		}
		mg_free(data.entries);
	}

	mg_printf(conn, "%s", "
NameModifiedSize

%s %s  %s
"); conn->status_code = 200; } #endif /* NO_FILESYSTEMS */ /* Send len bytes from the opened file to the client. */ static void send_file_data(struct mg_connection *conn, struct mg_file *filep, int64_t offset, int64_t len) { char buf[MG_BUF_LEN]; int to_read, num_read, num_written; int64_t size; if (!filep || !conn) { return; } /* Sanity check the offset */ size = (filep->stat.size > INT64_MAX) ? INT64_MAX : (int64_t)(filep->stat.size); offset = (offset < 0) ? 0 : ((offset > size) ? size : offset); #if defined(MG_USE_OPEN_FILE) if ((len > 0) && (filep->access.membuf != NULL) && (size > 0)) { /* file stored in memory */ if (len > size - offset) { len = size - offset; } mg_write(conn, filep->access.membuf + offset, (size_t)len); } else /* else block below */ #endif if (len > 0 && filep->access.fp != NULL) { /* file stored on disk */ #if defined(__linux__) /* sendfile is only available for Linux */ if ((conn->ssl == 0) && (conn->throttle == 0) && (!mg_strcasecmp(conn->dom_ctx->config[ALLOW_SENDFILE_CALL], "yes"))) { off_t sf_offs = (off_t)offset; ssize_t sf_sent; int sf_file = fileno(filep->access.fp); int loop_cnt = 0; do { /* 2147479552 (0x7FFFF000) is a limit found by experiment on * 64 bit Linux (2^31 minus one memory page of 4k?). */ size_t sf_tosend = (size_t)((len < 0x7FFFF000) ? len : 0x7FFFF000); sf_sent = sendfile(conn->client.sock, sf_file, &sf_offs, sf_tosend); if (sf_sent > 0) { len -= sf_sent; offset += sf_sent; } else if (loop_cnt == 0) { /* This file can not be sent using sendfile. * This might be the case for pseudo-files in the * /sys/ and /proc/ file system. * Use the regular user mode copy code instead. */ break; } else if (sf_sent == 0) { /* No error, but 0 bytes sent. May be EOF? */ return; } loop_cnt++; } while ((len > 0) && (sf_sent >= 0)); if (sf_sent > 0) { return; /* OK */ } /* sf_sent<0 means error, thus fall back to the classic way */ /* This is always the case, if sf_file is not a "normal" file, * e.g., for sending data from the output of a CGI process. */ offset = (int64_t)sf_offs; } #endif if ((offset > 0) && (fseeko(filep->access.fp, offset, SEEK_SET) != 0)) { mg_cry_internal(conn, "%s: fseeko() failed: %s", __func__, strerror(ERRNO)); mg_send_http_error( conn, 500, "%s", "Error: Unable to access file at requested position."); } else { while (len > 0) { /* Calculate how much to read from the file in the buffer */ to_read = sizeof(buf); if ((int64_t)to_read > len) { to_read = (int)len; } /* Read from file, exit the loop on error */ if ((num_read = (int)fread(buf, 1, (size_t)to_read, filep->access.fp)) <= 0) { break; } /* Send read bytes to the client, exit the loop on error */ if ((num_written = mg_write(conn, buf, (size_t)num_read)) != num_read) { break; } /* Both read and were successful, adjust counters */ len -= num_written; } } } } static int parse_range_header(const char *header, int64_t *a, int64_t *b) { /* This is an improper workaround of the crazyness that mingw/windows does around readinf int64_t*/ int xa, xb; int ret = sscanf(header, "bytes=%d-%d", &xa, &xb); *a = xa, *b = xb; return ret; } static void construct_etag(char *buf, size_t buf_len, const struct mg_file_stat *filestat) { if ((filestat != NULL) && (buf != NULL)) { mg_snprintf(NULL, NULL, /* All calls to construct_etag use 64 byte buffer */ buf, buf_len, "\"%lx.%" INT64_FMT "\"", (unsigned long)filestat->last_modified, filestat->size); } } static void fclose_on_exec(struct mg_file_access *filep, struct mg_connection *conn) { if (filep != NULL && filep->fp != NULL) { #if defined(_WIN32) (void)conn; /* Unused. */ #else if (fcntl(fileno(filep->fp), F_SETFD, FD_CLOEXEC) != 0) { mg_cry_internal(conn, "%s: fcntl(F_SETFD FD_CLOEXEC) failed: %s", __func__, strerror(ERRNO)); } #endif } } #if defined(USE_ZLIB) #include "mod_zlib.inl" #endif #if !defined(NO_FILESYSTEMS) static void handle_static_file_request(struct mg_connection *conn, const char *path, struct mg_file *filep, const char *mime_type, const char *additional_headers) { char date[64], lm[64], etag[64]; char range[128]; /* large enough, so there will be no overflow */ const char *msg = "OK"; const char *range_hdr; time_t curtime = time(NULL); int64_t cl, r1, r2; struct vec mime_vec; int n, truncated; char gz_path[PATH_MAX]; const char *encoding = ""; const char *origin_hdr; const char *cors_orig_cfg; const char *cors1, *cors2, *cors3; int is_head_request; #if defined(USE_ZLIB) /* Compression is allowed, unless there is a reason not to use compression. * If the file is already compressed, too small or a "range" request was * made, on the fly compression is not possible. */ int allow_on_the_fly_compression = 1; #endif if ((conn == NULL) || (conn->dom_ctx == NULL) || (filep == NULL)) { return; } is_head_request = !strcmp(conn->request_info.request_method, "HEAD"); if (mime_type == NULL) { get_mime_type(conn, path, &mime_vec); } else { mime_vec.ptr = mime_type; mime_vec.len = strlen(mime_type); } if (filep->stat.size > INT64_MAX) { mg_send_http_error(conn, 500, "Error: File size is too large to send\n%" INT64_FMT, filep->stat.size); return; } cl = (int64_t)filep->stat.size; conn->status_code = 200; range[0] = '\0'; #if defined(USE_ZLIB) /* if this file is in fact a pre-gzipped file, rewrite its filename * it's important to rewrite the filename after resolving * the mime type from it, to preserve the actual file's type */ if (!conn->accept_gzip) { allow_on_the_fly_compression = 0; } #endif /* Check if there is a range header */ range_hdr = mg_get_header(conn, "Range"); /* For gzipped files, add *.gz */ if (filep->stat.is_gzipped) { mg_snprintf(conn, &truncated, gz_path, sizeof(gz_path), "%s.gz", path); if (truncated) { mg_send_http_error(conn, 500, "Error: Path of zipped file too long (%s)", path); return; } path = gz_path; encoding = "Content-Encoding: gzip\r\n"; #if defined(USE_ZLIB) /* File is already compressed. No "on the fly" compression. */ allow_on_the_fly_compression = 0; #endif } else if ((conn->accept_gzip) && (range_hdr == NULL) && (filep->stat.size >= MG_FILE_COMPRESSION_SIZE_LIMIT)) { struct mg_file_stat file_stat; mg_snprintf(conn, &truncated, gz_path, sizeof(gz_path), "%s.gz", path); if (!truncated && mg_stat(conn, gz_path, &file_stat) && !file_stat.is_directory) { file_stat.is_gzipped = 1; filep->stat = file_stat; cl = (int64_t)filep->stat.size; path = gz_path; encoding = "Content-Encoding: gzip\r\n"; #if defined(USE_ZLIB) /* File is already compressed. No "on the fly" compression. */ allow_on_the_fly_compression = 0; #endif } } if (!mg_fopen(conn, path, MG_FOPEN_MODE_READ, filep)) { mg_send_http_error(conn, 500, "Error: Cannot open file\nfopen(%s): %s", path, strerror(ERRNO)); return; } fclose_on_exec(&filep->access, conn); /* If "Range" request was made: parse header, send only selected part * of the file. */ r1 = r2 = 0; if ((range_hdr != NULL) && ((n = parse_range_header(range_hdr, &r1, &r2)) > 0) && (r1 >= 0) && (r2 >= 0)) { /* actually, range requests don't play well with a pre-gzipped * file (since the range is specified in the uncompressed space) */ if (filep->stat.is_gzipped) { mg_send_http_error( conn, 416, /* 416 = Range Not Satisfiable */ "%s", "Error: Range requests in gzipped files are not supported"); (void)mg_fclose( &filep->access); /* ignore error on read only file */ return; } conn->status_code = 206; cl = (n == 2) ? (((r2 > cl) ? cl : r2) - r1 + 1) : (cl - r1); mg_snprintf(conn, NULL, /* range buffer is big enough */ range, sizeof(range), "Content-Range: bytes " "%" INT64_FMT "-%" INT64_FMT "/%" INT64_FMT "\r\n", r1, r1 + cl - 1, filep->stat.size); msg = "Partial Content"; #if defined(USE_ZLIB) /* Do not compress ranges. */ allow_on_the_fly_compression = 0; #endif } /* Do not compress small files. Small files do not benefit from file * compression, but there is still some overhead. */ #if defined(USE_ZLIB) if (filep->stat.size < MG_FILE_COMPRESSION_SIZE_LIMIT) { /* File is below the size limit. */ allow_on_the_fly_compression = 0; } #endif /* Standard CORS header */ cors_orig_cfg = conn->dom_ctx->config[ACCESS_CONTROL_ALLOW_ORIGIN]; origin_hdr = mg_get_header(conn, "Origin"); if (cors_orig_cfg && *cors_orig_cfg && origin_hdr) { /* Cross-origin resource sharing (CORS), see * http://www.html5rocks.com/en/tutorials/cors/, * http://www.html5rocks.com/static/images/cors_server_flowchart.png * - * preflight is not supported for files. */ cors1 = "Access-Control-Allow-Origin: "; cors2 = cors_orig_cfg; cors3 = "\r\n"; } else { cors1 = cors2 = cors3 = ""; } /* Prepare Etag, Date, Last-Modified headers. Must be in UTC, * according to * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 */ gmt_time_string(date, sizeof(date), &curtime); gmt_time_string(lm, sizeof(lm), &filep->stat.last_modified); construct_etag(etag, sizeof(etag), &filep->stat); /* Send header */ (void)mg_printf(conn, "HTTP/1.1 %d %s\r\n" "%s%s%s" /* CORS */ "Date: %s\r\n" "Last-Modified: %s\r\n" "Etag: %s\r\n" "Content-Type: %.*s\r\n" "Connection: %s\r\n", conn->status_code, msg, cors1, cors2, cors3, date, lm, etag, (int)mime_vec.len, mime_vec.ptr, suggest_connection_header(conn)); send_static_cache_header(conn); send_additional_header(conn); #if defined(USE_ZLIB) /* On the fly compression allowed */ if (allow_on_the_fly_compression) { /* For on the fly compression, we don't know the content size in * advance, so we have to use chunked encoding */ (void)mg_printf(conn, "Content-Encoding: gzip\r\n" "Transfer-Encoding: chunked\r\n"); } else #endif { /* Without on-the-fly compression, we know the content-length * and we can use ranges (with on-the-fly compression we cannot). * So we send these response headers only in this case. */ (void)mg_printf(conn, "Content-Length: %" INT64_FMT "\r\n" "Accept-Ranges: bytes\r\n" "%s" /* range */ "%s" /* encoding */, cl, range, encoding); } /* The previous code must not add any header starting with X- to make * sure no one of the additional_headers is included twice */ if (additional_headers != NULL) { (void)mg_printf(conn, "%.*s\r\n\r\n", (int)strlen(additional_headers), additional_headers); } else { (void)mg_printf(conn, "\r\n"); } if (!is_head_request) { #if defined(USE_ZLIB) if (allow_on_the_fly_compression) { /* Compress and send */ send_compressed_data(conn, filep); } else #endif { /* Send file directly */ send_file_data(conn, filep, r1, cl); } } (void)mg_fclose(&filep->access); /* ignore error on read only file */ } int mg_send_file_body(struct mg_connection *conn, const char *path) { struct mg_file file = STRUCT_FILE_INITIALIZER; if (!mg_fopen(conn, path, MG_FOPEN_MODE_READ, &file)) { return -1; } fclose_on_exec(&file.access, conn); send_file_data(conn, &file, 0, INT64_MAX); (void)mg_fclose(&file.access); /* Ignore errors for readonly files */ return 0; /* >= 0 for OK */ } #endif /* NO_FILESYSTEMS */ #if !defined(NO_CACHING) /* Return True if we should reply 304 Not Modified. */ static int is_not_modified(const struct mg_connection *conn, const struct mg_file_stat *filestat) { char etag[64]; const char *ims = mg_get_header(conn, "If-Modified-Since"); const char *inm = mg_get_header(conn, "If-None-Match"); construct_etag(etag, sizeof(etag), filestat); return ((inm != NULL) && !mg_strcasecmp(etag, inm)) || ((ims != NULL) && (filestat->last_modified <= parse_date_string(ims))); } static void handle_not_modified_static_file_request(struct mg_connection *conn, struct mg_file *filep) { char date[64], lm[64], etag[64]; time_t curtime = time(NULL); if ((conn == NULL) || (filep == NULL)) { return; } conn->status_code = 304; gmt_time_string(date, sizeof(date), &curtime); gmt_time_string(lm, sizeof(lm), &filep->stat.last_modified); construct_etag(etag, sizeof(etag), &filep->stat); (void)mg_printf(conn, "HTTP/1.1 %d %s\r\n" "Date: %s\r\n", conn->status_code, mg_get_response_code_text(conn, conn->status_code), date); send_static_cache_header(conn); send_additional_header(conn); (void)mg_printf(conn, "Last-Modified: %s\r\n" "Etag: %s\r\n" "Connection: %s\r\n" "\r\n", lm, etag, suggest_connection_header(conn)); } #endif #if !defined(NO_FILESYSTEMS) void mg_send_file(struct mg_connection *conn, const char *path) { mg_send_mime_file2(conn, path, NULL, NULL); } void mg_send_mime_file(struct mg_connection *conn, const char *path, const char *mime_type) { mg_send_mime_file2(conn, path, mime_type, NULL); } void mg_send_mime_file2(struct mg_connection *conn, const char *path, const char *mime_type, const char *additional_headers) { struct mg_file file = STRUCT_FILE_INITIALIZER; if (!conn) { /* No conn */ return; } if (mg_stat(conn, path, &file.stat)) { #if !defined(NO_CACHING) if (is_not_modified(conn, &file.stat)) { /* Send 304 "Not Modified" - this must not send any body data */ handle_not_modified_static_file_request(conn, &file); } else #endif /* NO_CACHING */ if (file.stat.is_directory) { if (!mg_strcasecmp(conn->dom_ctx->config[ENABLE_DIRECTORY_LISTING], "yes")) { handle_directory_request(conn, path); } else { mg_send_http_error(conn, 403, "%s", "Error: Directory listing denied"); } } else { handle_static_file_request( conn, path, &file, mime_type, additional_headers); } } else { mg_send_http_error(conn, 404, "%s", "Error: File not found"); } } /* For a given PUT path, create all intermediate subdirectories. * Return 0 if the path itself is a directory. * Return 1 if the path leads to a file. * Return -1 for if the path is too long. * Return -2 if path can not be created. */ static int put_dir(struct mg_connection *conn, const char *path) { char buf[PATH_MAX]; const char *s, *p; struct mg_file file = STRUCT_FILE_INITIALIZER; size_t len; int res = 1; for (s = p = path + 2; (p = strchr(s, '/')) != NULL; s = ++p) { len = (size_t)(p - path); if (len >= sizeof(buf)) { /* path too long */ res = -1; break; } memcpy(buf, path, len); buf[len] = '\0'; /* Try to create intermediate directory */ DEBUG_TRACE("mkdir(%s)", buf); if (!mg_stat(conn, buf, &file.stat) && mg_mkdir(conn, buf, 0755) != 0) { /* path does not exixt and can not be created */ res = -2; break; } /* Is path itself a directory? */ if (p[1] == '\0') { res = 0; } } return res; } static void remove_bad_file(const struct mg_connection *conn, const char *path) { int r = mg_remove(conn, path); if (r != 0) { mg_cry_internal(conn, "%s: Cannot remove invalid file %s", __func__, path); } } long long mg_store_body(struct mg_connection *conn, const char *path) { char buf[MG_BUF_LEN]; long long len = 0; int ret, n; struct mg_file fi; if (conn->consumed_content != 0) { mg_cry_internal(conn, "%s: Contents already consumed", __func__); return -11; } ret = put_dir(conn, path); if (ret < 0) { /* -1 for path too long, * -2 for path can not be created. */ return ret; } if (ret != 1) { /* Return 0 means, path itself is a directory. */ return 0; } if (mg_fopen(conn, path, MG_FOPEN_MODE_WRITE, &fi) == 0) { return -12; } ret = mg_read(conn, buf, sizeof(buf)); while (ret > 0) { n = (int)fwrite(buf, 1, (size_t)ret, fi.access.fp); if (n != ret) { (void)mg_fclose( &fi.access); /* File is bad and will be removed anyway. */ remove_bad_file(conn, path); return -13; } len += ret; ret = mg_read(conn, buf, sizeof(buf)); } /* File is open for writing. If fclose fails, there was probably an * error flushing the buffer to disk, so the file on disk might be * broken. Delete it and return an error to the caller. */ if (mg_fclose(&fi.access) != 0) { remove_bad_file(conn, path); return -14; } return len; } #endif /* NO_FILESYSTEMS */ /* Parse a buffer: * Forward the string pointer till the end of a word, then * terminate it and forward till the begin of the next word. */ static int skip_to_end_of_word_and_terminate(char **ppw, int eol) { /* Forward until a space is found - use isgraph here */ /* See http://www.cplusplus.com/reference/cctype/ */ while (isgraph((unsigned char)**ppw)) { (*ppw)++; } /* Check end of word */ if (eol) { /* must be a end of line */ if ((**ppw != '\r') && (**ppw != '\n')) { return -1; } } else { /* must be a end of a word, but not a line */ if (**ppw != ' ') { return -1; } } /* Terminate and forward to the next word */ do { **ppw = 0; (*ppw)++; } while (isspace((unsigned char)**ppw)); /* Check after term */ if (!eol) { /* if it's not the end of line, there must be a next word */ if (!isgraph((unsigned char)**ppw)) { return -1; } } /* ok */ return 1; } /* Parse HTTP headers from the given buffer, advance buf pointer * to the point where parsing stopped. * All parameters must be valid pointers (not NULL). * Return <0 on error. */ static int parse_http_headers(char **buf, struct mg_header hdr[MG_MAX_HEADERS]) { int i; int num_headers = 0; for (i = 0; i < (int)MG_MAX_HEADERS; i++) { char *dp = *buf; /* Skip all ASCII characters (>SPACE, <127), to find a ':' */ while ((*dp != ':') && (*dp >= 33) && (*dp <= 126)) { dp++; } if (dp == *buf) { /* End of headers reached. */ break; } if (*dp != ':') { /* This is not a valid field. */ return -1; } /* End of header key (*dp == ':') */ /* Truncate here and set the key name */ *dp = 0; hdr[i].name = *buf; /* Skip all spaces */ do { dp++; } while ((*dp == ' ') || (*dp == '\t')); /* The rest of the line is the value */ hdr[i].value = dp; /* Find end of line */ while ((*dp != 0) && (*dp != '\r') && (*dp != '\n')) { dp++; }; /* eliminate \r */ if (*dp == '\r') { *dp = 0; dp++; if (*dp != '\n') { /* This is not a valid line. */ return -1; } } /* here *dp is either 0 or '\n' */ /* in any case, we have a new header */ num_headers = i + 1; if (*dp) { *dp = 0; dp++; *buf = dp; if ((dp[0] == '\r') || (dp[0] == '\n')) { /* This is the end of the header */ break; } } else { *buf = dp; break; } } return num_headers; } struct mg_http_method_info { const char *name; int request_has_body; int response_has_body; int is_safe; int is_idempotent; int is_cacheable; }; /* https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods */ static struct mg_http_method_info http_methods[] = { /* HTTP (RFC 2616) */ {"GET", 0, 1, 1, 1, 1}, {"POST", 1, 1, 0, 0, 0}, {"PUT", 1, 0, 0, 1, 0}, {"DELETE", 0, 0, 0, 1, 0}, {"HEAD", 0, 0, 1, 1, 1}, {"OPTIONS", 0, 0, 1, 1, 0}, {"CONNECT", 1, 1, 0, 0, 0}, /* TRACE method (RFC 2616) is not supported for security reasons */ /* PATCH method (RFC 5789) */ {"PATCH", 1, 0, 0, 0, 0}, /* PATCH method only allowed for CGI/Lua/LSP and callbacks. */ /* WEBDAV (RFC 2518) */ {"PROPFIND", 0, 1, 1, 1, 0}, /* http://www.webdav.org/specs/rfc4918.html, 9.1: * Some PROPFIND results MAY be cached, with care, * as there is no cache validation mechanism for * most properties. This method is both safe and * idempotent (see Section 9.1 of [RFC2616]). */ {"MKCOL", 0, 0, 0, 1, 0}, /* http://www.webdav.org/specs/rfc4918.html, 9.1: * When MKCOL is invoked without a request body, * the newly created collection SHOULD have no * members. A MKCOL request message may contain * a message body. The precise behavior of a MKCOL * request when the body is present is undefined, * ... ==> We do not support MKCOL with body data. * This method is idempotent, but not safe (see * Section 9.1 of [RFC2616]). Responses to this * method MUST NOT be cached. */ /* Unsupported WEBDAV Methods: */ /* PROPPATCH, COPY, MOVE, LOCK, UNLOCK (RFC 2518) */ /* + 11 methods from RFC 3253 */ /* ORDERPATCH (RFC 3648) */ /* ACL (RFC 3744) */ /* SEARCH (RFC 5323) */ /* + MicroSoft extensions * https://msdn.microsoft.com/en-us/library/aa142917.aspx */ /* REPORT method (RFC 3253) */ {"REPORT", 1, 1, 1, 1, 1}, /* REPORT method only allowed for CGI/Lua/LSP and callbacks. */ /* It was defined for WEBDAV in RFC 3253, Sec. 3.6 * (https://tools.ietf.org/html/rfc3253#section-3.6), but seems * to be useful for REST in case a "GET request with body" is * required. */ {NULL, 0, 0, 0, 0, 0} /* end of list */ }; static const struct mg_http_method_info * get_http_method_info(const char *method) { /* Check if the method is known to the server. The list of all known * HTTP methods can be found here at * http://www.iana.org/assignments/http-methods/http-methods.xhtml */ const struct mg_http_method_info *m = http_methods; while (m->name) { if (!strcmp(m->name, method)) { return m; } m++; } return NULL; } static int is_valid_http_method(const char *method) { return (get_http_method_info(method) != NULL); } /* Parse HTTP request, fill in mg_request_info structure. * This function modifies the buffer by NUL-terminating * HTTP request components, header names and header values. * Parameters: * buf (in/out): pointer to the HTTP header to parse and split * len (in): length of HTTP header buffer * re (out): parsed header as mg_request_info * buf and ri must be valid pointers (not NULL), len>0. * Returns <0 on error. */ static int parse_http_request(char *buf, int len, struct mg_request_info *ri) { int request_length; int init_skip = 0; /* Reset attributes. DO NOT TOUCH is_ssl, remote_addr, * remote_port */ ri->remote_user = ri->request_method = ri->request_uri = ri->http_version = NULL; ri->num_headers = 0; /* RFC says that all initial whitespaces should be ingored */ /* This included all leading \r and \n (isspace) */ /* See table: http://www.cplusplus.com/reference/cctype/ */ while ((len > 0) && isspace((unsigned char)*buf)) { buf++; len--; init_skip++; } if (len == 0) { /* Incomplete request */ return 0; } /* Control characters are not allowed, including zero */ if (iscntrl((unsigned char)*buf)) { return -1; } /* Find end of HTTP header */ request_length = get_http_header_len(buf, len); if (request_length <= 0) { return request_length; } buf[request_length - 1] = '\0'; if ((*buf == 0) || (*buf == '\r') || (*buf == '\n')) { return -1; } /* The first word has to be the HTTP method */ ri->request_method = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } /* Check for a valid http method */ if (!is_valid_http_method(ri->request_method)) { return -1; } /* The second word is the URI */ ri->request_uri = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } /* Next would be the HTTP version */ ri->http_version = buf; if (skip_to_end_of_word_and_terminate(&buf, 1) <= 0) { return -1; } /* Check for a valid HTTP version key */ if (strncmp(ri->http_version, "HTTP/", 5) != 0) { /* Invalid request */ return -1; } ri->http_version += 5; /* Parse all HTTP headers */ ri->num_headers = parse_http_headers(&buf, ri->http_headers); if (ri->num_headers < 0) { /* Error while parsing headers */ return -1; } return request_length + init_skip; } static int parse_http_response(char *buf, int len, struct mg_response_info *ri) { int response_length; int init_skip = 0; char *tmp, *tmp2; long l; /* Initialize elements. */ ri->http_version = ri->status_text = NULL; ri->num_headers = ri->status_code = 0; /* RFC says that all initial whitespaces should be ingored */ /* This included all leading \r and \n (isspace) */ /* See table: http://www.cplusplus.com/reference/cctype/ */ while ((len > 0) && isspace((unsigned char)*buf)) { buf++; len--; init_skip++; } if (len == 0) { /* Incomplete request */ return 0; } /* Control characters are not allowed, including zero */ if (iscntrl((unsigned char)*buf)) { return -1; } /* Find end of HTTP header */ response_length = get_http_header_len(buf, len); if (response_length <= 0) { return response_length; } buf[response_length - 1] = '\0'; if ((*buf == 0) || (*buf == '\r') || (*buf == '\n')) { return -1; } /* The first word is the HTTP version */ /* Check for a valid HTTP version key */ if (strncmp(buf, "HTTP/", 5) != 0) { /* Invalid request */ return -1; } buf += 5; if (!isgraph((unsigned char)buf[0])) { /* Invalid request */ return -1; } ri->http_version = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } /* The second word is the status as a number */ tmp = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } l = strtol(tmp, &tmp2, 10); if ((l < 100) || (l >= 1000) || ((tmp2 - tmp) != 3) || (*tmp2 != 0)) { /* Everything else but a 3 digit code is invalid */ return -1; } ri->status_code = (int)l; /* The rest of the line is the status text */ ri->status_text = buf; /* Find end of status text */ /* isgraph or isspace = isprint */ while (isprint((unsigned char)*buf)) { buf++; } if ((*buf != '\r') && (*buf != '\n')) { return -1; } /* Terminate string and forward buf to next line */ do { *buf = 0; buf++; } while (isspace((unsigned char)*buf)); /* Parse all HTTP headers */ ri->num_headers = parse_http_headers(&buf, ri->http_headers); if (ri->num_headers < 0) { /* Error while parsing headers */ return -1; } return response_length + init_skip; } /* Keep reading the input (either opened file descriptor fd, or socket sock, * or SSL descriptor ssl) into buffer buf, until \r\n\r\n appears in the * buffer (which marks the end of HTTP request). Buffer buf may already * have some data. The length of the data is stored in nread. * Upon every read operation, increase nread by the number of bytes read. */ static int read_message(FILE *fp, struct mg_connection *conn, char *buf, int bufsiz, int *nread) { int request_len, n = 0; struct timespec last_action_time; double request_timeout; if (!conn) { return 0; } memset(&last_action_time, 0, sizeof(last_action_time)); if (conn->dom_ctx->config[REQUEST_TIMEOUT]) { /* value of request_timeout is in seconds, config in milliseconds */ request_timeout = atof(conn->dom_ctx->config[REQUEST_TIMEOUT]) / 1000.0; } else { request_timeout = -1.0; } if (conn->handled_requests > 0) { if (conn->dom_ctx->config[KEEP_ALIVE_TIMEOUT]) { request_timeout = atof(conn->dom_ctx->config[KEEP_ALIVE_TIMEOUT]) / 1000.0; } } request_len = get_http_header_len(buf, *nread); while (request_len == 0) { /* Full request not yet received */ if (conn->phys_ctx->stop_flag != 0) { /* Server is to be stopped. */ return -1; } if (*nread >= bufsiz) { /* Request too long */ return -2; } n = pull_inner( fp, conn, buf + *nread, bufsiz - *nread, request_timeout); if (n == -2) { /* Receive error */ return -1; } /* update clock after every read request */ clock_gettime(CLOCK_MONOTONIC, &last_action_time); if (n > 0) { *nread += n; request_len = get_http_header_len(buf, *nread); } else { request_len = 0; } if ((request_len == 0) && (request_timeout >= 0)) { if (mg_difftimespec(&last_action_time, &(conn->req_time)) > request_timeout) { /* Timeout */ return -1; } } } return request_len; } #if !defined(NO_CGI) || !defined(NO_FILES) static int forward_body_data(struct mg_connection *conn, FILE *fp, SOCKET sock, SSL *ssl) { const char *expect; char buf[MG_BUF_LEN]; int success = 0; if (!conn) { return 0; } expect = mg_get_header(conn, "Expect"); DEBUG_ASSERT(fp != NULL); if (!fp) { mg_send_http_error(conn, 500, "%s", "Error: NULL File"); return 0; } if ((expect != NULL) && (mg_strcasecmp(expect, "100-continue") != 0)) { /* Client sent an "Expect: xyz" header and xyz is not 100-continue. */ mg_send_http_error(conn, 417, "Error: Can not fulfill expectation"); } else { if (expect != NULL) { (void)mg_printf(conn, "%s", "HTTP/1.1 100 Continue\r\n\r\n"); conn->status_code = 100; } else { conn->status_code = 200; } DEBUG_ASSERT(conn->consumed_content == 0); if (conn->consumed_content != 0) { mg_send_http_error(conn, 500, "%s", "Error: Size mismatch"); return 0; } for (;;) { int nread = mg_read(conn, buf, sizeof(buf)); if (nread <= 0) { success = (nread == 0); break; } if (push_all(conn->phys_ctx, fp, sock, ssl, buf, nread) != nread) { break; } } /* Each error code path in this function must send an error */ if (!success) { /* NOTE: Maybe some data has already been sent. */ /* TODO (low): If some data has been sent, a correct error * reply can no longer be sent, so just close the connection */ mg_send_http_error(conn, 500, "%s", ""); } } return success; } #endif #if defined(USE_TIMERS) #define TIMER_API static #include "timer.inl" #endif /* USE_TIMERS */ #if !defined(NO_CGI) /* This structure helps to create an environment for the spawned CGI * program. * Environment is an array of "VARIABLE=VALUE\0" ASCIIZ strings, * last element must be NULL. * However, on Windows there is a requirement that all these * VARIABLE=VALUE\0 * strings must reside in a contiguous buffer. The end of the buffer is * marked by two '\0' characters. * We satisfy both worlds: we create an envp array (which is vars), all * entries are actually pointers inside buf. */ struct cgi_environment { struct mg_connection *conn; /* Data block */ char *buf; /* Environment buffer */ size_t buflen; /* Space available in buf */ size_t bufused; /* Space taken in buf */ /* Index block */ char **var; /* char **envp */ size_t varlen; /* Number of variables available in var */ size_t varused; /* Number of variables stored in var */ }; static void addenv(struct cgi_environment *env, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(2, 3); /* Append VARIABLE=VALUE\0 string to the buffer, and add a respective * pointer into the vars array. Assumes env != NULL and fmt != NULL. */ static void addenv(struct cgi_environment *env, const char *fmt, ...) { size_t i, n, space; int truncated = 0; char *added; va_list ap; if ((env->varlen - env->varused) < 2) { mg_cry_internal(env->conn, "%s: Cannot register CGI variable [%s]", __func__, fmt); return; } /* Calculate how much space is left in the buffer */ space = (env->buflen - env->bufused); do { /* Space for "\0\0" is always needed. */ if (space <= 2) { /* Allocate new buffer */ n = env->buflen + CGI_ENVIRONMENT_SIZE; added = (char *)mg_realloc_ctx(env->buf, n, env->conn->phys_ctx); if (!added) { /* Out of memory */ mg_cry_internal( env->conn, "%s: Cannot allocate memory for CGI variable [%s]", __func__, fmt); return; } /* Retarget pointers */ env->buf = added; env->buflen = n; for (i = 0, n = 0; i < env->varused; i++) { env->var[i] = added + n; n += strlen(added + n) + 1; } space = (env->buflen - env->bufused); } /* Make a pointer to the free space int the buffer */ added = env->buf + env->bufused; /* Copy VARIABLE=VALUE\0 string into the free space */ va_start(ap, fmt); mg_vsnprintf(env->conn, &truncated, added, space - 1, fmt, ap); va_end(ap); /* Do not add truncated strings to the environment */ if (truncated) { /* Reallocate the buffer */ space = 0; } } while (truncated); /* Calculate number of bytes added to the environment */ n = strlen(added) + 1; env->bufused += n; /* Append a pointer to the added string into the envp array */ env->var[env->varused] = added; env->varused++; } /* Return 0 on success, non-zero if an error occurs. */ static int prepare_cgi_environment(struct mg_connection *conn, const char *prog, struct cgi_environment *env) { const char *s; struct vec var_vec; char *p, src_addr[IP_ADDR_STR_LEN], http_var_name[128]; int i, truncated, uri_len; if ((conn == NULL) || (prog == NULL) || (env == NULL)) { return -1; } env->conn = conn; env->buflen = CGI_ENVIRONMENT_SIZE; env->bufused = 0; env->buf = (char *)mg_malloc_ctx(env->buflen, conn->phys_ctx); if (env->buf == NULL) { mg_cry_internal(conn, "%s: Not enough memory for environmental buffer", __func__); return -1; } env->varlen = MAX_CGI_ENVIR_VARS; env->varused = 0; env->var = (char **)mg_malloc_ctx(env->varlen * sizeof(char *), conn->phys_ctx); if (env->var == NULL) { mg_cry_internal(conn, "%s: Not enough memory for environmental variables", __func__); mg_free(env->buf); return -1; } addenv(env, "SERVER_NAME=%s", conn->dom_ctx->config[AUTHENTICATION_DOMAIN]); addenv(env, "SERVER_ROOT=%s", conn->dom_ctx->config[DOCUMENT_ROOT]); addenv(env, "DOCUMENT_ROOT=%s", conn->dom_ctx->config[DOCUMENT_ROOT]); addenv(env, "SERVER_SOFTWARE=CivetWeb/%s", mg_version()); /* Prepare the environment block */ addenv(env, "%s", "GATEWAY_INTERFACE=CGI/1.1"); addenv(env, "%s", "SERVER_PROTOCOL=HTTP/1.1"); addenv(env, "%s", "REDIRECT_STATUS=200"); /* For PHP */ #if defined(USE_IPV6) if (conn->client.lsa.sa.sa_family == AF_INET6) { addenv(env, "SERVER_PORT=%d", ntohs(conn->client.lsa.sin6.sin6_port)); } else #endif { addenv(env, "SERVER_PORT=%d", ntohs(conn->client.lsa.sin.sin_port)); } sockaddr_to_string(src_addr, sizeof(src_addr), &conn->client.rsa); addenv(env, "REMOTE_ADDR=%s", src_addr); addenv(env, "REQUEST_METHOD=%s", conn->request_info.request_method); addenv(env, "REMOTE_PORT=%d", conn->request_info.remote_port); addenv(env, "REQUEST_URI=%s", conn->request_info.request_uri); addenv(env, "LOCAL_URI=%s", conn->request_info.local_uri); /* SCRIPT_NAME */ uri_len = (int)strlen(conn->request_info.local_uri); if (conn->path_info == NULL) { if (conn->request_info.local_uri[uri_len - 1] != '/') { /* URI: /path_to_script/script.cgi */ addenv(env, "SCRIPT_NAME=%s", conn->request_info.local_uri); } else { /* URI: /path_to_script/ ... using index.cgi */ const char *index_file = strrchr(prog, '/'); if (index_file) { addenv(env, "SCRIPT_NAME=%s%s", conn->request_info.local_uri, index_file + 1); } } } else { /* URI: /path_to_script/script.cgi/path_info */ addenv(env, "SCRIPT_NAME=%.*s", uri_len - (int)strlen(conn->path_info), conn->request_info.local_uri); } addenv(env, "SCRIPT_FILENAME=%s", prog); if (conn->path_info == NULL) { addenv(env, "PATH_TRANSLATED=%s", conn->dom_ctx->config[DOCUMENT_ROOT]); } else { addenv(env, "PATH_TRANSLATED=%s%s", conn->dom_ctx->config[DOCUMENT_ROOT], conn->path_info); } addenv(env, "HTTPS=%s", (conn->ssl == NULL) ? "off" : "on"); if ((s = mg_get_header(conn, "Content-Type")) != NULL) { addenv(env, "CONTENT_TYPE=%s", s); } if (conn->request_info.query_string != NULL) { addenv(env, "QUERY_STRING=%s", conn->request_info.query_string); } if ((s = mg_get_header(conn, "Content-Length")) != NULL) { addenv(env, "CONTENT_LENGTH=%s", s); } if ((s = getenv("PATH")) != NULL) { addenv(env, "PATH=%s", s); } if (conn->path_info != NULL) { addenv(env, "PATH_INFO=%s", conn->path_info); } if (conn->status_code > 0) { /* CGI error handler should show the status code */ addenv(env, "STATUS=%d", conn->status_code); } #if defined(_WIN32) if ((s = getenv("COMSPEC")) != NULL) { addenv(env, "COMSPEC=%s", s); } if ((s = getenv("SYSTEMROOT")) != NULL) { addenv(env, "SYSTEMROOT=%s", s); } if ((s = getenv("SystemDrive")) != NULL) { addenv(env, "SystemDrive=%s", s); } if ((s = getenv("ProgramFiles")) != NULL) { addenv(env, "ProgramFiles=%s", s); } if ((s = getenv("ProgramFiles(x86)")) != NULL) { addenv(env, "ProgramFiles(x86)=%s", s); } #else if ((s = getenv("LD_LIBRARY_PATH")) != NULL) { addenv(env, "LD_LIBRARY_PATH=%s", s); } #endif /* _WIN32 */ if ((s = getenv("PERLLIB")) != NULL) { addenv(env, "PERLLIB=%s", s); } if (conn->request_info.remote_user != NULL) { addenv(env, "REMOTE_USER=%s", conn->request_info.remote_user); addenv(env, "%s", "AUTH_TYPE=Digest"); } /* Add all headers as HTTP_* variables */ for (i = 0; i < conn->request_info.num_headers; i++) { (void)mg_snprintf(conn, &truncated, http_var_name, sizeof(http_var_name), "HTTP_%s", conn->request_info.http_headers[i].name); if (truncated) { mg_cry_internal(conn, "%s: HTTP header variable too long [%s]", __func__, conn->request_info.http_headers[i].name); continue; } /* Convert variable name into uppercase, and change - to _ */ for (p = http_var_name; *p != '\0'; p++) { if (*p == '-') { *p = '_'; } *p = (char)toupper((unsigned char)*p); } addenv(env, "%s=%s", http_var_name, conn->request_info.http_headers[i].value); } /* Add user-specified variables */ s = conn->dom_ctx->config[CGI_ENVIRONMENT]; while ((s = next_option(s, &var_vec, NULL)) != NULL) { addenv(env, "%.*s", (int)var_vec.len, var_vec.ptr); } env->var[env->varused] = NULL; env->buf[env->bufused] = '\0'; return 0; } /* Data for CGI process control: PID and number of references */ struct process_control_data { pid_t pid; int references; }; static int abort_process(void *data) { /* Waitpid checks for child status and won't work for a pid that does not * identify a child of the current process. Thus, if the pid is reused, * we will not affect a different process. */ struct process_control_data *proc = (struct process_control_data *)data; int status = 0; int refs; pid_t ret_pid; ret_pid = waitpid(proc->pid, &status, WNOHANG); if ((ret_pid != (pid_t)-1) && (status == 0)) { /* Stop child process */ DEBUG_TRACE("CGI timer: Stop child process %d\n", proc->pid); kill(proc->pid, SIGABRT); /* Wait until process is terminated (don't leave zombies) */ while (waitpid(proc->pid, &status, 0) != (pid_t)-1) /* nop */ ; } else { DEBUG_TRACE("CGI timer: Child process %d already stopped\n", proc->pid); } /* Dec reference counter */ refs = mg_atomic_dec(&proc->references); if (refs == 0) { /* no more references - free data */ mg_free(data); } return 0; } /* Local (static) function assumes all arguments are valid. */ static void handle_cgi_request(struct mg_connection *conn, const char *prog) { char *buf; size_t buflen; int headers_len, data_len, i, truncated; int fdin[2] = {-1, -1}, fdout[2] = {-1, -1}, fderr[2] = {-1, -1}; const char *status, *status_text, *connection_state; char *pbuf, dir[PATH_MAX], *p; struct mg_request_info ri; struct cgi_environment blk; FILE *in = NULL, *out = NULL, *err = NULL; struct mg_file fout = STRUCT_FILE_INITIALIZER; pid_t pid = (pid_t)-1; struct process_control_data *proc = NULL; #if defined(USE_TIMERS) double cgi_timeout = -1.0; if (conn->dom_ctx->config[CGI_TIMEOUT]) { /* Get timeout in seconds */ cgi_timeout = atof(conn->dom_ctx->config[CGI_TIMEOUT]) * 0.001; } #endif buf = NULL; buflen = conn->phys_ctx->max_request_size; i = prepare_cgi_environment(conn, prog, &blk); if (i != 0) { blk.buf = NULL; blk.var = NULL; goto done; } /* CGI must be executed in its own directory. 'dir' must point to the * directory containing executable program, 'p' must point to the * executable program name relative to 'dir'. */ (void)mg_snprintf(conn, &truncated, dir, sizeof(dir), "%s", prog); if (truncated) { mg_cry_internal(conn, "Error: CGI program \"%s\": Path too long", prog); mg_send_http_error(conn, 500, "Error: %s", "CGI path too long"); goto done; } if ((p = strrchr(dir, '/')) != NULL) { *p++ = '\0'; } else { dir[0] = '.'; dir[1] = '\0'; p = (char *)prog; } if ((pipe(fdin) != 0) || (pipe(fdout) != 0) || (pipe(fderr) != 0)) { status = strerror(ERRNO); mg_cry_internal( conn, "Error: CGI program \"%s\": Can not create CGI pipes: %s", prog, status); mg_send_http_error(conn, 500, "Error: Cannot create CGI pipe: %s", status); goto done; } proc = (struct process_control_data *) mg_malloc_ctx(sizeof(struct process_control_data), conn->phys_ctx); if (proc == NULL) { mg_cry_internal(conn, "Error: CGI program \"%s\": Out or memory", prog); mg_send_http_error(conn, 500, "Error: Out of memory [%s]", prog); goto done; } DEBUG_TRACE("CGI: spawn %s %s\n", dir, p); pid = spawn_process(conn, p, blk.buf, blk.var, fdin, fdout, fderr, dir); if (pid == (pid_t)-1) { status = strerror(ERRNO); mg_cry_internal( conn, "Error: CGI program \"%s\": Can not spawn CGI process: %s", prog, status); mg_send_http_error(conn, 500, "Error: Cannot spawn CGI process [%s]: %s", prog, status); mg_free(proc); proc = NULL; goto done; } /* Store data in shared process_control_data */ proc->pid = pid; proc->references = 1; #if defined(USE_TIMERS) if (cgi_timeout > 0.0) { proc->references = 2; // Start a timer for CGI timer_add(conn->phys_ctx, cgi_timeout /* in seconds */, 0.0, 1, abort_process, (void *)proc); } #endif /* Parent closes only one side of the pipes. * If we don't mark them as closed, close() attempt before * return from this function throws an exception on Windows. * Windows does not like when closed descriptor is closed again. */ (void)close(fdin[0]); (void)close(fdout[1]); (void)close(fderr[1]); fdin[0] = fdout[1] = fderr[1] = -1; if (((in = fdopen(fdin[1], "wb")) == NULL) || ((out = fdopen(fdout[0], "rb")) == NULL) || ((err = fdopen(fderr[0], "rb")) == NULL)) { status = strerror(ERRNO); mg_cry_internal(conn, "Error: CGI program \"%s\": Can not open fd: %s", prog, status); mg_send_http_error(conn, 500, "Error: CGI can not open fd\nfdopen: %s", status); goto done; } setbuf(in, NULL); setbuf(out, NULL); setbuf(err, NULL); fout.access.fp = out; if ((conn->content_len != 0) || (conn->is_chunked)) { DEBUG_TRACE("CGI: send body data (%" INT64_FMT ")\n", conn->content_len); /* This is a POST/PUT request, or another request with body data. */ if (!forward_body_data(conn, in, INVALID_SOCKET, NULL)) { /* Error sending the body data */ mg_cry_internal( conn, "Error: CGI program \"%s\": Forward body data failed", prog); goto done; } } /* Close so child gets an EOF. */ fclose(in); in = NULL; fdin[1] = -1; /* Now read CGI reply into a buffer. We need to set correct * status code, thus we need to see all HTTP headers first. * Do not send anything back to client, until we buffer in all * HTTP headers. */ data_len = 0; buf = (char *)mg_malloc_ctx(buflen, conn->phys_ctx); if (buf == NULL) { mg_send_http_error(conn, 500, "Error: Not enough memory for CGI buffer (%u bytes)", (unsigned int)buflen); mg_cry_internal( conn, "Error: CGI program \"%s\": Not enough memory for buffer (%u " "bytes)", prog, (unsigned int)buflen); goto done; } DEBUG_TRACE("CGI: %s", "wait for response"); headers_len = read_message(out, conn, buf, (int)buflen, &data_len); DEBUG_TRACE("CGI: response: %li", (signed long)headers_len); if (headers_len <= 0) { /* Could not parse the CGI response. Check if some error message on * stderr. */ i = pull_all(err, conn, buf, (int)buflen); if (i > 0) { /* CGI program explicitly sent an error */ /* Write the error message to the internal log */ mg_cry_internal(conn, "Error: CGI program \"%s\" sent error " "message: [%.*s]", prog, i, buf); /* Don't send the error message back to the client */ mg_send_http_error(conn, 500, "Error: CGI program \"%s\" failed.", prog); } else { /* CGI program did not explicitly send an error, but a broken * respon header */ mg_cry_internal(conn, "Error: CGI program sent malformed or too big " "(>%u bytes) HTTP headers: [%.*s]", (unsigned)buflen, data_len, buf); mg_send_http_error(conn, 500, "Error: CGI program sent malformed or too big " "(>%u bytes) HTTP headers: [%.*s]", (unsigned)buflen, data_len, buf); } /* in both cases, abort processing CGI */ goto done; } pbuf = buf; buf[headers_len - 1] = '\0'; ri.num_headers = parse_http_headers(&pbuf, ri.http_headers); /* Make up and send the status line */ status_text = "OK"; if ((status = get_header(ri.http_headers, ri.num_headers, "Status")) != NULL) { conn->status_code = atoi(status); status_text = status; while (isdigit((unsigned char)*status_text) || *status_text == ' ') { status_text++; } } else if (get_header(ri.http_headers, ri.num_headers, "Location") != NULL) { conn->status_code = 307; } else { conn->status_code = 200; } connection_state = get_header(ri.http_headers, ri.num_headers, "Connection"); if (!header_has_option(connection_state, "keep-alive")) { conn->must_close = 1; } DEBUG_TRACE("CGI: response %u %s", conn->status_code, status_text); (void)mg_printf(conn, "HTTP/1.1 %d %s\r\n", conn->status_code, status_text); /* Send headers */ for (i = 0; i < ri.num_headers; i++) { DEBUG_TRACE("CGI header: %s: %s", ri.http_headers[i].name, ri.http_headers[i].value); mg_printf(conn, "%s: %s\r\n", ri.http_headers[i].name, ri.http_headers[i].value); } mg_write(conn, "\r\n", 2); /* Send chunk of data that may have been read after the headers */ mg_write(conn, buf + headers_len, (size_t)(data_len - headers_len)); /* Read the rest of CGI output and send to the client */ DEBUG_TRACE("CGI: %s", "forward all data"); send_file_data(conn, &fout, 0, INT64_MAX); DEBUG_TRACE("CGI: %s", "all data sent"); done: mg_free(blk.var); mg_free(blk.buf); if (pid != (pid_t)-1) { abort_process((void *)proc); } if (fdin[0] != -1) { close(fdin[0]); } if (fdout[1] != -1) { close(fdout[1]); } if (fderr[1] != -1) { close(fderr[1]); } if (in != NULL) { fclose(in); } else if (fdin[1] != -1) { close(fdin[1]); } if (out != NULL) { fclose(out); } else if (fdout[0] != -1) { close(fdout[0]); } if (err != NULL) { fclose(err); } else if (fderr[0] != -1) { close(fderr[0]); } mg_free(buf); } #endif /* !NO_CGI */ #if !defined(NO_FILES) static void mkcol(struct mg_connection *conn, const char *path) { int rc, body_len; struct de de; char date[64]; time_t curtime = time(NULL); if (conn == NULL) { return; } /* TODO (mid): Check the mg_send_http_error situations in this function */ memset(&de.file, 0, sizeof(de.file)); if (!mg_stat(conn, path, &de.file)) { mg_cry_internal(conn, "%s: mg_stat(%s) failed: %s", __func__, path, strerror(ERRNO)); } if (de.file.last_modified) { /* TODO (mid): This check does not seem to make any sense ! */ /* TODO (mid): Add a webdav unit test first, before changing * anything here. */ mg_send_http_error( conn, 405, "Error: mkcol(%s): %s", path, strerror(ERRNO)); return; } body_len = conn->data_len - conn->request_len; if (body_len > 0) { mg_send_http_error( conn, 415, "Error: mkcol(%s): %s", path, strerror(ERRNO)); return; } rc = mg_mkdir(conn, path, 0755); if (rc == 0) { conn->status_code = 201; gmt_time_string(date, sizeof(date), &curtime); mg_printf(conn, "HTTP/1.1 %d Created\r\n" "Date: %s\r\n", conn->status_code, date); send_static_cache_header(conn); send_additional_header(conn); mg_printf(conn, "Content-Length: 0\r\n" "Connection: %s\r\n\r\n", suggest_connection_header(conn)); } else { if (errno == EEXIST) { mg_send_http_error( conn, 405, "Error: mkcol(%s): %s", path, strerror(ERRNO)); } else if (errno == EACCES) { mg_send_http_error( conn, 403, "Error: mkcol(%s): %s", path, strerror(ERRNO)); } else if (errno == ENOENT) { mg_send_http_error( conn, 409, "Error: mkcol(%s): %s", path, strerror(ERRNO)); } else { mg_send_http_error( conn, 500, "fopen(%s): %s", path, strerror(ERRNO)); } } } static void put_file(struct mg_connection *conn, const char *path) { struct mg_file file = STRUCT_FILE_INITIALIZER; const char *range; int64_t r1, r2; int rc; char date[64]; time_t curtime = time(NULL); if (conn == NULL) { return; } if (mg_stat(conn, path, &file.stat)) { /* File already exists */ conn->status_code = 200; if (file.stat.is_directory) { /* This is an already existing directory, * so there is nothing to do for the server. */ rc = 0; } else { /* File exists and is not a directory. */ /* Can it be replaced? */ #if defined(MG_USE_OPEN_FILE) if (file.access.membuf != NULL) { /* This is an "in-memory" file, that can not be replaced */ mg_send_http_error(conn, 405, "Error: Put not possible\nReplacing %s " "is not supported", path); return; } #endif /* Check if the server may write this file */ if (access(path, W_OK) == 0) { /* Access granted */ conn->status_code = 200; rc = 1; } else { mg_send_http_error( conn, 403, "Error: Put not possible\nReplacing %s is not allowed", path); return; } } } else { /* File should be created */ conn->status_code = 201; rc = put_dir(conn, path); } if (rc == 0) { /* put_dir returns 0 if path is a directory */ gmt_time_string(date, sizeof(date), &curtime); mg_printf(conn, "HTTP/1.1 %d %s\r\n", conn->status_code, mg_get_response_code_text(NULL, conn->status_code)); send_no_cache_header(conn); send_additional_header(conn); mg_printf(conn, "Date: %s\r\n" "Content-Length: 0\r\n" "Connection: %s\r\n\r\n", date, suggest_connection_header(conn)); /* Request to create a directory has been fulfilled successfully. * No need to put a file. */ return; } if (rc == -1) { /* put_dir returns -1 if the path is too long */ mg_send_http_error(conn, 414, "Error: Path too long\nput_dir(%s): %s", path, strerror(ERRNO)); return; } if (rc == -2) { /* put_dir returns -2 if the directory can not be created */ mg_send_http_error(conn, 500, "Error: Can not create directory\nput_dir(%s): %s", path, strerror(ERRNO)); return; } /* A file should be created or overwritten. */ /* Currently CivetWeb does not nead read+write access. */ if (!mg_fopen(conn, path, MG_FOPEN_MODE_WRITE, &file) || file.access.fp == NULL) { (void)mg_fclose(&file.access); mg_send_http_error(conn, 500, "Error: Can not create file\nfopen(%s): %s", path, strerror(ERRNO)); return; } fclose_on_exec(&file.access, conn); range = mg_get_header(conn, "Content-Range"); r1 = r2 = 0; if ((range != NULL) && parse_range_header(range, &r1, &r2) > 0) { conn->status_code = 206; /* Partial content */ fseeko(file.access.fp, r1, SEEK_SET); } if (!forward_body_data(conn, file.access.fp, INVALID_SOCKET, NULL)) { /* forward_body_data failed. * The error code has already been sent to the client, * and conn->status_code is already set. */ (void)mg_fclose(&file.access); return; } if (mg_fclose(&file.access) != 0) { /* fclose failed. This might have different reasons, but a likely * one is "no space on disk", http 507. */ conn->status_code = 507; } gmt_time_string(date, sizeof(date), &curtime); mg_printf(conn, "HTTP/1.1 %d %s\r\n", conn->status_code, mg_get_response_code_text(NULL, conn->status_code)); send_no_cache_header(conn); send_additional_header(conn); mg_printf(conn, "Date: %s\r\n" "Content-Length: 0\r\n" "Connection: %s\r\n\r\n", date, suggest_connection_header(conn)); } static void delete_file(struct mg_connection *conn, const char *path) { struct de de; memset(&de.file, 0, sizeof(de.file)); if (!mg_stat(conn, path, &de.file)) { /* mg_stat returns 0 if the file does not exist */ mg_send_http_error(conn, 404, "Error: Cannot delete file\nFile %s not found", path); return; } #if 0 /* Ignore if a file in memory is inside a folder */ if (de.access.membuf != NULL) { /* the file is cached in memory */ mg_send_http_error( conn, 405, "Error: Delete not possible\nDeleting %s is not supported", path); return; } #endif if (de.file.is_directory) { if (remove_directory(conn, path)) { /* Delete is successful: Return 204 without content. */ mg_send_http_error(conn, 204, "%s", ""); } else { /* Delete is not successful: Return 500 (Server error). */ mg_send_http_error(conn, 500, "Error: Could not delete %s", path); } return; } /* This is an existing file (not a directory). * Check if write permission is granted. */ if (access(path, W_OK) != 0) { /* File is read only */ mg_send_http_error( conn, 403, "Error: Delete not possible\nDeleting %s is not allowed", path); return; } /* Try to delete it. */ if (mg_remove(conn, path) == 0) { /* Delete was successful: Return 204 without content. */ mg_send_http_error(conn, 204, "%s", ""); } else { /* Delete not successful (file locked). */ mg_send_http_error(conn, 423, "Error: Cannot delete file\nremove(%s): %s", path, strerror(ERRNO)); } } #endif /* !NO_FILES */ #if !defined(NO_FILESYSTEMS) static void send_ssi_file(struct mg_connection *, const char *, struct mg_file *, int); static void do_ssi_include(struct mg_connection *conn, const char *ssi, char *tag, int include_level) { char file_name[MG_BUF_LEN], path[512], *p; struct mg_file file = STRUCT_FILE_INITIALIZER; size_t len; int truncated = 0; if (conn == NULL) { return; } /* sscanf() is safe here, since send_ssi_file() also uses buffer * of size MG_BUF_LEN to get the tag. So strlen(tag) is * always < MG_BUF_LEN. */ if (sscanf(tag, " virtual=\"%511[^\"]\"", file_name) == 1) { /* File name is relative to the webserver root */ file_name[511] = 0; (void)mg_snprintf(conn, &truncated, path, sizeof(path), "%s/%s", conn->dom_ctx->config[DOCUMENT_ROOT], file_name); } else if (sscanf(tag, " abspath=\"%511[^\"]\"", file_name) == 1) { /* File name is relative to the webserver working directory * or it is absolute system path */ file_name[511] = 0; (void) mg_snprintf(conn, &truncated, path, sizeof(path), "%s", file_name); } else if ((sscanf(tag, " file=\"%511[^\"]\"", file_name) == 1) || (sscanf(tag, " \"%511[^\"]\"", file_name) == 1)) { /* File name is relative to the currect document */ file_name[511] = 0; (void)mg_snprintf(conn, &truncated, path, sizeof(path), "%s", ssi); if (!truncated) { if ((p = strrchr(path, '/')) != NULL) { p[1] = '\0'; } len = strlen(path); (void)mg_snprintf(conn, &truncated, path + len, sizeof(path) - len, "%s", file_name); } } else { mg_cry_internal(conn, "Bad SSI #include: [%s]", tag); return; } if (truncated) { mg_cry_internal(conn, "SSI #include path length overflow: [%s]", tag); return; } if (!mg_fopen(conn, path, MG_FOPEN_MODE_READ, &file)) { mg_cry_internal(conn, "Cannot open SSI #include: [%s]: fopen(%s): %s", tag, path, strerror(ERRNO)); } else { fclose_on_exec(&file.access, conn); if (match_prefix(conn->dom_ctx->config[SSI_EXTENSIONS], strlen(conn->dom_ctx->config[SSI_EXTENSIONS]), path) > 0) { send_ssi_file(conn, path, &file, include_level + 1); } else { send_file_data(conn, &file, 0, INT64_MAX); } (void)mg_fclose(&file.access); /* Ignore errors for readonly files */ } } #if !defined(NO_POPEN) static void do_ssi_exec(struct mg_connection *conn, char *tag) { char cmd[1024] = ""; struct mg_file file = STRUCT_FILE_INITIALIZER; if (sscanf(tag, " \"%1023[^\"]\"", cmd) != 1) { mg_cry_internal(conn, "Bad SSI #exec: [%s]", tag); } else { cmd[1023] = 0; if ((file.access.fp = popen(cmd, "r")) == NULL) { mg_cry_internal(conn, "Cannot SSI #exec: [%s]: %s", cmd, strerror(ERRNO)); } else { send_file_data(conn, &file, 0, INT64_MAX); pclose(file.access.fp); } } } #endif /* !NO_POPEN */ static int mg_fgetc(struct mg_file *filep, int offset) { (void)offset; /* unused in case MG_USE_OPEN_FILE is set */ if (filep == NULL) { return EOF; } #if defined(MG_USE_OPEN_FILE) if ((filep->access.membuf != NULL) && (offset >= 0) && (((unsigned int)(offset)) < filep->stat.size)) { return ((const unsigned char *)filep->access.membuf)[offset]; } else /* else block below */ #endif if (filep->access.fp != NULL) { return fgetc(filep->access.fp); } else { return EOF; } } static void send_ssi_file(struct mg_connection *conn, const char *path, struct mg_file *filep, int include_level) { char buf[MG_BUF_LEN]; int ch, offset, len, in_tag, in_ssi_tag; if (include_level > 10) { mg_cry_internal(conn, "SSI #include level is too deep (%s)", path); return; } in_tag = in_ssi_tag = len = offset = 0; /* Read file, byte by byte, and look for SSI include tags */ while ((ch = mg_fgetc(filep, offset++)) != EOF) { if (in_tag) { /* We are in a tag, either SSI tag or html tag */ if (ch == '>') { /* Tag is closing */ buf[len++] = '>'; if (in_ssi_tag) { /* Handle SSI tag */ buf[len] = 0; if ((len > 12) && !memcmp(buf + 5, "include", 7)) { do_ssi_include(conn, path, buf + 12, include_level + 1); #if !defined(NO_POPEN) } else if ((len > 9) && !memcmp(buf + 5, "exec", 4)) { do_ssi_exec(conn, buf + 9); #endif /* !NO_POPEN */ } else { mg_cry_internal(conn, "%s: unknown SSI " "command: \"%s\"", path, buf); } len = 0; in_ssi_tag = in_tag = 0; } else { /* Not an SSI tag */ /* Flush buffer */ (void)mg_write(conn, buf, (size_t)len); len = 0; in_tag = 0; } } else { /* Tag is still open */ buf[len++] = (char)(ch & 0xff); if ((len == 5) && !memcmp(buf, "

webfakes::httpbin_app() API (1.0.0)

Download OpenAPI specification:Download

This is a webfakes web app for HTTP testing in R packages. It implements the https://httpbin.org API

HTTP methods

GET request

An endpoint to make a GET request agains. It returns the request's parameters and the request headers, in JSON.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "headers": { },
  • "origin": "string",
  • "path": "string",
  • "url": "string"
}

DELETE request

Endpoint to make DELETE requests againts.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

PATCH request

Endpoint to make PATCH requests against.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

POST request

Endpoints to make a POST request against.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

PUT request

Endpoints to make a PUT request against.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Auth

Basic authentication.

Basic authentication with specified user and password

path Parameters
user
required
string

User name.

password
required
string

Password.

header Parameters
Authorization
string

Base64 encoded user name and password. See https://en.wikipedia.org/wiki/Basic_access_authentication#Client_side for the protocol.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Hidden basic authentication.

Basic authentication with specified user and password. This is the same as /basic-auth, but it returns a 404 if no authentication is supplied or the authentication fails.

path Parameters
user
required
string

User name.

password
required
string

Password.

header Parameters
Authorization
string

Base64 encoded user name and password. See https://en.wikipedia.org/wiki/Basic_access_authentication#Client_side for the protocol.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Digest authentication.

Digest authentication with specified user and password

path Parameters
qop
required
string

Quality of protection, possible values are auth and auth-int.

user
required
string

User name.

passwd
required
string

Password.

query Parameters
require_cookie
any

Whether to require cookie handling for a successful authentication. Set to 1, 'tortrue` to require cookies.

header Parameters
Authorization
string

Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Digest authentication algorithms.

Digest authentication with specified user and password, and algorithm.

path Parameters
qop
required
string

Quality of protection, possible values are auth and auth-int.

user
required
string

User name.

passwd
required
string

Password.

algorithm
required
string

Hashing algorithm to use: MD5, SHA-256 or SHA-512.

query Parameters
require_cookie
any

Whether to require cookie handling for a successful authentication. Set to 1, 'tortrue` to require cookies.

header Parameters
Authorization
string

Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Digest authentication, stale after.

Digest authentication with specified user, password and algorithm, with a stale_after value.

path Parameters
qop
required
string

Quality of protection, possible values are auth and auth-int.

user
required
string

User name.

passwd
required
string

Password.

algorithm
required
string

Hashing algorithm to use: MD5, SHA-256 or SHA-512.

stale_after
required
string

How many requests are allowed. This is stored in a cookie.

query Parameters
require_cookie
any

Whether to require cookie handling for a successful authentication. Set to 1, 'tortrue` to require cookies.

header Parameters
Authorization
string

Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Bearer authentication.

Checks is that a token is supplied in the Authorization header.

header Parameters
Authorization
string^Bearer

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "token": "string"
}

Status codes

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Request inspection

HTTP request headers.

Responses

Response samples

Content type
application/json
{
  • "headers": { }
}

IP address of the client.

Responses

Response samples

Content type
application/json
{
  • "origin": "string"
}

The client's user agent.

Responses

Response samples

Content type
application/json
{
  • "user-agent": "string"
}

Response inspection

Caching

Returns a 304 if an If-Modified-Since header or If-None-Match is present. Returns the same as a GET otherwise.

header Parameters
If-Modified-Since
any
If-None-Match
any

Responses

Cache control.

Sets a Cache-Control header for n seconds.

path Parameters
value
integer

Responses

Work with ETags.

Assumes the specified etag parameter as the ETag header.

  • If a matching tag is supplied in the If-None-Match header, then it returns with HTTP status code 304.
  • If a non-matching If-Match header is supplied, then it return with HTTP status code 412.
  • Otherwise it returns with a JSON body and status 200.

See https://en.wikipedia.org/wiki/HTTP_ETag for more about ETag headers.

path Parameters
etag
required
string

Assumed ETag value.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "headers": { },
  • "origin": "string",
  • "path": "string",
  • "url": "string"
}

Set response headers.

Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times.

query Parameters
object

Responses

Response samples

Content type
application/json
{ }

Set response headers.

Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times.

query Parameters
object

Responses

Response samples

Content type
application/json
{ }

Response formats

A simple page that is denied for robots.

This exists in conjunction with /robots.txt which disallows this page.

Responses

Send gzip encoded data.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Deflated data.

Returns Deflate-encoded data. This needs the zip R package.

brotli compression

Returns brotli-encoded data. This needs the brotli R package.

Send UTF-8 enconded data.

Responses

Send an HTML page.

Responses

Send a JSON document.

Responses

Response samples

Content type
application/json
{ }

Send example `robots.txt` rules.

It will not allow the /deny endpoint.

Responses

Send an XML document.

Responses

Dynamic data

Base64 decoder

Base64 decode the supplied value and echo it back.

path Parameters
value
required
string

Assumed ETag value.

Responses

Random bytes.

path Parameters
n
required
integer

Number of bytes, maximum 10000.

Responses

Delayed response.

Wait for the specified number of seconds before sending the response.

path Parameters
secs
required
number

Number of seconds, fractions are supported.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Drip bytes

Drip the specified number of bytes over the specified number of seconds, potentially after some initial delay.

query Parameters
duration
number
Default: 2

Number of seconds for the whole response.

numbytes
integer
Default: 10

Number of bytes to return.

code
integer
Default: 200

HTTP status code to return with.

delay
number
Default: 0

Initial delay, in seconds. Zero means no delay.

Responses

Links

Generate a page containing n links to other pages which do the same.

path Parameters
n
required
integer

Number of links.

offset
integer

Id of the page to return, out of n.

Responses

Stream JSON chunks.

Stream n JSON responses, each terminated by a newline character.

path Parameters
n
required
integer

Number of JSON chunks to send.

Responses

Stream in chunks.

Streams n random bytes generated with given seed, at given chunk size per packet.

path Parameters
n
required
integer

Number of bytes, maximum 102400.

query Parameters
seed
number
Default: 42

Random seed.

chunk-size
integer
Default: 10240

Size of a chunked for the chunked encoding. The last chunk migh be shorter than his.

Responses

Range requests.

Send only part of the response, after a Range header.

path Parameters
n
required
integer

Number of bytes, maximum 102400.

query Parameters
chunk_size
number
Default: 10240

Chunk size, if duration is not 0.

duration
number
Default: 0

Duration for the total response. It applies to the full response size without ranges.

Responses

Range requests..

path Parameters
n
required
integer

Number of bytes, maximum 102400.

query Parameters
chunk_size
number
Default: 10240

Chunk size, if duration is not 0.

duration
number
Default: 0

Duration for the total response. It applies to the full response size without ranges.

Responses

Random UUID.

It is pseudo-random, and not secure.

Responses

Response samples

Content type
application/json
{
  • "uuid": "ee4610df-3b7b-4624-8ce9-da6975b3e9f1"
}

Cookies

Cookie data.

Returns cookie data.

Responses

Response samples

Content type
application/json
{
  • "cookies": { }
}

Delete cookies.

Deletes cookie(s) as provided by the query string and redirects to cookie list.

query Parameters
freeform
any

Responses

Set cookies.

Sets cookie(s) as provided by the query string and redirects to cookie list.

query Parameters
freeform
any

Responses

Set one cookie.

Sets a cookie and redirects to cookie list.

path Parameters
name
string

Cookie name.

value
string

Cookie value.

Responses

Images

Return an image.

It selects the format according to the Accept request header. Supported formats: image/jpeg, image/png, image/svg+xml, image/webp, image/*.

header Parameters
Accept
string

Accepted image content type.

Responses

Response samples

Content type
application/json
{
  • "message": "Client did not request a supported media type.",
  • "accept": [
    ]
}

Return an image of the specified format.

path Parameters
format
required
string

Image format. May be jpeg, png, svg or webp.

Responses

Redirects

Redirect to an absolute URL, n times.

path Parameters
n
required
integer

Number of times to redirect. Maximum is five.

Responses

Redirect to a relative URL, n times.

path Parameters
n
required
integer

Number of times to redirect. Maximum is five.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Anything

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}
webfakes/inst/examples/httpbin/assets/forms-post.html0000644000176200001440000000256614430677111022622 0ustar liggesusers

Pizza Size

Pizza Toppings

webfakes/inst/examples/httpbin/doc-template.hbs0000644000176200001440000000072114172041777021372 0ustar liggesusers {{title}} {{{redocHead}}} {{{redocHTML}}} webfakes/inst/examples/httpbin/images/0000755000176200001440000000000014172041777017563 5ustar liggesuserswebfakes/inst/examples/httpbin/images/Rlogo.svg0000644000176200001440000004062114172041777021371 0ustar liggesusers webfakes/inst/examples/httpbin/images/Rlogo.jpeg0000644000176200001440000001120614172041777021514 0ustar liggesusersJFIFvvC       C Ld G !"1Q#27Aaq$Bt3Wbs%Rruv2!A123QR"4aq#B ?V& :c֨j/@]Emx!+b9q|#B7a= 횥j3E9e)&'iy` s0eGr#[i2](򙏷@ s/zA ݹ]) Zz!,$ʼnI> <&2I#{4iihG@djTST̠7\}ߊ]W +0`zʚOn0xOeM.LysI vsIBW/;Uef߷#f,"CE@*k٫d^-T5q/L-C.Gq oDsWrw L&ݘ&fBy-lr߀ p{=3/EIr> )Om4TSTlL98Wpm,wg[-TM3Y(}\~ I'p/k3a"-傰wVirS{3iHEZrܹm'dPxn-zen4¦5)q#UR cQ-g:GWJ.T!!jVu]v_نM|hS0x4fǍUW |ңmCQsܵ \3݈1=:>YY[/'eϳG'y7'M)q>c 9Laz,gե\+Wِ.Ewp {|ؗle*/-7q FͶhϦ-Q-I7anFeEdWVҥ+嘂Yt`A縮8 9c{+WO~۪&sFu k*͵4tf!K)U,7<:ic!ޓgŎy?Oc2}ϬI2"Z{H^C2#?D$#$'b uyQWCSUNډ>A\jA%SQEECw Uy0UvۊhYYfxl/=]d*L<[O41;ľ p_$rU/Փ K^ZdT3BOz\j#Y8+=E-i2F'mjvW@ͫR7"+.@*Ô,UW4鴪VVo;ƪMrk u wH,yv,v*=Uܪ*!׹Tw`&/rVsd,M,oLU2J6} U`_F \dt+Eࡽ1W7,sw{ݙ(Rq\8̹0MEQ ]K!  \4pfܣ~uձ.`0Hwl7Vk>,u;{uFL wf>>b,Wӆ\ AWG*TSOJ]j+)<SkT [>`ݮsoHGS~]}>R$gVVٚ;݆ӚKySc !cc؏ iZ'f*:0.uBz+|GTfx41C` E"/&eF1k>T'}4LLLL}v4kebghАJ7U;{ӆfݭvNeGXV:n̑2Ȃxzw؋\> b BJ:MڃkiɁȒ;+op ۍSE圛sWU &QF-M|ռ7Qv UɬXI-4+x؂YIrwpFDiX%ѮzLGL. H#^b›ȓ;e\zXUVT*fC^xnLJcm6TX%n`|aOFy]2毩-1n35㨥WC5$#T'paA{nIJNf$q&ڎ3u&۱Cu][wA4Lض u\`;&D@uiz3òCӾm'Nvwߤ6n.˅>Ѷcm۪v](7s[u] LeY( .ׅ8u8q\0YBQKh~76a9l?w_m+eEQE{}/a`[vݒzx[3uJ8 .na4EU5/)1ʋԮ]Ǫ#˴۶8(PěOnߍz,,ׅe T{-uJR$ǁm۰ٶ ۶`6,˂8@,|GnzŸLؓ2-Ӻ6 &P #`CQ޾#SUw/FTmۀ¶P8c_p]-SaIxb[lˆi 1 òUU^u ?tOj3M|۱#K( mwB"4M@ tdxRy3#aۻm8Fm{@X6,˄eZ0LeT 3oO% ?`LK øuoɞo> !0MA5 u=io?Р `g^R`;,˂i0-eum Ʋ,X 4a0=Z]U} T_bq8exJH˱x4!(2r  i.@348za0,X]*JD}Ӵ&ζ풭2*!B@34òi2M4Ų-4at]aPpU_y{VT˶INPe!Mҩ$t]E( ='x8EQ ,MӠit]aucXnS2Tq]qnS Àxa8.4Mi0b& ]aui&tW89;XpXy'L&}nSOQP8P,"N\dYqX  " y.`:TM(exb0-=h[k ^P8woE5EO=v,(ݿVg eq<\ǁ( e0m-ӄ=P)yYcE~ֶ鄐7D׃<Es8yP(/@(CQdȊ IQeo4GT ǶKIiP(B,-'CA Àa M8A4 MSjZ~tMנh}羻fCin̓ !8CYY2`5f @<G<GmM*+*P0%Q 5 #ni]Ne['s LB|}%j8}y`px׮'ˊq:N˱8E狎 # KP,C4 / 0E@4M#bb$3iE:/Cń6TW,d (EA.];cR)Bt+?c^[|JƵ7\vQضMqx<{{u\ **$Q(B\p/I_t? ;=ErLt:mUU 0i,Z(2$YBP%]]y&$i!kllgS>.ҲsL8 ˀJD$B.ȐUPͻCY4U] ?FUE>,[/̛ Ӵ   Q^^  !$ YV04ucvryomxB7z|eNPS+'y,! H B 5)//?!y|(v04!(/+9֯S΁eYBBè䉓1f8I D HXz HbQ)妧xV>f](Jp]S±-&$A33Vxel#&( `9Ȥ% 777Oy }se(@@f=t?TMC(B$F$CuU5LMca6XD6,+>ss_@umL qM0uÆ FQ]]I'cBK; "bd*w[ "#A?kɢeoj2F?[lV@ B0-rT:t:$B%h`ϵ67_}劋ٹ4͌Ϧ8-[ ˃y)NҫL`0'r2!4(B(Ix7qpACԢyxAh湮xΪ5+ѱl\B]Gqĉ/?]xq:L Сk*YD&5MbwϟcƠ::z?]~֍Wz^ KI|_L T0PUU!(t:y |g@1 (++C&UqhUS::u`ӦeDȴq,#^^C#Qq>s J{X>7ah V^wgmKu]tyo<-J.q]ѲL4nB:$I_?kǽ3(pASݫ,Xl1h'# 0 tk׭F6^QNᄒ`aR3,CcJ"X`Vċ Ӳ DIB._nX{'} B ux͗p羀h Z@6-R+Vyͷc2xݍW]V䥎 Hh$!egA}]=VY\.A pcs4]ǦM144BA$Pdy{K_=\kp(kaRF.=ɡi (1O aXdrP 4QG˰c-֯] ! A}i_׼kk.+(;,H$e+_G1}6888pUI<Ϗ+u7A zY ^rFe"Q(xy C3csa GنĠkVa릵 :k[dp@0p$@ ° }C7a:vuCovD咠( [vbGM|WG1O@:9L>=q @Ea .L,wvD.Pg=*.UU]YD"W`/#=f@u0@\ bUja`u@~l 0zlvlE<M{3k9+rP]]p 4̲<(-Y<#<,=ҁQfUAoOO)@i0n:|.*Ma۶Ntn]D7o y!6\BTmq\mnXرm3Ƕ ~4ҩ!| аn݆SL4e˗7LcrUe6`#dYigl]F__z1~\mrd30tCМt~8q"#I0"WEq+kjCӴH&j攼** uwy (קˑe >Tr{wiYq4a˲#Τ<XʼU#>F&)AD\^ukv,X P$L1Gߎ@D7giO3$fj^$7{Ao ݳN魮y8/p8lzx0E-Eh"Wm8s0MsɧJahewXSsO 4 ,d0Bzy"iCS 1 <e4uXq,9eNP5EBX*0B%ޣ«sEKð.M3,Fae 2e]q]&$A(؎4 |4SMeyUE4[iB(uJG cN8,'`sD}m&Ojkv_DMHnCAēN~7.< k7B2/Z?^9uV5weƼbJC!(RRfӅp( ghC$EV&޻pǥ+FoW'zm.N;|vR>p8OνG?rjhtEGc ]? ^cPCCCp <)1*P( x}}PTD"}_||868.$r>H 9Qz1:cՍېI%0ؿ(8AYY\l]dWӟ;oZ1aG}gEuL>{r/Nr޹X|YCCu]kv+s-˂i & ULò,T'JCA@Szz IL&|C=O6:KpQmHt[[7i{(C8E(Sl`8e!94MS1˧>4ff{kӴv IDAT}G1A_dC'9)>iYV,+ƓO<S rzB{y.a^?)/Ǯ9X2̂8R䩲W Iz]G^c яl6 ˴MeuMM :̼"8S\D,5譗/w㈕U!fҨk =1PH0pH`( ͙4o>Ļ}/+򍺮pI&Ŝ9OAKJ/^q}0@8w׿PF#}(;6F8]eAUXA@Q, ` 9 MզH\yJ4|OWTP(,z5p|5c HY4nlX;Ǯu,)O e`zqX @=] EƷ"B TU &Scwu\x׾U_9*_[*N8? C2!ھ};}P JaB[JZNf5MiwkQlY FҘ@74j,*b9,0g&ߘq=Y D?I Jy]KjլY/+&n0L6+04zw!+G*5]6 O@$ð^D4]#CGvg|K{xO~8Quo7ބeZQ!8 -w>?<d|.e+1nl3m5:a 46?Ͽ{+OfhA"9UU"łeM̀eY؎!8>N1b`@3 V0 YU,l a\eSOW5M+ 12Uy ebES8xjN!!|W|Mo03 ALz$\jGǑǝgB4V?ôrU' ªqc^ Gwϝ.HTc^S1IaZ;/oNd5B}XYێ]n MkU; JUuHB0RBs%,ʫGoB Uض#T㨊}t_<.vAQߚ#.va2LsgYC`Dxt`)}b5/  b@Q(qEP*t]E6Eee%G5b X%h po"A$k/Xx RI[n}G͸M@zq\@A*@ DID.fYds9,4ai'^Md<^h,p8\2@Q`!O\K%o͢{)FrxU#x6mMJoǫDMӎw\]<;R A e38ҫ.u(lSTׯK$ 4l&ʊ 4i@"*0fAe(dIp$]Ϧ/ˮO #㺫o< {~S*<[oi6u]q{KjI ǻ@?m{| f1~r&΀zedtڎLuc%/Mӵ UuP ]öNxygxS\]np/iph+~ɶ/1E Ǽf: ʴk\Xm6S5QUdc\hniC׮׀CA3+ n}myۦQKZWNe4/#:~|3:npm(g3phtMA>d_mNjinP??iTl:$ID&Ei쇦uY!QH%# s/14FS&5_?@є }4MKxйmgY^t9@lKйi ]Э}g_wwhnX۶0 @(+WU>SxaN a>rԖ=[!~qCQ‘X?kZfIENDB`webfakes/inst/examples/httpbin/images/Rlogo.webp0000644000176200001440000000513414172041777021527 0ustar liggesusersRIFFT WEBPVP8X cKALPHzm۶!I'˶Ѷm۶m۶m۶mV~7"bHXC`Zh4Ξ6KdO35Q*<4uN'YQG.W}{PfuN^A4ϠOD ^QiQ"8MЬqi}\AֻoNӉBߟ^=O)RZ^uc2|GQwtx߬n2N1 h!^6;mMzN + j7%  )/h3ȃvK _ o" <=#I]'+ &Li/"Y#0I#)BAi# 7E{ȝw|%RHj)^9K*}$@]$Gg$$@)Er$AD#kIz7o #YEti. cehgO}uq ˃3 ec/.G̙3Ty CZS]idsg r8-EKW8fo$mU+%,Ot0BE pl.Fi?9GQwO"02;]8uQ'ʉ*Dɇ6L`wKPoV?2Q=)Zy$?w4A Eqhg œDD~ k!, )GZא ϛ<$qwZbUpN,}H9Iy6 )BE,3,(tjȵEp e2=*ŸDo>sA;^YVŒڮ.ٙMS:#+ Q4.c@kS6̳?Y3ewh؟t4;4㳇Xvzԩ KVP8 *dL>FK% e|*.e.wyx&=򞉪2v?a.H0ꈭng6d۪{f_dK"7"A:Ÿ?1M,/`^G HDV͓~=[bI6Z[T=V:^JQrC#~05A^pehh> `3$?$eJ`'CfuJHpxsY ށ2x2#}8D zB7ozM'ԯ? /MM,'Q&#wrYMaۖ0{B\PoѰBV:$ڶ;*@n4ߜ?a zUb|\7͙y ,v*nƷ6b~J^4*: aѼJPfq !-r¤ZfÌUHQ6߹0o4g筰>7)MB)R"0[:G9w"u Y%YA_{&gT+SěՆh.Qިy;rVj?.C½qE]y.ͤ|Y"9smb4cHcդys9_\WY?,.nI'h$IkԿN<˲;/G/N$9zÛ[kK_qie!q?!T0/*7)>O848ْ%S4$Bw4% oa,[K^#Q:KT;y{sdX0S.Y:]&L2w1b[GK Y"EC [؉4H<,ei~ k,&@GG6n'"QaoȫyDۛ for the protocol. get: tags: - Auth summary: Basic authentication. description: | Basic authentication with specified user and password responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. headers: WWW-Authenticate: description: Contains the authentication realm. schema: type: string pattern: '^Basic realm=".*"' '/hidden-basic-auth/:user/:password': parameters: - name: user in: path required: true description: User name. schema: type: string - name: password in: path required: true description: Password. schema: type: string - name: Authorization in: header schema: type: string description: | Base64 encoded user name and password. See for the protocol. get: tags: - Auth summary: Hidden basic authentication. description: | Basic authentication with specified user and password. This is the same as `/basic-auth`, but it returns a 404 if no authentication is supplied or the authentication fails. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '404': description: Failed authentication. '/digest-auth/:qop/:user/:passwd': parameters: - name: qop in: path required: true description: Quality of protection, possible values are `auth` and `auth-int`. schema: type: string - name: user in: path required: true description: User name. schema: type: string - name: passwd in: path required: true description: Password. schema: type: string - name: require_cookie in: query required: false description: | Whether to require cookie handling for a successful authentication. Set to `1`, 't` or `true` to require cookies. scheme: type: string - name: Authorization in: header schema: type: string description: | Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details. get: tags: - Auth summary: Digest authentication. description: | Digest authentication with specified user and password responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. '/digest-auth/:qop/:user/:passwd/:algorithm': parameters: - name: qop in: path required: true description: Quality of protection, possible values are `auth` and `auth-int`. schema: type: string - name: user in: path required: true description: User name. schema: type: string - name: passwd in: path required: true description: Password. schema: type: string - name: algorithm in: path required: true description: | Hashing algorithm to use: `MD5`, `SHA-256` or `SHA-512`. schema: type: string default: MD5 - name: require_cookie in: query required: false description: | Whether to require cookie handling for a successful authentication. Set to `1`, 't` or `true` to require cookies. scheme: type: string - name: Authorization in: header schema: type: string description: | Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details. get: tags: - Auth summary: Digest authentication algorithms. description: | Digest authentication with specified user and password, and algorithm. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. '/digest-auth/:qop/:user/:passwd/:algorithm/:stale_after': parameters: - name: qop in: path required: true description: Quality of protection, possible values are `auth` and `auth-int`. schema: type: string - name: user in: path required: true description: User name. schema: type: string - name: passwd in: path required: true description: Password. schema: type: string - name: algorithm in: path required: true description: | Hashing algorithm to use: `MD5`, `SHA-256` or `SHA-512`. schema: type: string default: MD5 - name: stale_after in: path required: true description: | How many requests are allowed. This is stored in a cookie. schema: type: string default: 'never' - name: require_cookie in: query required: false description: | Whether to require cookie handling for a successful authentication. Set to `1`, 't` or `true` to require cookies. scheme: type: string - name: Authorization in: header schema: type: string description: | Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details. get: tags: - Auth summary: Digest authentication, stale after. description: | Digest authentication with specified user, password and algorithm, with a stale_after value. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. '/bearer': parameters: - name: Authorization in: header schema: type: string pattern: '^Bearer ' get: tags: - Auth summary: Bearer authentication. description: | Checks is that a token is supplied in the `Authorization` header. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean token: type: string '401': description: Failed authentication. headers: WWW-Authenticate: description: Contains the authentication realm. schema: type: string pattern: '^bearer$' '/status/:status': parameters: - $ref: '#/components/parameters/status' delete: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. get: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. patch: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. post: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. put: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. '/headers': get: tags: - Request inspection summary: HTTP request headers. responses: '200': description: The requests's HTTP headers. content: application/json: schema: type: object properties: headers: type: object '/ip': get: tags: - Request inspection summary: IP address of the client. responses: '200': description: | The IP address of the client, usually `127.0.0.1`, as the webfakes server runs on the localhost. content: application/json: schema: type: object properties: origin: type: string '/user-agent': get: tags: - Request inspection summary: The client's user agent. responses: '200': description: The content of the `User-Agent` HTTP request header. content: application/json: schema: type: object properties: 'user-agent': type: string '/cache': parameters: - name: If-Modified-Since in: header - name: If-None-Match in: header get: tags: - Response inspection summary: Caching description: | Returns a 304 if an `If-Modified-Since` header or `If-None-Match` is present. Returns the same as a GET otherwise. responses: '200': description: Cached response. content: application/json: $ref: '#/components/schemas/get' '304': description: Modified. '/cache/:value': parameters: - name: value in: path schema: type: integer get: tags: - Response inspection summary: Cache control. description: | Sets a `Cache-Control` header for n seconds. responses: '200': description: Cache control set. content: application/json: $ref: '#/components/schemas/get' '/etag/:etag': parameters: - name: etag in: path required: true description: Assumed ETag value. schema: type: string get: tags: - Response inspection summary: Work with ETags. description: | Assumes the specified `etag` parameter as the `ETag` header. * If a matching tag is supplied in the `If-None-Match` header, then it returns with HTTP status code 304. * If a non-matching `If-Match` header is supplied, then it return with HTTP status code 412. * Otherwise it returns with a JSON body and status 200. See for more about `ETag` headers. responses: '200': description: | Non-matching `If-Non-Match` or matching `If-Match`, or no such headers at all. content: application/json: schema: $ref: '#/components/schemas/get' headers: ETag: description: The supplied `etag` value. schema: type: string '304': description: | Matching `If-Non-Match` header. '412': description: | Non-matchinf `If-Match` header. '/response-headers': parameters: - in: query name: params schema: type: object additionalProperties: type: string get: tags: - Response inspection summary: Set response headers. description: | Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times. responses: '200': description: | The passed query parameters are also returned as a JSON response. content: application/json: schema: type: object post: tags: - Response inspection summary: Set response headers. description: | Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times. responses: '200': description: | The passed query parameters are also returned as a JSON response. content: application/json: schema: type: object '/deny': get: tags: - Response formats summary: A simple page that is denied for robots. description: | This exists in conjunction with `/robots.txt` which disallows this page. responses: '200': description: A simple page. content: text/plain: schema: type: string example: 'Example content.' '/gzip': get: tags: - Response formats summary: Send gzip encoded data. responses: '200': description: Some gzipped JSON. content: application/json: schema: $ref: '#/components/schemas/common' headers: 'Content-Encoding': description: Set to `gzip`. schema: type: string pattern: '^gzip$' '/deflate': get: tags: - Response formats summary: Deflated data. description: | Returns Deflate-encoded data. This needs the zip R package. content: application/json: schema: $ref: '#/components/schemas/common' headers: 'Content-Encoding': description: Set to `deflate`. schema: type: string pattern: '^deflate$' '/brotli': get: tags: - Response formats summary: brotli compression description: | Returns brotli-encoded data. This needs the brotli R package. content: application/json: schema: $ref: '#/components/schemas/common' headers: 'Content-Encoding': description: Set to `brotli`. schema: type: string pattern: '^brotli' '/encoding/utf8': get: tags: - Response formats summary: Send UTF-8 enconded data. responses: '200': description: HTML with a lot of UTF-8. content: text/html: schema: type: string headers: 'Content-Type': description: Set to `text/html; charset=utf-8`. schema: type: string '/html': get: tags: - Response formats summary: Send an HTML page. responses: '200': description: An example HTML page. content: text/html: schema: type: string '/json': get: tags: - Response formats summary: Send a JSON document. responses: '200': description: An example JSON document. content: application/json: schema: type: object '/robots.txt': get: tags: - Response formats summary: Send example `robots.txt` rules. description: It will not allow the `/deny` endpoint. responses: '200': description: An example `robots.txt` file. content: text/plain: schema: type: string '/xml': get: tags: - Response formats summary: Send an XML document. responses: '200': description: An example XML document. content: application/xml: schema: type: string '/base64/:value': parameters: - name: value in: path required: true description: Assumed ETag value. schema: type: string get: tags: - Dynamic data summary: Base64 decoder description: Base64 decode the supplied value and echo it back. responses: '200': description: Decoded base64. content: application/octet-stream: schema: type: string '/bytes/:n': parameters: - name: n in: path required: true description: Number of bytes, maximum 10000. schema: type: integer get: tags: - Dynamic data summary: Random bytes. responses: '200': description: Random bytes. content: application/octest-stream: schema: type: string '/delay/:secs': parameters: - name: secs in: path required: true description: Number of seconds, fractions are supported. schema: type: number get: tags: - Dynamic data summary: Delayed response. description: | Wait for the specified number of seconds before sending the response. responses: '200': description: JSON response. content: application/json: schema: $ref: '#/components/schemas/common' '/drip': parameters: - name: duration in: query description: Number of seconds for the whole response. schema: type: number default: 2 - name: numbytes in: query description: Number of bytes to return. schema: type: integer default: 10 - name: code in: query description: HTTP status code to return with. schema: type: integer default: 200 - name: delay in: query description: Initial delay, in seconds. Zero means no delay. schema: type: number default: 0 get: tags: - Dynamic data summary: Drip bytes description: | Drip the specified number of bytes over the specified number of seconds, potentially after some initial delay. responses: '200': description: | Data of the specified length. The status code is not neccesarily 200, but the one specified in the `code` query parameter. content: application/octet-stream: schema: type: string '/links/:n/:offset': parameters: - name: n in: path required: true description: Number of links. schema: type: integer - name: offset in: path required: false description: Id of the page to return, out of `n`. schema: type: integer get: tags: - Dynamic data summary: Links description: | Generate a page containing n links to other pages which do the same. responses: '200': description: | HTML links. content: text/html '/stream/:n': parameters: - name: n in: path required: true description: Number of JSON chunks to send. schema: type: integer get: tags: - Dynamic data summary: Stream JSON chunks. description: | Stream `n` JSON responses, each terminated by a newline character. responses: '200': description: | Streamed JSON responses. Each JSON chunk contains an `id` field, from zero, up to `n-1`. content: application/json '/stream-bytes/:n': parameters: - name: n in: path required: true description: Number of bytes, maximum 102400. schema: type: integer - name: seed in: query description: Random seed. schema: type: number default: 42 - name: chunk-size in: query description: | Size of a chunked for the chunked encoding. The last chunk migh be shorter than his. schema: type: integer default: 10240 get: tags: - Dynamic data summary: Stream in chunks. description: | Streams n random bytes generated with given seed, at given chunk size per packet. responses: '200': description: Random bytes. content: application/octest-stream: schema: type: string '/range/:n': parameters: - name: n in: path required: true description: Number of bytes, maximum 102400. schema: type: integer - name: chunk_size in: query required: false description: Chunk size, if `duration` is not 0. schema: type: number default: 10240 - name: duration in: query required: false description: | Duration for the total response. It applies to the full response size without ranges. schema: type: number default: 0 get: tags: - Dynamic data summary: Range requests. description: | Send only part of the response, after a `Range` header. responses: '200': description: | Full response, if there was no `Range` header, or the `Range` header was invalid. content: text/plain '206': description: | Partial response, containing the requested range. head: tags: - Dynamic data summary: Range requests.. desciption: | HEAD request to an endpoint that supports ranges. responses: '200': description: | Response with an `Accept-Ranges: bytes` header. '/uuid': get: tags: - Dynamic data summary: Random UUID. description: It is pseudo-random, and not secure. responses: '200': description: Random UUID v4 in JSON. content: application/json: schema: type: object properties: uuid: type: string example: "ee4610df-3b7b-4624-8ce9-da6975b3e9f1" '/cookies': get: tags: - Cookies summary: Cookie data. description: Returns cookie data. responses: '200': description: Set cookies. content: application/json: schema: type: object properties: cookies: type: object '/cookies/delete': parameters: - name: freeform in: query style: "form" get: tags: - Cookies summary: Delete cookies. description: | Deletes cookie(s) as provided by the query string and redirects to cookie list. responses: '302': description: Redirect to the cookie list. '/cookies/set': parameters: - name: freeform in: query style: "form" get: tags: - Cookies summary: Set cookies. description: | Sets cookie(s) as provided by the query string and redirects to cookie list. responses: '302': description: Redirect to the cookie list. '/cookies/set/:name/:value': parameters: - name: name in: path description: Cookie name. schema: type: string - name: value in: path description: Cookie value. schema: type: string get: tags: - Cookies summary: Set one cookie. description: | Sets a cookie and redirects to cookie list. responses: '302': description: Redirect to the cookie list. '/image': parameters: - name: Accept in: header description: Accepted image content type. schema: type: string get: tags: - Images summary: Return an image. description: | It selects the format according to the `Accept` request header. Supported formats: `image/jpeg`, `image/png`, `image/svg+xml`, `image/webp`, `image/*`. responses: '200': description: Image file. content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary image/svg+xml: schema: type: string format: binary image/webp: schema: type: string format: binary '406': description: The client did not request a supported media type. content: application/json: schema: type: object properties: message: type: string example: "Client did not request a supported media type." accept: type: array items: type: string example: - "image/jpeg" - "image/png" - "image/svg+xml" - "image/webp" '/image/:format': parameters: - name: format description: Image format. May be `jpeg`, `png`, `svg` or `webp`. in: path schema: type: string required: true get: tags: - Images summary: Return an image of the specified format. responses: '200': description: Image file. content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary image/svg+xml: schema: type: string format: binary image/webp: schema: type: string format: binary '/absolute-redirect/:n': parameters: - name: n in: path required: true description: Number of times to redirect. Maximum is five. schema: type: integer get: tags: - Redirects summary: Redirect to an absolute URL, n times. responses: '302': description: Redirect. '/relative-redirect/:n': parameters: - name: n in: path description: Number of times to redirect. Maximum is five. schema: type: integer required: true get: tags: - Redirects summary: Redirect to a relative URL, n times. responses: '302': description: Redirect. '/redirect-to': parameters: - name: url in: query description: URL to redirect to. schema: type: string - name: status_code in: query description: HTTP status code to use for the redirection. schema: type: integer default: 302 delete: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) get: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) patch: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) post: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) put: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) '/anything': delete: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' get: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' patch: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' post: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' put: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' components: parameters: status: name: status in: path required: true description: Status code. schema: type: integer schemas: get: properties: args: type: object headers: type: object origin: type: string path: type: string url: type: string required: - args - headers - origin - path - url common: properties: args: type: object data: type: object files: type: object form: type: object headers: type: object json: type: object method: type: string path: type: string origin: type: string url: type: string webfakes/inst/examples/httpbin/app.R0000644000176200001440000000014414172041777017220 0ustar liggesusers library(webfakes) app <- httpbin_app() app$listen(as.integer(Sys.getenv("PORT", NA_character_))) webfakes/inst/examples/hello/0000755000176200001440000000000014172041777015751 5ustar liggesuserswebfakes/inst/examples/hello/views/0000755000176200001440000000000014172041777017106 5ustar liggesuserswebfakes/inst/examples/hello/views/test.txt0000644000176200001440000000003114172041777020620 0ustar liggesusersThis is {2-1} glue test! webfakes/inst/examples/hello/app.R0000644000176200001440000000070514172041777016656 0ustar liggesusers library(webfakes) app <- new_app() app$engine("txt", tmpl_glue()) app$use(mw_log()) app$get("/hello", function(req, res) { res$send("Hello there!") }) app$get(new_regexp("^/hi(/.*)?$"), function(req, res) { res$send("Hi indeed!") }) app$post("/hello", function(req, res) { res$send("Got it, thanks!") }) app$get("/view", function(req, res) { txt <- res$render("test") res$ set_type("text/plain")$ send(txt) }) app$listen(3000L) webfakes/inst/examples/static/0000755000176200001440000000000014172041777016135 5ustar liggesuserswebfakes/inst/examples/static/public/0000755000176200001440000000000014172041777017413 5ustar liggesuserswebfakes/inst/examples/static/public/foo/0000755000176200001440000000000014172041777020176 5ustar liggesuserswebfakes/inst/examples/static/public/foo/bar.html0000644000176200001440000000006414172041777021630 0ustar liggesusersHello world! webfakes/inst/examples/static/public/foo/bar.json0000644000176200001440000000005314172041777021633 0ustar liggesusers{ "foo": "bar", "bar": [1, 2, 3] } webfakes/inst/examples/static/public/bar/0000755000176200001440000000000014172041777020157 5ustar liggesuserswebfakes/inst/examples/static/public/bar/foo.txt0000644000176200001440000000002514172041777021500 0ustar liggesusersThis is a text file. webfakes/inst/examples/static/app.R0000644000176200001440000000022014172041777017032 0ustar liggesusers library(webfakes) app <- new_app() app$use(mw_log()) app$use(mw_static("public")) app$listen(as.integer(Sys.getenv("PORT", NA_character_))) webfakes/inst/credits/0000755000176200001440000000000014172041777014465 5ustar liggesuserswebfakes/inst/credits/redoc.md0000644000176200001440000001333414172041777016107 0ustar liggesusers See https://github.com/Redocly/redoc#readme for Redoc. Redoc authors: * Roman Hotsiy * Cesar * Dimitar Nanov * Ivan Goncharov * Anya Stasiuk * Bohdan Khorolets * Brendan Abbott * Ben Firshman * brushmate * Adam Altman * Jérémy Derussé * Matthias Mohr * Mike Stead * kedashoe * leoliu * amanganiello * Patrick Elam <40776618+patrickelam@users.noreply.github.com> * Mohamed Zenadi * Sergey Dubovyk * Zakary Kamal Ismail * Jon Nicholson * Alex * TATSUNO Yasuhiro * Oleksiy Kachynskyy * Mike Ralphson * Phil Sturgeon * Dimitar Nanov * Faheem Abrar * Fredrik Lengstrand <10126466+fredriklengstrand@users.noreply.github.com> * Gaurav Jain * George Wilson * GreenHedgehog * Homa Wong * Ingo Claro * Jacob Baskin * Jean-Daniel * Joe Honzawa * Josh Price * Julian <2564520+julmuell@users.noreply.github.com> * Julian Kühnel * Julien Feltesse * Khoa Tran * Kris Kalavantavanich * Kryštof Korb * LeFnord * Leonard Ehrenfried * Lev Pachmanov <31389480+levpachmanov@users.noreply.github.com> * Luciano Jr * Luigi Pinca * Making GitHub Delicious * Marius Rumpf * Mason Malone * Mathias Schreck * Melvyn Sopacua <40824897+mes3yd@users.noreply.github.com> * Melvyn Sopacua * Michael Huynh <43751307+miqh@users.noreply.github.com> * Morgan Terry * Nan Yan <625518543@qq.com> * Nick Oliver * Oleg Vaskevich * Patrick Niklaus * Patrick Rodacker * Pete Nykänen * Peter Golm * Peter Wessels * Petr Flaks * Primož Pincolič * Quinn Blenkinsop * Robert DeRose * Sander van de Graaf * Sebastián Ramírez * Sergio Regueira * SeungWoon Maz Lee * Sheila Kelly <36901025+viralanomaly@users.noreply.github.com> * Siarhei Bautrukevich * Skyler Lewis * SoftBrix * Sérgio Ramos * Tim Hordern * Vincent Giersch * Vladimir L * William Boman * Zach Pomerantz * bwjohnson-ss <41123899+bwjohnson-ss@users.noreply.github.com> * davchen51 <46250804+davchen51@users.noreply.github.com> * duxiaofeng * fritz-c * jsmartfo * kmbenitez * lrobledo * lscholten * mknoszlig * neumond * pengfluf * russellrobinson * tomjankes * torbenw * travis@localhost * unarist * zhzxang * Adam Altman * Adam DuVander * Adrien Gallou * Aleksandr Karo * Alex Scammon * Anastasiya Mashoshyna * Andrew Berry * Andrew Zhukevych <34597767+Hollister009@users.noreply.github.com> * Andrii Tykhan * Antherkiv * Anthony Porthouse * Anto * Anton Komarev <1849174+antonkomarev@users.noreply.github.com> * Benny Neugebauer * Blake Erickson * Brendan Abbott * Cesar Landeros Delgado * Cesar Level * Chris Faulkner * Cédric Bertolini <41571181+wizacedric@users.noreply.github.com> * Cédric Fabianski * Daniel Chao * Dave Oram * David Beacham * David Cumps * David J. Felix * David Revay * DeBr0glie The authors were extracted from the git history: ```sh git clone https://github.com/Redocly/redoc.git cd redoc git shortlog --summary --numbered --email --all | cut -f2 | sed 's/^/* /' ``` webfakes/inst/credits/ciwetweb.md0000644000176200001440000000750214172041777016624 0ustar liggesusers This is from https://github.com/civetweb/civetweb/blob/master/CREDITS.md # Civetweb Contributors * Abhishek Lekshmanan * Abramo Bagnara * Adam Bailey * Alan Somers * Alberto Bignotti * Alex Kozlov * AndreyArsov * Anton Te * beaver * bel2125 * Ben M. Ward * Bernhard Lehner * BigJoe * Bjoern Petri * Braedy Kuzma * Breno Ramalho Lemes * brett * Brian Lambert * Brian Spratke * cdbishop * celeron55 * Charles Olivi * Chris Han * Chris Jones * Chris Rehn * Christian Mauderer * Christopher Galas * cjh * Colden Cullen * Colm Sloan * Cortronic * Daniel Oaks * Daniel Rempel * Danny Al-Gaaf * Dave Brower * daveelton * David Arnold * David Loffredo * Dialga * Domenico Di Iorio * Drew Wells * duong2179 * ehlertjd * eugene * Eric Tsau * Erick Vieyra * Erik Beran * Erik Partridge * extergnoto * F-Secure Corporation * Fabrice Fontaine * feneuilflo * Fernando G. Aranda * Frank Hilliger * Grahack * Gregor Jasny * grenclave * grunk * guangqing.chen * Guilherme Amadio * hansipie * HariKamath Kamath * Henry Chang * Herumb Shandilya * Herve Codina * Iain Morton * ImgBotApp * Ivan Dlugos * Jack * Jacob Repp * Jacob Skillin * Jan Kowalewski * Jan Willem Janssen * Jeremy Lin * Jesse Williamson * Jim Evans * jmc- * Joakim L. Gilje * Jochen Scheib * Joe Mucchiello * Joel Gallant * Johan De Taeye * John Faith * Jordan * Jordan Shelley * Joshua Boyd * Joshua D. Boyd * k-mds * kakwa * kalphamon * Karol Mroz * Keith Holman * Keith Kyzivat * Ken Walters * Kevin Branigan * Kevin Wojniak * Khem Raj * Kimmo Mustonen * Krzysztof Kozlowski * Lammert Bies * Lars Immisch * Lawrence * Li Peng * Lianghui * Luka Rahne * Lukas Martanovic * Maarten Fremouw * makrsmark * marco * Mark Lakata * Martin Gaida * Mateusz Gralka * Matt Clarkson * Mellnik * Mike Crowe * mingodad * Morgan McGuire * mrdvlpr.xnu * Nat! * Neil Jensen * newsoft * nfrmtkr * Nick Hildebrant * Nigel Stewart * nihildeb * No Face Press * palortoff * Patrick Drechsler * Patrick Trinkle * Paul Sokolovsky * Paulo Brizolara * pavel.pimenov * PavelVozenilek * Perttu Ahola * Peter Foerster * Philipp Friedenberger * Philipp Hasper * Piotr Zierhoffer * pkvamme * Radoslaw Zarzynski * Red54 * Retallack Mark mark.retallack * Richard Screene * Rimas Misevi-ìius * Rinat Dobrokhotov * ryankopf * Sage Weil * Sangwhan Moon * Saumitra Vikram * Scott Nations * Sebastien Jodogne * Sergey Linev * sgmesservey * shantanugadgil * Sherwyn Sen * shreyajaggi8 * Simon Hailes * slidertom * SpaceLord * sunfch * suzukibitman * Símal Rasmussen * Tamotsu Kanoh * thewaterymoon * Thiago Macedo * THILMANT, Bernard * Thomas Davis * Thomas Klausner * Thorsten Horstmann * tnoho * Tom Deblauwe * Tomas Andrle * Tomasz Gorochowik * Toni Wilk * Torben Jonas * Uilian Ries * Ulrich Hertlein * Walt Steverson * wangli28 * webxer * William Greathouse * xeoshow * xtne6f * Yehuda Sadeh * Yury Z * zhen.wang and others. # Mongoose Contributors CivetWeb is based on the Mongoose code. The following users contributed to the original Mongoose release between 2010 and 2013. This list was generated from the Mongoose GIT logs. It does not contain contributions from the Mongoose mailing list. There is no record for contributors prior to 2010. * Sergey Lyubka * Arnout Vandecappelle (Essensium/Mind) * Benoît Amiaux * Cody Hanson * Colin Leitner * Daniel Oaks * Eric Bakan * Erik Oomen * Filipp Kovalev * Ger Hobbelt * Hendrik Polczynski * Henrique Mendonça * Igor Okulist * Jay * Joe Mucchiello * John Safranek * Joseph Mainwaring * José Miguel Gonçalves * KIU Shueng Chuan * Katerina Blinova * Konstantin Sorokin * Marin Atanasov Nikolov * Matt Healy * Miguel Morales * Mikhail Nikalyukin * MikieMorales * Mitch Hendrickson * Nigel Stewart * Pavel * Pavel Khlebovich * Rogerz Zhang * Sebastian Reinhard * Stefan Doehla * Thileepan * abadc0de * arvidn * bick * ff.feng * jmucchiello * jwang * lsm * migal * mlamb * nullable.type * shantanugadgil * tayS * test * valenok