File: static_paths.R

package info (click to toggle)
r-cran-httpuv 1.6.15%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,292 kB
  • sloc: ansic: 6,499; cpp: 5,501; makefile: 103; sh: 56
file content (326 lines) | stat: -rw-r--r-- 10,183 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#' Create a staticPath object
#'
#' The \code{staticPath} function creates a \code{staticPath} object. Note that
#' if any of the arguments (other than \code{path}) are \code{NULL}, then that
#' means that for this particular static path, it should inherit the behavior
#' from the staticPathOptions set for the application as a whole.
#'
#' The \code{excludeStaticPath} function tells the application to ignore a
#' particular path for static serving. This is useful when you want to include a
#' path for static serving (like \code{"/"}) but then exclude a subdirectory of
#' it (like \code{"/dynamic"}) so that the subdirectory will always be passed to
#' the R code for handling requests. \code{excludeStaticPath} can be used not
#' only for directories; it can also exclude specific files.
#'
#' @param path The local path.
#' @inheritParams staticPathOptions
#'
#' @seealso \code{\link{staticPathOptions}}.
#'
#' @export
staticPath <- function(
  path,
  indexhtml    = NULL,
  fallthrough  = NULL,
  html_charset = NULL,
  headers      = NULL,
  validation   = NULL
) {
  if (!is.character(path) || length(path) != 1 || path == "") {
    stop("`path` must be a non-empty string.")
  }

  path <- normalizePath(path, winslash = "/", mustWork = TRUE)
  path <- enc2utf8(path)

  structure(
    list(
      path = path,
      options = normalizeStaticPathOptions(staticPathOptions(
        indexhtml    = indexhtml,
        fallthrough  = fallthrough,
        html_charset = html_charset,
        headers      = headers,
        validation   = validation,
        exclude      = FALSE
      ))
    ),
    class = "staticPath"
  )
}

#' @rdname staticPath
#' @export
excludeStaticPath <- function() {
  structure(
    list(
      path = "",
      options = staticPathOptions(
        indexhtml    = NULL,
        fallthrough  = NULL,
        html_charset = NULL,
        headers      = NULL,
        validation   = NULL,
        exclude      = TRUE
      )
    ),
    class = "staticPath"
  )
}

as.staticPath <- function(path) {
  UseMethod("as.staticPath", path)
}

as.staticPath.staticPath <- function(path) {
  path
}

as.staticPath.character <- function(path) {
  staticPath(path)
}

as.staticPath.default <- function(path) {
  stop("Cannot convert object of class ", class(path), " to a staticPath object.")
}

#' @export
print.staticPath <- function(x, ...) {
  cat(format(x, ...), sep = "\n")
  invisible(x)
}

#' @export
format.staticPath <- function(x, ...) {
  ret <- paste0(
    "<staticPath>\n",
    "  Local path:        ", x$path, "\n",
    format_opts(x$options)
  )
}

#' Create options for static paths
#'
#'
#' @param indexhtml If an index.html file is present, should it be served up
#'   when the client requests the static path or any subdirectory?
#' @param fallthrough With the default value, \code{FALSE}, if a request is made
#'   for a file that doesn't exist, then httpuv will immediately send a 404
#'   response from the background I/O thread, without needing to call back into
#'   the main R thread. This offers the best performance. If the value is
#'   \code{TRUE}, then instead of sending a 404 response, httpuv will call the
#'   application's \code{call} function, and allow it to handle the request.
#' @param html_charset When HTML files are served, the value that will be
#'   provided for \code{charset} in the Content-Type header. For example, with
#'   the default value, \code{"utf-8"}, the header is \code{Content-Type:
#'   text/html; charset=utf-8}. If \code{""} is used, then no \code{charset}
#'   will be added in the Content-Type header.
#' @param headers Additional headers and values that will be included in the
#'   response.
#' @param validation An optional validation pattern. Presently, the only type of
#'   validation supported is an exact string match of a header. For example, if
#'   \code{validation} is \code{'"abc" = "xyz"'}, then HTTP requests must have a
#'   header named \code{abc} (case-insensitive) with the value \code{xyz}
#'   (case-sensitive). If a request does not have a matching header, than httpuv
#'   will give a 403 Forbidden response. If the \code{character(0)} (the
#'   default), then no validation check will be performed.
#' @param exclude Should this path be excluded from static serving? (This is
#'   only to be used internally, for \code{\link{excludeStaticPath}}.)
#'
#' @export
staticPathOptions <- function(
  indexhtml    = TRUE,
  fallthrough  = FALSE,
  html_charset = "utf-8",
  headers      = list(),
  validation   = character(0),
  exclude      = FALSE
) {
  res <- structure(
    list(
      indexhtml    = indexhtml,
      fallthrough  = fallthrough,
      html_charset = html_charset,
      headers      = headers,
      validation   = validation,
      exclude      = exclude
    ),
    class = "staticPathOptions"
  )

  normalizeStaticPathOptions(res)
}

#' @export
print.staticPathOptions <- function(x, ...) {
  cat(format(x, ...), sep = "\n")
  invisible(x)
}


#' @export
format.staticPathOptions <- function(x, ...) {
  paste0(
    "<staticPathOptions>\n",
    format_opts(x, format_empty = "<none>")
  )
}

format_opts <- function(x, format_empty = "<inherit>") {
  format_option <- function(opt) {
    if (is.null(opt) || length(opt) == 0) {
      format_empty

    } else if (!is.null(names(opt))) {
      # Named character vector
      lines <- mapply(
        function(name, value) paste0('    "', name, '" = "', value, '"'),
        names(opt),
        opt,
        SIMPLIFY = FALSE,
        USE.NAMES = FALSE
      )

      lines <- paste(as.character(lines), collapse = "\n")
      lines <- paste0("\n", lines)
      lines

    } else {
      paste(as.character(opt), collapse = " ")
    }
  }
  ret <- paste0(
    "  Use index.html:    ", format_option(x$indexhtml),    "\n",
    "  Fallthrough to R:  ", format_option(x$fallthrough),  "\n",
    "  HTML charset:      ", format_option(x$html_charset), "\n",
    "  Extra headers:     ", format_option(x$headers),      "\n",
    "  Validation params: ", format_option(x$validation),   "\n",
    "  Exclude path:      ", format_option(x$exclude),      "\n"
  )
}


# This function always returns a named list of staticPath objects. The names
# will all start with "/". The input can be a named character vector or a
# named list containing a mix of strings and staticPath objects. This function
# is idempotent.
normalizeStaticPaths <- function(paths) {
  if (is.null(paths) || length(paths) == 0) {
    return(list())
  }

  if (any_unnamed(paths)) {
    stop("paths must be a named character vector, a named list containing strings and staticPath objects, or NULL.")
  }

  if (!is.character(paths) && !is.list(paths)) {
    stop("paths must be a named character vector, a named list containing strings and staticPath objects, or NULL.")
  }

  # Convert to list of staticPath objects. Need this verbose wrapping of
  # as.staticPath because of S3 dispatch for non-registered methods.
  paths <- lapply(paths, function(path) as.staticPath(path))

  # Make sure URL paths have a leading '/' and no trailing '/'.
  names(paths) <- vapply(names(paths), function(path) {
    path <- enc2utf8(path)

    if (path == "") {
      stop("All paths must be non-empty strings.")
    }
    # Ensure there's a leading / for every path
    if (substr(path, 1, 1) != "/") {
      path <- paste0("/", path)
    }
    # Strip trailing slashes, except when the path is just "/".
    if (path != "/") {
      path <- sub("/+$", "", path)
    }

    path
  }, "")

  paths
}

# Takes a staticPathOptions object and modifies it so that the resulting
# object is easier to work with on the C++ side. The resulting object is not
# meant to be modified on the R side. This function is idempotent; if the
# object has already been normalized, it will not be modified. For each entry,
# a NULL means to inherit.
normalizeStaticPathOptions <- function(opts) {
  if (isTRUE(attr(opts, "normalized", exact = TRUE))) {
    return(opts)
  }

  # html_charset can accept "" or character(0). But on the C++ side, we want
  # "".
  if (!is.null(opts$html_charset)) {
    if (length(opts$html_charset) == 0) {
      opts$html_charset <- ""
    }
  }

  if (!is.null(opts$exclude)) {
    if (!is.logical(opts$exclude) || length(opts$exclude) != 1) {
      stop("`exclude` option must be TRUE or FALSE.")
    }
  }

  # Can be a named list of strings, or a named character vector. On the C++
  # side, we want a named character vector.
  if (is.list(opts$headers)) {
    # Convert list to named character vector
    opts$headers <- unlist(opts$headers, recursive = FALSE)
    # Special case: if opts$headers was an empty list before unlist(), it is
    # now NULL. Replace it with an empty named character vector.
    if (length(opts$headers) == 0) {
      opts$headers <- c(a="a")[0]
    }

    if (!is.character(opts$headers) || any_unnamed(opts$headers)) {
      stop("`headers` option must be a named list or character vector.")
    }
  }

  if (!is.null(opts$validation)) {
    if (!is.character(opts$validation) || length(opts$validation) > 1) {
      stop("`validation` option must be a character vector with zero or one element.")
    }

    # Both "" and character(0) result in character(0). Length-1 strings other
    # than "" will be parsed.
    if (length(opts$validation) == 1) {
      if (opts$validation == "") {
        opts$validation <- character(0)

      } else {
        fail <- FALSE
        tryCatch(
          p <- parse(text = opts$validation)[[1]],
          error = function(e) fail <<- TRUE
        )
        if (!fail) {
          if (length(p) != 3            ||
              p[[1]] != as.symbol("==") ||
              !is.character(p[[2]])     ||
              length(p[[2]]) != 1       ||
              !is.character(p[[3]])     ||
              length(p[[3]]) != 1)
          {
            fail <- TRUE
          }
        }
        if (fail) {
          stop("`validation` must be a string of the form: '\"xxx\" == \"yyy\"'")
        }

        # Turn it into a char vector for easier processing in C++
        opts$validation <- as.character(p)
      }
    }
  }

  attr(opts, "normalized") <- TRUE
  opts
}