├── configure.win ├── .covrignore ├── client ├── src │ ├── index.js │ ├── assets │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── icons │ │ │ ├── hamburger.svg │ │ │ ├── arrow-right.svg │ │ │ ├── arrow-left.svg │ │ │ ├── export.svg │ │ │ ├── cross.svg │ │ │ ├── download.svg │ │ │ ├── image.svg │ │ │ ├── copy.svg │ │ │ ├── vdots.svg │ │ │ ├── magnify-minus.svg │ │ │ ├── trash.svg │ │ │ └── magnify-plus.svg │ │ └── plot-none.svg │ ├── index.ts │ ├── resources.ts │ ├── index.ejs │ ├── app.ts │ ├── views │ │ ├── container.ejs │ │ ├── overlayView.ts │ │ ├── exportDialog.ejs │ │ ├── toolbar.ejs │ │ ├── toolbarView.ts │ │ ├── toolbarData.ts │ │ ├── sidebarView.ts │ │ ├── plotView.ts │ │ └── exportView.ts │ ├── typings │ │ └── clipboard.d.ts │ ├── utils.ts │ ├── viewer.ts │ └── style │ │ └── style.scss ├── .eslintignore ├── tsconfig.json ├── .eslintrc ├── package.json ├── webpack.config.js └── .gitignore ├── .github ├── .gitignore ├── ISSUE_TEMPLATE │ ├── other.md │ ├── bug_report.md │ └── feature-request.md ├── dependabot.yaml └── workflows │ ├── lint.yml │ ├── pkgdown.yaml │ ├── R-CMD-check.yaml │ ├── test-coverage.yaml │ └── rhub.yaml ├── vignettes ├── .gitignore ├── b01_vscode.Rmd ├── a00_installation.Rmd ├── b02_rstudio.Rmd ├── a01_how-to-get-started.Rmd ├── b03_docker.Rmd └── c01_httpgd-api.Rmd ├── cleanup ├── configure ├── tests ├── testthat.R └── testthat │ ├── test-security.R │ ├── helper-server.R │ └── test-server.R ├── _pkgdown.yml ├── inst ├── www │ ├── favicon.ico │ ├── 199bb9cab408c177.png │ ├── e13eb9df618a2fbe.png │ ├── 18c7a89e30a180cb.svg │ ├── style.css │ └── index.html └── licenses │ ├── belle-MIT.txt │ └── CrowCpp-BSD-3-Clause.txt ├── src ├── httpgd_version.h ├── Makevars.in ├── lib │ ├── crow │ │ ├── version.h │ │ ├── returnable.h │ │ ├── middlewares │ │ │ ├── utf-8.h │ │ │ ├── cors.h │ │ │ └── cookie_parser.h │ │ ├── ci_map.h │ │ ├── settings.h │ │ ├── middleware_context.h │ │ ├── http_request.h │ │ ├── compression.h │ │ ├── mime_types.h │ │ ├── socket_adaptors.h │ │ ├── logging.h │ │ ├── task_timer.h │ │ ├── TinySHA1.hpp │ │ └── parser.h │ └── crow.h ├── Makevars.ucrt ├── unigd_impl.h ├── httpgd_rng.h ├── Makevars.win ├── unigd_impl.cpp ├── httpgd_rng.cpp ├── cpp11.cpp ├── httpgd_webserver.h ├── httpgd.cpp └── optional_lex.h ├── .clang-format ├── .gitignore ├── codecov.yml ├── LICENSE.note ├── tools └── winlibs.R ├── valgrind.dockerfile ├── R ├── cpp11.R └── httpgd-package.R ├── .Rbuildignore ├── man ├── hgd_generate_token.Rd ├── hgd_view.Rd ├── hgd_browse.Rd ├── hgd_close.Rd ├── httpgd-package.Rd ├── hgd_details.Rd ├── hgd_url.Rd ├── hgd_watch.Rd └── hgd.Rd ├── NAMESPACE ├── cran-comments.md ├── DESCRIPTION ├── NEWS.md └── README.md /configure.win: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.covrignore: -------------------------------------------------------------------------------- 1 | src/lib 2 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /cleanup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -f src/Makevars configure.log -------------------------------------------------------------------------------- /client/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | webpack.config.js -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | cat src/Makevars.in > src/Makevars 2 | 3 | # Success 4 | exit 0 -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(httpgd) 3 | 4 | test_check("httpgd") -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://nx10.github.io/httpgd 2 | template: 3 | bootstrap: 5 4 | -------------------------------------------------------------------------------- /inst/www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nx10/httpgd/HEAD/inst/www/favicon.ico -------------------------------------------------------------------------------- /src/httpgd_version.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTPGD_VERSION 2 | #define HTTPGD_VERSION "2.0.1" 3 | #endif 4 | -------------------------------------------------------------------------------- /client/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nx10/httpgd/HEAD/client/src/assets/favicon.ico -------------------------------------------------------------------------------- /inst/www/199bb9cab408c177.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nx10/httpgd/HEAD/inst/www/199bb9cab408c177.png -------------------------------------------------------------------------------- /inst/www/e13eb9df618a2fbe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nx10/httpgd/HEAD/inst/www/e13eb9df618a2fbe.png -------------------------------------------------------------------------------- /client/src/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nx10/httpgd/HEAD/client/src/assets/favicon-16x16.png -------------------------------------------------------------------------------- /client/src/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nx10/httpgd/HEAD/client/src/assets/favicon-32x32.png -------------------------------------------------------------------------------- /src/Makevars.in: -------------------------------------------------------------------------------- 1 | PKG_CPPFLAGS = -Ilib -DFMT_HEADER_ONLY 2 | 3 | all: clean 4 | 5 | clean: 6 | rm -f $(SHLIB) $(OBJECTS) -------------------------------------------------------------------------------- /src/lib/crow/version.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace crow 4 | { 5 | constexpr const char VERSION[] = "master"; 6 | } 7 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | BreakBeforeBraces: Allman 4 | ColumnLimit: '90' 5 | SortIncludes: 'true' 6 | 7 | ... 8 | -------------------------------------------------------------------------------- /client/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./style/style.scss"; 2 | import App from "./app" ; 3 | 4 | window.onload = function () { 5 | App.viewer.init(); 6 | } -------------------------------------------------------------------------------- /client/src/resources.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | export const ASSET_PLOT_NONE: string = require('./assets/plot-none.svg'); -------------------------------------------------------------------------------- /client/src/assets/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: Something other than a bug report or feature request 4 | title: '' 5 | labels: 6 | assignees: '' 7 | 8 | --- 9 | 10 | -------------------------------------------------------------------------------- /src/Makevars.ucrt: -------------------------------------------------------------------------------- 1 | PKG_CPPFLAGS = -Ilib \ 2 | -DFMT_HEADER_ONLY \ 3 | -DHTTPGD_DEBUG_DEVICE 4 | 5 | PKG_LIBS = -lmswsock -lwsock32 -lws2_32 -lbcrypt 6 | 7 | all: clean 8 | 9 | clean: 10 | rm -f $(OBJECTS) -------------------------------------------------------------------------------- /client/src/assets/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/assets/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/assets/icons/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | src/*.o 6 | src/*.so 7 | src/*.dll 8 | .vscode 9 | windows 10 | *.Rproj 11 | inst/doc 12 | revdep 13 | configure.log 14 | src/Makevars 15 | docs 16 | src-i386 17 | src-x64 18 | -------------------------------------------------------------------------------- /client/src/assets/icons/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/assets/icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/assets/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | } 11 | } -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /client/src/assets/icons/vdots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /inst/www/18c7a89e30a180cb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/plot-none.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/unigd_impl.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_UNIGD_IMPL_H__ 2 | #define __UNIGD_UNIGD_IMPL_H__ 3 | 4 | #include "unigd_api_v1.h" 5 | 6 | namespace httpgd 7 | { 8 | namespace ugd 9 | { 10 | extern unigd_api_v1 *api; 11 | extern UNIGD_CLIENT_ID httpgd_client_id; 12 | } // namespace ugd 13 | 14 | } // namespace httpgd 15 | 16 | #endif /* __UNIGD_UNIGD_IMPL_H__ */ 17 | -------------------------------------------------------------------------------- /LICENSE.note: -------------------------------------------------------------------------------- 1 | The httpgd package as a whole is distributed under GPL-2. The httpgd package includes other open source software components. The following is a list of these components: 2 | 3 | * fmt: MIT 4 | * belle: MIT 5 | * svglite: GPL-2 | GPL-3 6 | * material-design-icons: Apache 2.0 7 | 8 | Full copies of the license agreements used by these components are included in `inst/licenses`. -------------------------------------------------------------------------------- /tools/winlibs.R: -------------------------------------------------------------------------------- 1 | VERSION <- commandArgs(TRUE) 2 | if(!file.exists(sprintf("../windows/harfbuzz-%s/include/png.h", VERSION))){ 3 | if(getRversion() < "3.3.0") setInternet2() 4 | download.file(sprintf("https://github.com/rwinlib/harfbuzz/archive/v%s.zip", VERSION), "lib.zip", quiet = TRUE) 5 | dir.create("../windows", showWarnings = FALSE) 6 | unzip("lib.zip", exdir = "../windows") 7 | unlink("lib.zip") 8 | } 9 | -------------------------------------------------------------------------------- /valgrind.dockerfile: -------------------------------------------------------------------------------- 1 | FROM rocker/r-devel-san:latest 2 | 3 | WORKDIR /app 4 | COPY . . 5 | 6 | RUN R -e "install.packages('pak', repos = 'http://cran.us.r-project.org')" 7 | RUN apt-get install -y --no-install-recommends libxml2-dev libssl-dev 8 | RUN R -e "pak::pkg_install('.', dependencies=TRUE)" 9 | 10 | ENTRYPOINT ["/bin/sh"] 11 | 12 | # Use this: cd tests && R -d "valgrind --leak-check=full" -f testthat.R -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-inferrable-types": 0 14 | } 15 | } -------------------------------------------------------------------------------- /client/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | R Plot 6 | 7 | 8 | 9 | 10 | 11 | <%- include views/container %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/assets/icons/magnify-minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/httpgd_rng.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_HTTPGD_RNG_H__ 2 | #define __UNIGD_HTTPGD_RNG_H__ 3 | 4 | #include 5 | 6 | namespace httpgd 7 | { 8 | // Can not use R's RNG for this for security reasons. 9 | // (Seed could be predicted) 10 | namespace rng 11 | { 12 | std::string uuid(); 13 | std::string token(int length); 14 | } // namespace rng 15 | } // namespace httpgd 16 | 17 | #endif /* __UNIGD_HTTPGD_RNG_H__ */ 18 | -------------------------------------------------------------------------------- /client/src/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /R/cpp11.R: -------------------------------------------------------------------------------- 1 | # Generated by cpp11: do not edit by hand 2 | 3 | httpgd_ <- function(devnum, host, port, cors, token, silent, wwwpath) { 4 | .Call(`_httpgd_httpgd_`, devnum, host, port, cors, token, silent, wwwpath) 5 | } 6 | 7 | httpgd_details_ <- function(devnum) { 8 | .Call(`_httpgd_httpgd_details_`, devnum) 9 | } 10 | 11 | httpgd_random_token_ <- function(len) { 12 | .Call(`_httpgd_httpgd_random_token_`, len) 13 | } 14 | -------------------------------------------------------------------------------- /tests/testthat/test-security.R: -------------------------------------------------------------------------------- 1 | test_that("User defined token", { 2 | testtok <- "123abc" 3 | hgd(token = testtok, silent = TRUE) 4 | hs <- hgd_details() 5 | dev.off() 6 | expect_equal(hs$token, testtok) 7 | }) 8 | 9 | test_that("Token R seed independence", { 10 | set.seed(1234) 11 | a <- hgd_generate_token(8) 12 | set.seed(1234) 13 | b <- hgd_generate_token(8) 14 | expect_false(isTRUE(all.equal(a, b))) 15 | }) 16 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/client" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | production-dependencies: 9 | dependency-type: production 10 | development-dependencies: 11 | dependency-type: development 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /src/Makevars.win: -------------------------------------------------------------------------------- 1 | VERSION_HARFBUZZ = 2.7.4 2 | 3 | RWINLIB_HARFBUZZ = ../windows/harfbuzz-${VERSION_HARFBUZZ} 4 | 5 | PKG_CPPFLAGS = -Ilib -I${RWINLIB_HARFBUZZ}/include \ 6 | -DFMT_HEADER_ONLY 7 | 8 | PKG_LIBS = -L${RWINLIB_HARFBUZZ}/lib${R_ARCH}${CRT} -lWs2_32 -lwsock32 9 | 10 | all: winlibs 11 | 12 | winlibs: 13 | "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" "../tools/winlibs.R" ${VERSION_HARFBUZZ} 14 | 15 | clean: 16 | rm -f $(OBJECTS) 17 | -------------------------------------------------------------------------------- /client/src/assets/icons/magnify-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^.vscode.*$ 4 | ^docs/$ 5 | ^docs$ 6 | ^.github$ 7 | ^.git$ 8 | ^LICENSE$ 9 | ^windows$ 10 | ^.*\.ts$ 11 | ^.*tsconfig.json$ 12 | ^\.github$ 13 | ^cran-comments\.md$ 14 | ^CRAN-RELEASE$ 15 | ^revdep$ 16 | ^configure.log$ 17 | ^src/Makevars$ 18 | ^client/$ 19 | ^client$ 20 | ^_pkgdown\.yml$ 21 | ^pkgdown$ 22 | ^CRAN-SUBMISSION$ 23 | ^codecov\.yml$ 24 | ^\.covrignore$ 25 | ^\.clang-format$ 26 | ^valgrind.dockerfile$ 27 | -------------------------------------------------------------------------------- /client/src/app.ts: -------------------------------------------------------------------------------- 1 | import { HttpgdViewer } from "./viewer"; 2 | 3 | const sparams = new URL(window.location.href).searchParams; 4 | 5 | export default { 6 | viewer: new HttpgdViewer( 7 | sparams.get("hgd") || sparams.get("host") || window.location.host, 8 | sparams.get("token") || undefined, 9 | sparams.has("ws") ? (sparams.get("ws") != "0") : true, 10 | sparams.has("sidebar") ? (sparams.get("sidebar") == "0") : false 11 | ) 12 | }; -------------------------------------------------------------------------------- /client/src/views/container.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%- include exportDialog %> 6 | 8 |
9 | <%- include toolbar %> 10 | 11 | 12 |
13 | 14 |
-------------------------------------------------------------------------------- /R/httpgd-package.R: -------------------------------------------------------------------------------- 1 | #' httpgd: HTTP server graphics device 2 | #' 3 | #' Asynchronous HTTP server graphics device. 4 | #' 5 | #' @name httpgd-package 6 | #' @useDynLib httpgd, .registration=TRUE 7 | "_PACKAGE" 8 | 9 | .onLoad <- function(libname, pkgname) { 10 | #httpgd_ipc_open_() 11 | } 12 | 13 | #' @importFrom grDevices dev.list dev.off 14 | .onUnload <- function (libpath) { 15 | hgd_close(all = TRUE) 16 | #httpgd_ipc_close_() 17 | library.dynam.unload("httpgd", libpath) 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/crow/returnable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace crow 6 | { 7 | /// An abstract class that allows any other class to be returned by a handler. 8 | struct returnable 9 | { 10 | std::string content_type; 11 | virtual std::string dump() const = 0; 12 | 13 | returnable(std::string ctype): 14 | content_type{ctype} 15 | {} 16 | 17 | virtual ~returnable(){}; 18 | }; 19 | } // namespace crow 20 | -------------------------------------------------------------------------------- /client/src/typings/clipboard.d.ts: -------------------------------------------------------------------------------- 1 | // from: https://github.com/Microsoft/TypeScript/issues/26728 2 | 3 | interface ClipboardItem { 4 | readonly types: string[] 5 | getType: (type: string) => Promise 6 | } 7 | 8 | declare let ClipboardItem: { 9 | prototype: ClipboardItem 10 | new(objects: Record): ClipboardItem 11 | } 12 | 13 | interface Clipboard { 14 | read?(): Promise> 15 | write?(items: Array): Promise 16 | } -------------------------------------------------------------------------------- /man/hgd_generate_token.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd_generate_token} 4 | \alias{hgd_generate_token} 5 | \title{Generate random alphanumeric token.} 6 | \usage{ 7 | hgd_generate_token(len) 8 | } 9 | \arguments{ 10 | \item{len}{Token length (number of characters).} 11 | } 12 | \value{ 13 | Random token string. 14 | } 15 | \description{ 16 | This is mainly used internally by httpgd, but exposed for 17 | testing purposes. 18 | } 19 | \examples{ 20 | hgd_generate_token(6) 21 | } 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(hgd) 4 | export(hgd_browse) 5 | export(hgd_close) 6 | export(hgd_details) 7 | export(hgd_generate_token) 8 | export(hgd_url) 9 | export(hgd_view) 10 | export(hgd_watch) 11 | importFrom(grDevices,dev.cur) 12 | importFrom(grDevices,dev.list) 13 | importFrom(grDevices,dev.off) 14 | importFrom(unigd,ugd) 15 | importFrom(unigd,ugd_clear) 16 | importFrom(unigd,ugd_close) 17 | importFrom(unigd,ugd_state) 18 | importFrom(utils,browseURL) 19 | importFrom(utils,changedFiles) 20 | importFrom(utils,fileSnapshot) 21 | useDynLib(httpgd, .registration=TRUE) 22 | -------------------------------------------------------------------------------- /vignettes/b01_vscode.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "VS Code" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{VS Code} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | The [R extension](https://github.com/REditorSupport/vscode-R) for [VSCode](https://code.visualstudio.com/) includes a direct integration with `httpgd`: 11 | 12 | ![](https://user-images.githubusercontent.com/33600480/147694280-c5272f76-6b12-4260-928a-d43565068f09.png) 13 | 14 | [See the project wiki](https://github.com/REditorSupport/vscode-R/wiki/Plot-viewer) for more information and setup instructions. 15 | -------------------------------------------------------------------------------- /man/hgd_view.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd_view} 4 | \alias{hgd_view} 5 | \title{Open httpgd URL in the IDE.} 6 | \usage{ 7 | hgd_view() 8 | } 9 | \value{ 10 | \code{viewer} function return value. 11 | } 12 | \description{ 13 | Global option \code{viewer} needs to be set to a function that accepts the client 14 | URL as a parameter. 15 | } 16 | \details{ 17 | This function will only work after starting a device with \code{\link[=hgd]{hgd()}}. 18 | } 19 | \examples{ 20 | \dontrun{ 21 | 22 | hgd() 23 | hgd_view() 24 | hist(rnorm(100)) 25 | 26 | dev.off() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/testthat/helper-server.R: -------------------------------------------------------------------------------- 1 | # On posix all requests mus be done from an extra session 2 | # otherwise the main session will deadlock. 3 | fetch_get <- function(...) { 4 | if (.Platform$OS.type == "windows") { 5 | return( 6 | httr::GET(...) 7 | ) 8 | } else { 9 | return( 10 | (function() { 11 | future::plan("multisession") 12 | v <- future::value(future::future({ 13 | return(httr::GET(...)) 14 | })) 15 | future::plan("sequential") 16 | v 17 | })() 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /src/lib/crow/middlewares/utf-8.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "crow/http_request.h" 3 | #include "crow/http_response.h" 4 | 5 | namespace crow 6 | { 7 | 8 | struct UTF8 9 | { 10 | struct context 11 | {}; 12 | 13 | void before_handle(request& /*req*/, response& /*res*/, context& /*ctx*/) 14 | {} 15 | 16 | void after_handle(request& /*req*/, response& res, context& /*ctx*/) 17 | { 18 | if (get_header_value(res.headers, "Content-Type").empty()) 19 | { 20 | res.set_header("Content-Type", "text/plain; charset=utf-8"); 21 | } 22 | } 23 | }; 24 | 25 | } // namespace crow 26 | -------------------------------------------------------------------------------- /man/hgd_browse.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd_browse} 4 | \alias{hgd_browse} 5 | \title{Open httpgd URL in the browser.} 6 | \usage{ 7 | hgd_browse(..., which = dev.cur(), browser = getOption("browser")) 8 | } 9 | \arguments{ 10 | \item{...}{Parameters passed to \code{\link[=hgd_url]{hgd_url()}}.} 11 | 12 | \item{which}{Which device (ID).} 13 | 14 | \item{browser}{Program to be used as HTML browser.} 15 | } 16 | \value{ 17 | URL. 18 | } 19 | \description{ 20 | This function will only work after starting a device with \code{\link[=hgd]{hgd()}}. 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | hgd() 26 | hgd_browse() # open browser 27 | hist(rnorm(100)) 28 | 29 | dev.off() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/crow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "crow/query_string.h" 3 | #include "crow/http_parser_merged.h" 4 | #include "crow/ci_map.h" 5 | #include "crow/TinySHA1.hpp" 6 | #include "crow/settings.h" 7 | #include "crow/socket_adaptors.h" 8 | #include "crow/json.h" 9 | #include "crow/mustache.h" 10 | #include "crow/logging.h" 11 | #include "crow/task_timer.h" 12 | #include "crow/utility.h" 13 | #include "crow/common.h" 14 | #include "crow/http_request.h" 15 | #include "crow/websocket.h" 16 | #include "crow/parser.h" 17 | #include "crow/http_response.h" 18 | #include "crow/multipart.h" 19 | #include "crow/routing.h" 20 | #include "crow/middleware.h" 21 | #include "crow/middleware_context.h" 22 | #include "crow/compression.h" 23 | #include "crow/http_connection.h" 24 | #include "crow/http_server.h" 25 | #include "crow/app.h" 26 | -------------------------------------------------------------------------------- /man/hgd_close.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd_close} 4 | \alias{hgd_close} 5 | \title{Close httpgd device.} 6 | \usage{ 7 | hgd_close(which = dev.cur(), all = FALSE) 8 | } 9 | \arguments{ 10 | \item{which}{Which device (ID).} 11 | 12 | \item{all}{Should all running httpgd devices be closed.} 13 | } 14 | \value{ 15 | Number and name of the new active device (after the specified device 16 | has been shut down). 17 | } 18 | \description{ 19 | This achieves the same effect as \code{\link[grDevices:dev]{grDevices::dev.off()}}, 20 | but will only close the device if it has the httpgd type. 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | hgd() 26 | hgd_browse() # open browser 27 | hist(rnorm(100)) 28 | hgd_close() # Equvalent to dev.off() 29 | 30 | hgd() 31 | hgd() 32 | hgd() 33 | hgd_close(all = TRUE) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | linux: 7 | if: contains(github.event.head_commit.message, '[ci skip]') == false 8 | strategy: 9 | matrix: 10 | r: [latest] 11 | # r: [3.5, latest, devel] 12 | runs-on: ubuntu-latest 13 | container: rocker/tidyverse:${{ matrix.r }} 14 | env: 15 | NOT_CRAN: true 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install apt-get dependencies 19 | run: | 20 | apt-get update 21 | apt-get install git ssh curl bzip2 libffi-dev -y 22 | - name: Install lintr 23 | run: | 24 | Rscript -e "install.packages('lintr', repos = 'https://cloud.r-project.org')" 25 | shell: 26 | bash 27 | - name: Running lintr 28 | run: | 29 | Rscript -e "library(lintr); lint_package()" 30 | shell: 31 | bash 32 | -------------------------------------------------------------------------------- /client/src/views/overlayView.ts: -------------------------------------------------------------------------------- 1 | import { HttpgdViewer } from '../viewer'; 2 | import { getById } from '../utils' 3 | 4 | export class OverlayView { 5 | static readonly TEXT_CONNECTION_LOST: string = "Connection lost."; 6 | static readonly TEXT_DEVICE_INACTIVE: string = "Device inactive."; 7 | 8 | private viewer: HttpgdViewer; 9 | 10 | private overlayContainer: HTMLElement; 11 | private overlayText: HTMLElement; 12 | 13 | constructor(viewer: HttpgdViewer) { 14 | this.viewer = viewer; 15 | this.overlayContainer = getById("overlay"); 16 | this.overlayText = getById("overlay-text"); 17 | } 18 | 19 | public show(text: string): void { 20 | this.overlayText.innerText = text; 21 | this.overlayContainer.style.display = "inline"; 22 | } 23 | 24 | public hide(): void { 25 | this.overlayContainer.style.display = "none"; 26 | } 27 | } -------------------------------------------------------------------------------- /man/httpgd-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd-package.R 3 | \docType{package} 4 | \name{httpgd-package} 5 | \alias{httpgd} 6 | \alias{httpgd-package} 7 | \title{httpgd: HTTP server graphics device} 8 | \description{ 9 | Asynchronous HTTP server graphics device. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://github.com/nx10/httpgd} 15 | \item \url{https://nx10.github.io/httpgd/} 16 | \item Report bugs at \url{https://github.com/nx10/httpgd/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Florian Rupprecht \email{floruppr@gmail.com} (\href{https://orcid.org/0000-0002-1795-8624}{ORCID}) 22 | 23 | Other contributors: 24 | \itemize{ 25 | \item Kun Ren \email{mail@renkun.me} [contributor] 26 | \item Jeroen Ooms \email{jeroen@berkeley.edu} (\href{https://orcid.org/0000-0002-4035-0289}{ORCID}) [contributor] 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | - GitHub rlib/actions 3 | - R-hub builder 4 | 5 | ## CRAN Check Results 6 | 7 | > Version: 2.0.3 8 | > Check: whether package can be installed 9 | > Result: WARN 10 | > Found the following significant warnings: 11 | > lib/crow/common.h:351:39: warning: identifier '_method' preceded by whitespace in a literal operator declaration is deprecated [-Wdeprecated-literal-operator] 12 | > See ‘/home/hornik/tmp/R.check/r-devel-clang/Work/PKGS/httpgd.Rcheck/00install.out’ for details. 13 | > * used C++ compiler: ‘Debian clang version 19.1.7 (1+b1)’ 14 | > Flavor: r-devel-linux-x86_64-debian-clang 15 | 16 | The deprecated syntax has been updated. 17 | 18 | > rchk Warning 19 | 20 | This is caused by upstream package `cpp11`. 21 | 22 | > R CMD CHECK allocator deprecation warning 23 | 24 | This is caused by upstream package `AsioHeaders`. 25 | 26 | ## Downstream dependencies 27 | There are no downstream dependencies. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | 14 | 15 | (Write your answer here.) 16 | 17 | ### To Reproduce 18 | 21 | 22 | (Write your answer here.) 23 | 24 | ### Expected behavior 25 | 28 | 29 | (Write your answer here.) 30 | 31 | ### Screenshots 32 | 35 | 36 | (Write your answer here.) 37 | 38 | ### Environment 39 | - OS: 40 | - Browser: 41 | - R version: 42 | - httpgd version: 43 | 44 | ### Additional context 45 | 48 | 49 | (Write your answer here.) 50 | -------------------------------------------------------------------------------- /man/hgd_details.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd_details} 4 | \alias{hgd_details} 5 | \title{httpgd device status.} 6 | \usage{ 7 | hgd_details(which = dev.cur()) 8 | } 9 | \arguments{ 10 | \item{which}{Which device (ID).} 11 | } 12 | \value{ 13 | List of status variables with the following named items: 14 | \verb{$host}: Server hostname, 15 | \verb{$port}: Server port, 16 | \verb{$token}: Security token, 17 | \verb{$hsize}: Plot history size (how many plots are accessible), 18 | \verb{$upid}: Update ID (changes when the device has received new information), 19 | \verb{$active}: Is the device the currently activated device. 20 | } 21 | \description{ 22 | Access status information of a httpgd graphics device. 23 | This function will only work after starting a device with \code{\link[=hgd]{hgd()}}. 24 | } 25 | \examples{ 26 | \dontrun{ 27 | 28 | hgd() 29 | hgd_details() 30 | plot(1, 1) 31 | hgd_details() 32 | 33 | dev.off() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your proposal related to a problem? 11 | 12 | 16 | 17 | (Write your answer here.) 18 | 19 | ### Describe the solution you'd like 20 | 21 | 24 | 25 | (Describe your proposed solution here.) 26 | 27 | ### Describe alternatives you've considered 28 | 29 | 32 | 33 | (Write your answer here.) 34 | 35 | ### Additional context 36 | 37 | 41 | 42 | (Write your answer here.) 43 | -------------------------------------------------------------------------------- /vignettes/a00_installation.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Installation} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Install `httpgd` from CRAN: 11 | 12 | ```R 13 | install.packages("httpgd") 14 | ``` 15 | 16 | Or get the latest development version from GitHub: 17 | 18 | ```R 19 | remotes::install_github("nx10/httpgd") 20 | ``` 21 | 22 | See [system requirements](#System-requirements) for troubleshooting. 23 | 24 | ## System requirements 25 | 26 | Depends on `R` version ≥ 4.0 on windows, and R ≥ 3.2 on linux and macOS (a C++ compiler with basic C++17 support [is required](https://github.com/nx10/httpgd/issues/56)). 27 | 28 | Note that there is a rare bug in R versions < 4.1, that leads to [some plots disappearing when ggplot2 plots are resized and deleted in a specific way](https://github.com/nx10/httpgd/issues/50). 29 | 30 | Also see the [`unigd` system requirements](https://nx10.github.io/unigd/articles/a00_installation.html#system-requirements). -------------------------------------------------------------------------------- /src/unigd_impl.cpp: -------------------------------------------------------------------------------- 1 | #include "unigd_impl.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "unigd_api.h" 7 | 8 | namespace httpgd 9 | { 10 | namespace ugd 11 | { 12 | unigd_api_v1* api = nullptr; 13 | UNIGD_CLIENT_ID httpgd_client_id = 0; 14 | 15 | namespace 16 | { 17 | class unigd_api_guard 18 | { 19 | public: 20 | ~unigd_api_guard() { destroy(); } 21 | 22 | void create() 23 | { 24 | if (api == nullptr) unigd_api_v1_create(&api); 25 | } 26 | void destroy() 27 | { 28 | if (api != nullptr) 29 | { 30 | unigd_api_v1_destroy(api); 31 | api = nullptr; 32 | } 33 | } 34 | }; 35 | 36 | unigd_api_guard api_guard; 37 | } // namespace 38 | } // namespace ugd 39 | } // namespace httpgd 40 | 41 | // There is a bug in cpp11 / decor where pointer types are not detected when the asterisk 42 | // is right-aligned see: https://github.com/r-lib/decor/pull/11 43 | // clang-format off 44 | [[cpp11::init]] void import_unigd_api(DllInfo* dll) 45 | // clang-format on 46 | { 47 | httpgd::ugd::api_guard.create(); 48 | httpgd::ugd::httpgd_client_id = httpgd::ugd::api->register_client_id(); 49 | } 50 | -------------------------------------------------------------------------------- /inst/licenses/belle-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Brett Robinson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/crow/ci_map.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "crow/utility.h" 6 | 7 | namespace crow 8 | { 9 | /// Hashing function for ci_map (unordered_multimap). 10 | struct ci_hash 11 | { 12 | size_t operator()(const std::string& key) const 13 | { 14 | std::size_t seed = 0; 15 | std::locale locale; 16 | 17 | for (auto c : key) 18 | hash_combine(seed, std::toupper(c, locale)); 19 | 20 | return seed; 21 | } 22 | 23 | private: 24 | static inline void hash_combine(std::size_t& seed, char v) 25 | { 26 | std::hash hasher; 27 | seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); 28 | } 29 | }; 30 | 31 | /// Equals function for ci_map (unordered_multimap). 32 | struct ci_key_eq 33 | { 34 | bool operator()(const std::string& l, const std::string& r) const 35 | { 36 | return utility::string_equals(l, r); 37 | } 38 | }; 39 | 40 | using ci_map = std::unordered_multimap; 41 | } // namespace crow 42 | -------------------------------------------------------------------------------- /client/src/views/exportDialog.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "httpgd-client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "browser": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build:dev": "webpack --mode development --devtool inline-source-map", 9 | "build:prod": "webpack --mode production", 10 | "watch:dev": "webpack --watch --mode development", 11 | "watch:prod": "webpack --watch --mode production", 12 | "serve": "webpack serve --mode development --devtool inline-source-map" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@typescript-eslint/eslint-plugin": "^6.20.0", 18 | "@typescript-eslint/parser": "^6.20.0", 19 | "css-loader": "^6.10.0", 20 | "ejs-compiled-loader": "^3.1.0", 21 | "ejs-loader": "^0.5.0", 22 | "eslint": "^8.56.0", 23 | "html-loader": "^5.0.0", 24 | "html-webpack-plugin": "^5.6.0", 25 | "mini-css-extract-plugin": "^2.8.0", 26 | "sass": "^1.70.0", 27 | "sass-loader": "^14.1.0", 28 | "style-loader": "^3.3.4", 29 | "svg-inline-loader": "^0.8.2", 30 | "ts-loader": "^9.5.1", 31 | "typescript": "^5.3.3", 32 | "webpack": "^5.90.1", 33 | "webpack-cli": "^5.1.4", 34 | "webpack-dev-server": "^4.15.1" 35 | }, 36 | "dependencies": { 37 | "httpgd": "^0.1.7" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | tags: ['*'] 7 | workflow_dispatch: 8 | 9 | name: pkgdown 10 | 11 | jobs: 12 | pkgdown: 13 | runs-on: ubuntu-latest 14 | # Only restrict concurrency for non-PR jobs 15 | concurrency: 16 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 17 | env: 18 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 19 | permissions: 20 | contents: write 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: r-lib/actions/setup-pandoc@v2 25 | 26 | - uses: r-lib/actions/setup-r@v2 27 | with: 28 | use-public-rspm: true 29 | 30 | - uses: r-lib/actions/setup-r-dependencies@v2 31 | with: 32 | extra-packages: any::pkgdown, local::. 33 | needs: website 34 | 35 | - name: Build site 36 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 37 | shell: Rscript {0} 38 | 39 | - name: Deploy to GitHub pages 🚀 40 | if: github.event_name != 'pull_request' 41 | uses: JamesIves/github-pages-deploy-action@v4.6.1 42 | with: 43 | clean: false 44 | branch: gh-pages 45 | folder: docs -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: [push, pull_request] 4 | 5 | name: R-CMD-check 6 | 7 | jobs: 8 | R-CMD-check: 9 | runs-on: ${{ matrix.config.os }} 10 | 11 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | config: 17 | - {os: macos-latest, r: 'release'} 18 | - {os: windows-latest, r: 'release'} 19 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 20 | - {os: ubuntu-latest, r: 'release'} 21 | - {os: ubuntu-latest, r: 'oldrel-1'} 22 | 23 | env: 24 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 25 | R_KEEP_PKG_SOURCE: yes 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: r-lib/actions/setup-pandoc@v2 31 | 32 | - uses: r-lib/actions/setup-r@v2 33 | with: 34 | r-version: ${{ matrix.config.r }} 35 | http-user-agent: ${{ matrix.config.http-user-agent }} 36 | use-public-rspm: true 37 | 38 | - uses: r-lib/actions/setup-r-dependencies@v2 39 | with: 40 | extra-packages: any::rcmdcheck 41 | needs: check 42 | 43 | - uses: r-lib/actions/check-r-package@v2 44 | with: 45 | upload-snapshots: true -------------------------------------------------------------------------------- /src/httpgd_rng.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "httpgd_rng.h" 3 | 4 | #include 5 | #include 6 | 7 | namespace httpgd 8 | { 9 | namespace rng 10 | { 11 | std::string uuid() 12 | { 13 | const int uuidLength = 36; // Including hyphens 14 | std::string uuid; 15 | uuid.reserve(uuidLength); 16 | 17 | std::random_device rd; 18 | std::mt19937 gen(rd()); 19 | std::uniform_int_distribution<> dis(0, 15); 20 | 21 | auto randomByte = [&]() { 22 | const char hexChars[] = "0123456789abcdef"; 23 | return hexChars[dis(gen)]; 24 | }; 25 | 26 | for (int i = 0; i < 32; ++i) { 27 | if (i == 8 || i == 12 || i == 16 || i == 20) { 28 | uuid += '-'; 29 | } 30 | uuid += randomByte(); 31 | } 32 | 33 | return uuid; 34 | } 35 | 36 | std::string token(int length) 37 | { 38 | static const char alphanum[] = 39 | "0123456789" 40 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 41 | "abcdefghijklmnopqrstuvwxyz"; 42 | static auto rseed = static_cast( 43 | std::chrono::high_resolution_clock::now().time_since_epoch().count()); 44 | static std::mt19937 generator(rseed); 45 | static std::uniform_int_distribution distribution{ 46 | 0, static_cast((sizeof(alphanum) / sizeof(alphanum[0])) - 2)}; 47 | 48 | std::string rand_str(length, '\0'); 49 | for (int i = 0; i < length; ++i) 50 | { 51 | rand_str[i] = alphanum[distribution(generator)]; 52 | } 53 | 54 | return rand_str; 55 | } 56 | } // namespace rng 57 | } // namespace httpgd -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: httpgd 2 | Type: Package 3 | Title: A 'HTTP' Server Graphics Device 4 | Version: 2.0.4 5 | Authors@R: 6 | c( 7 | person(given = "Florian", family = "Rupprecht", email = "floruppr@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-1795-8624")), 8 | person(given = "Kun", family = "Ren", role = "ctb", email = "mail@renkun.me"), 9 | person("Jeroen", "Ooms", role = c("ctb"), email = "jeroen@berkeley.edu", comment = c(ORCID = "0000-0002-4035-0289")) 10 | ) 11 | Description: A graphics device for R that is accessible via network protocols. 12 | This package was created to make it easier to embed live R graphics in 13 | integrated development environments and other applications. 14 | The included 'HTML/JavaScript' client (plot viewer) aims to provide a better overall user experience when dealing with R graphics. 15 | The device asynchronously serves graphics via 'HTTP' and 'WebSockets'. 16 | License: GPL (>= 2) 17 | Depends: 18 | R (>= 3.2.0) 19 | Imports: 20 | unigd 21 | LinkingTo: 22 | unigd, 23 | cpp11 (>= 0.2.4), 24 | AsioHeaders (>= 1.22.1) 25 | Suggests: 26 | testthat, 27 | xml2 (>= 1.0.0), 28 | knitr, 29 | rmarkdown, 30 | covr, 31 | future, 32 | httr, 33 | jsonlite 34 | Roxygen: list(markdown = TRUE) 35 | RoxygenNote: 7.3.2 36 | Encoding: UTF-8 37 | URL: https://github.com/nx10/httpgd, 38 | https://nx10.github.io/httpgd/ 39 | BugReports: https://github.com/nx10/httpgd/issues 40 | VignetteBuilder: knitr 41 | -------------------------------------------------------------------------------- /man/hgd_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd_url} 4 | \alias{hgd_url} 5 | \title{httpgd URL.} 6 | \usage{ 7 | hgd_url( 8 | endpoint = "live", 9 | which = dev.cur(), 10 | host = NA, 11 | port = NA, 12 | explicit = FALSE, 13 | omit_token = FALSE, 14 | ... 15 | ) 16 | } 17 | \arguments{ 18 | \item{endpoint}{API endpoint. The default, \code{"live"} is the HTML/JS 19 | plot viewer. Can be set to a numeric plot index or plot ID 20 | (see \code{\link[unigd:ugd_id]{unigd::ugd_id()}}) to obtain the direct URL to the SVG.} 21 | 22 | \item{which}{Which device (ID).} 23 | 24 | \item{host}{Replaces hostname.} 25 | 26 | \item{port}{Replaces port.} 27 | 28 | \item{explicit}{Ads \code{hgd={host}:{port}} query parameter. Needed for host 29 | resolution in some editors.} 30 | 31 | \item{omit_token}{Should the security token be omitted from the URL.} 32 | 33 | \item{\\dots}{Other query parameters that will be appended to the URL.} 34 | } 35 | \value{ 36 | URL. 37 | } 38 | \description{ 39 | Generate URLs to the plot viewer or to plot SVGs. 40 | This function will only work after starting a device with \code{\link[=hgd]{hgd()}}. 41 | } 42 | \details{ 43 | Note: If the included client is used set \code{websockets=0} or 44 | \code{sidebar=0} to turn off WebSocket or plot history sidebar. 45 | } 46 | \examples{ 47 | \dontrun{ 48 | 49 | hgd() 50 | my_url <- hgd_url() 51 | hgd_url(0) 52 | hgd_url(plot_id(), width = 800, height = 600) 53 | 54 | dev.off() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /vignettes/b02_rstudio.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "RStudio" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{RStudio} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | `httpgd` fully supports RStudio: 11 | 12 | ![](https://user-images.githubusercontent.com/33600480/147690023-0544d9b8-a3a8-4c34-8201-60d1c193b0d8.png) 13 | 14 | ## Usage 15 | 16 | To open a client in the embedded RStudio `Viewer`-tab call `hgd_view()` after starting the server: 17 | 18 | ```R 19 | library(httpgd) 20 | hgd() 21 | hgd_view() 22 | ``` 23 | 24 | To view a client in an external browser window call: 25 | 26 | ```R 27 | # hgd() 28 | hgd_browse() 29 | ``` 30 | 31 | ## Troubleshooting 32 | 33 | Sometimes (if the `Viewer`-tab is resized with any plot in the `Plots`-tab) RStudio will activate its own graphics device, the `Plots`-tab will obtain focus, and `httpgd` will show the message _"Device inactive."_ in the `Viewer`-tab. 34 | 35 | The list of graphics devices (`dev.list()`) will look something like this: 36 | 37 | ```R 38 | > dev.list() 39 | RStudioGD agg_png httpgd 40 | 2 3 4 41 | ``` 42 | 43 | This can be easily solved by closing the current (RStudioGD) graphics device: 44 | 45 | ```R 46 | > dev.off() 47 | httpgd 48 | 4 49 | ``` 50 | 51 | After that `dev.list()` should only show a `httpgd` device: 52 | 53 | ```R 54 | > dev.list() 55 | httpgd 56 | 4 57 | ``` 58 | 59 | The problem should not reappear afterwards, even after resizing the pane. 60 | -------------------------------------------------------------------------------- /vignettes/a01_how-to-get-started.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting started" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Getting started} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```R 11 | hgd() 12 | ``` 13 | 14 | Copy the displayed link in the browser or call 15 | 16 | ```R 17 | hgd_browse() 18 | ``` 19 | 20 | to open a browser window automatically. 21 | 22 | Plot anything. 23 | 24 | ```R 25 | x = seq(0, 3 * pi, by = 0.1) 26 | plot(x, sin(x), type = "l") 27 | ``` 28 | 29 | Every plotting library will work. 30 | 31 | ```R 32 | library(ggplot2) 33 | ggplot(mpg, aes(displ, hwy, colour = class)) + 34 | geom_point() 35 | ``` 36 | 37 | Stop the server with: 38 | 39 | ```R 40 | dev.off() 41 | ``` 42 | 43 | ## Keyboard shortcuts 44 | 45 | | Keys | Result | 46 | |:----:|--------| 47 | | | Navigate plot history. | 48 | | + / - | Zoom in and out. | 49 | | 0 | Reset zoom level. | 50 | | N | Jump to the newest plot. | 51 | | del / D | Delete plot. | 52 | | alt+D | Clear all plots. | 53 | | S | Download plot as SVG. | 54 | | P | Download plot as PNG. | 55 | | C | Copy plot to clipboard (as PNG). | 56 | | H | Toggle plot history (sidebar). | 57 | 58 | ## See also 59 | 60 | See also the [plotting in `unigd`](https://nx10.github.io/unigd/articles/b00_guide.html) guide. -------------------------------------------------------------------------------- /client/src/views/toolbar.ejs: -------------------------------------------------------------------------------- 1 | <% const toolbar = require("./views/toolbarData.ts") %> 2 | 3 |
4 |
5 | <% for( let i = 0; i < toolbar.data.main.length; i++ ) { %> 6 | 7 | <% for( let j = 0; j < toolbar.data.main[i].length; j++ ) { 8 | const item = toolbar.data.main[i][j]; 9 | const title = item.title + " (" + item.shortcut + ")"; 10 | %> 11 | 12 | <%- item.content %> 13 | <%- title %> 14 | 15 | <% } %> 16 | 17 | <% } %> 18 | 19 | 20 | <%- toolbar.data.more.content %> 21 | 22 | 34 | 35 |
36 |
-------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::covr 27 | needs: coverage 28 | 29 | - name: Test coverage 30 | run: | 31 | covr::codecov( 32 | quiet = FALSE, 33 | clean = FALSE, 34 | install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") 35 | ) 36 | shell: Rscript {0} 37 | 38 | - name: Show testthat output 39 | if: always() 40 | run: | 41 | ## -------------------------------------------------------------------- 42 | find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true 43 | shell: bash 44 | 45 | - name: Upload test results 46 | if: failure() 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: coverage-test-failures 50 | path: ${{ runner.temp }}/package -------------------------------------------------------------------------------- /src/lib/crow/settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | // settings for crow 3 | // TODO(ipkn) replace with runtime config. libucl? 4 | 5 | /* #ifdef - enables debug mode */ 6 | //#define CROW_ENABLE_DEBUG 7 | 8 | /* #ifdef - enables logging */ 9 | #define CROW_ENABLE_LOGGING 10 | 11 | /* #ifdef - enforces section 5.2 and 6.1 of RFC6455 (only accepting masked messages from clients) */ 12 | //#define CROW_ENFORCE_WS_SPEC 13 | 14 | /* #define - specifies log level */ 15 | /* 16 | Debug = 0 17 | Info = 1 18 | Warning = 2 19 | Error = 3 20 | Critical = 4 21 | 22 | default to INFO 23 | */ 24 | #ifndef CROW_LOG_LEVEL 25 | #define CROW_LOG_LEVEL 1 26 | #endif 27 | 28 | #ifndef CROW_STATIC_DIRECTORY 29 | #define CROW_STATIC_DIRECTORY "static/" 30 | #endif 31 | #ifndef CROW_STATIC_ENDPOINT 32 | #define CROW_STATIC_ENDPOINT "/static/" 33 | #endif 34 | 35 | // compiler flags 36 | #if defined(_MSVC_LANG) && _MSVC_LANG >= 201402L 37 | #define CROW_CAN_USE_CPP14 38 | #endif 39 | #if __cplusplus >= 201402L 40 | #define CROW_CAN_USE_CPP14 41 | #endif 42 | 43 | #if defined(_MSVC_LANG) && _MSVC_LANG >= 201703L 44 | #define CROW_CAN_USE_CPP17 45 | #endif 46 | #if __cplusplus >= 201703L 47 | #define CROW_CAN_USE_CPP17 48 | #if defined(__GNUC__) && __GNUC__ < 8 49 | #define CROW_FILESYSTEM_IS_EXPERIMENTAL 50 | #endif 51 | #endif 52 | 53 | #if defined(_MSC_VER) 54 | #if _MSC_VER < 1900 55 | #define CROW_MSVC_WORKAROUND 56 | #define constexpr const 57 | #define noexcept throw() 58 | #endif 59 | #endif 60 | 61 | #if defined(__GNUC__) && __GNUC__ == 8 && __GNUC_MINOR__ < 4 62 | #if __cplusplus > 201103L 63 | #define CROW_GCC83_WORKAROUND 64 | #else 65 | #error "GCC 8.1 - 8.3 has a bug that prevents Crow from compiling with C++11. Please update GCC to > 8.3 or use C++ > 11." 66 | #endif 67 | #endif 68 | -------------------------------------------------------------------------------- /inst/licenses/CrowCpp-BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2014-2017, ipkn 4 | 2020-2022, CrowCpp 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the author nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | The Crow logo and other graphic material (excluding third party logos) used are under exclusive Copyright (c) 2021-2022, Farook Al-Sammarraie (The-EDev), All rights reserved. 33 | -------------------------------------------------------------------------------- /man/hgd_watch.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd_watch} 4 | \alias{hgd_watch} 5 | \title{Watch for changes in code files and refresh plots automatically.} 6 | \usage{ 7 | hgd_watch( 8 | watch = list.files(pattern = "\\\\.R$", ignore.case = T), 9 | on_change = function(changed_files) { 10 | print(changed_files) 11 | }, 12 | interval = 1, 13 | on_start = hgd_browse, 14 | on_exit = NULL, 15 | on_error = print, 16 | reset_par = TRUE, 17 | ... 18 | ) 19 | } 20 | \arguments{ 21 | \item{watch}{Paths that are watched for changes (see \code{\link[utils:changedFiles]{utils::fileSnapshot()}})} 22 | 23 | \item{on_change}{Will be called when a file changes.} 24 | 25 | \item{interval}{Time interval in which changes are detected (in seconds).} 26 | 27 | \item{on_start}{Will be called after the httpgd server is started 28 | (may be set to \code{NULL}).} 29 | 30 | \item{on_exit}{Will be called before the server is closed 31 | (may be set to \code{NULL}).} 32 | 33 | \item{on_error}{Will be called when on_change throws an error 34 | (may be set to \code{NULL}).} 35 | 36 | \item{reset_par}{If set to \code{TRUE}, global graphics parameters will be saved 37 | on device start and reset every time \code{\link[unigd:ugd_clear]{unigd::ugd_clear()}} is called (see 38 | \code{\link[graphics:par]{graphics::par()}}).} 39 | 40 | \item{...}{Additional parameters passed to \code{hgd(webserver=FALSE, ...)}} 41 | } 42 | \description{ 43 | This function may be used to rapidly develop visualizations 44 | by replacing a workflow of reloading and executing code manually. 45 | } 46 | \examples{ 47 | \dontrun{ 48 | 49 | # --- my_code.R --- 50 | 51 | plot(rnorm(100), col = "red") 52 | 53 | # --- Other file / interactive --- 54 | 55 | hgd_watch( 56 | watch = "my_code.R", # When my_code.R changes... 57 | on_change = function(...) { 58 | source("my_code.R") # ...call updated plot function. 59 | } 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cpp11.cpp: -------------------------------------------------------------------------------- 1 | // Generated by cpp11: do not edit by hand 2 | // clang-format off 3 | 4 | 5 | #include "cpp11/declarations.hpp" 6 | #include 7 | 8 | // httpgd.cpp 9 | bool httpgd_(int devnum, std::string host, int port, bool cors, std::string token, bool silent, std::string wwwpath); 10 | extern "C" SEXP _httpgd_httpgd_(SEXP devnum, SEXP host, SEXP port, SEXP cors, SEXP token, SEXP silent, SEXP wwwpath) { 11 | BEGIN_CPP11 12 | return cpp11::as_sexp(httpgd_(cpp11::as_cpp>(devnum), cpp11::as_cpp>(host), cpp11::as_cpp>(port), cpp11::as_cpp>(cors), cpp11::as_cpp>(token), cpp11::as_cpp>(silent), cpp11::as_cpp>(wwwpath))); 13 | END_CPP11 14 | } 15 | // httpgd.cpp 16 | cpp11::list httpgd_details_(int devnum); 17 | extern "C" SEXP _httpgd_httpgd_details_(SEXP devnum) { 18 | BEGIN_CPP11 19 | return cpp11::as_sexp(httpgd_details_(cpp11::as_cpp>(devnum))); 20 | END_CPP11 21 | } 22 | // httpgd.cpp 23 | std::string httpgd_random_token_(int len); 24 | extern "C" SEXP _httpgd_httpgd_random_token_(SEXP len) { 25 | BEGIN_CPP11 26 | return cpp11::as_sexp(httpgd_random_token_(cpp11::as_cpp>(len))); 27 | END_CPP11 28 | } 29 | 30 | extern "C" { 31 | static const R_CallMethodDef CallEntries[] = { 32 | {"_httpgd_httpgd_", (DL_FUNC) &_httpgd_httpgd_, 7}, 33 | {"_httpgd_httpgd_details_", (DL_FUNC) &_httpgd_httpgd_details_, 1}, 34 | {"_httpgd_httpgd_random_token_", (DL_FUNC) &_httpgd_httpgd_random_token_, 1}, 35 | {NULL, NULL, 0} 36 | }; 37 | } 38 | 39 | void import_unigd_api(DllInfo* dll); 40 | 41 | extern "C" attribute_visible void R_init_httpgd(DllInfo* dll){ 42 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 43 | R_useDynamicSymbols(dll, FALSE); 44 | import_unigd_api(dll); 45 | R_forceSymbols(dll, TRUE); 46 | } 47 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/index.ts', 7 | plugins: [ 8 | new MiniCssExtractPlugin({ 9 | filename: "style.css", 10 | chunkFilename: "[name].css" 11 | }), 12 | new HtmlWebpackPlugin({ 13 | template: './src/index.ejs', 14 | favicon: './src/assets/favicon.ico', 15 | minify: 'auto', 16 | }), 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.tsx?$/, 22 | use: 'ts-loader', 23 | exclude: /node_modules/, 24 | }, 25 | { 26 | test: /\.(scss|css)$/, 27 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 28 | }, 29 | { 30 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 31 | type: 'asset/resource', 32 | exclude: /icons/, 33 | }, 34 | { 35 | test: /icons.*\.svg$/i, 36 | loader: 'svg-inline-loader' 37 | }, 38 | { 39 | test: /\.ejs$/, 40 | use: { 41 | loader: 'ejs-compiled-loader', 42 | options: { 43 | htmlmin: true, 44 | htmlminOptions: { 45 | removeComments: true 46 | } 47 | } 48 | } 49 | }, 50 | ], 51 | }, 52 | resolve: { 53 | extensions: ['.tsx', '.ts', '.js'], 54 | }, 55 | output: { 56 | hashFunction: "xxhash64", 57 | filename: 'bundle.js', 58 | path: path.resolve(__dirname, '../inst/www'), 59 | clean: true, 60 | }, 61 | devServer: { 62 | static: path.join(__dirname, '../inst/www'), 63 | compress: true, 64 | port: 9000, 65 | }, 66 | }; -------------------------------------------------------------------------------- /src/httpgd_webserver.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_HTTPGD_WEBSERVER_H__ 2 | #define __UNIGD_HTTPGD_WEBSERVER_H__ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "unigd_impl.h" 13 | 14 | namespace httpgd 15 | { 16 | namespace web 17 | { 18 | struct HttpgdServerConfig 19 | { 20 | std::string host; 21 | int port; 22 | std::string wwwpath; 23 | bool cors; 24 | bool use_token; 25 | std::string token; 26 | bool record_history; 27 | bool silent; 28 | std::string id; 29 | }; 30 | 31 | class HttpgdLogHandler : public crow::ILogHandler 32 | { 33 | public: 34 | void log(std::string message, crow::LogLevel level) override; 35 | 36 | private: 37 | static std::string timestamp(); 38 | }; 39 | 40 | class WebServer 41 | { 42 | struct TokenGuard : crow::ILocalMiddleware 43 | { 44 | struct context 45 | { 46 | }; 47 | 48 | void before_handle(crow::request& req, crow::response& res, context& ctx); 49 | 50 | void after_handle(crow::request& req, crow::response& res, context& ctx); 51 | 52 | bool m_use_token = false; 53 | std::string m_token; 54 | }; 55 | 56 | public: 57 | WebServer(const HttpgdServerConfig& t_config); 58 | 59 | bool attach(int devnum); 60 | 61 | void device_start(); 62 | void device_close(); 63 | void device_state_change(); 64 | 65 | std::string status_info(); 66 | const HttpgdServerConfig& get_config(); 67 | 68 | unsigned short port(); 69 | void broadcast_state(const unigd_device_state& state); 70 | 71 | private: 72 | unigd_api_v1* m_api = nullptr; 73 | UNIGD_HANDLE m_ugd_handle = nullptr; 74 | unigd_graphics_client m_client; 75 | 76 | HttpgdServerConfig m_conf; 77 | crow::App m_app; 78 | HttpgdLogHandler m_log_handler; 79 | std::mutex m_mtx_update_subs; 80 | std::unordered_set m_update_subs; 81 | int m_last_upid = -1; 82 | bool m_last_active = true; 83 | std::thread m_server_thread; 84 | 85 | void run(); 86 | }; 87 | } // namespace web 88 | } // namespace httpgd 89 | 90 | #endif /* __UNIGD_HTTPGD_WEBSERVER_H__ */ 91 | -------------------------------------------------------------------------------- /src/httpgd.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | //#include 12 | 13 | #include 14 | #include 15 | 16 | #include "httpgd_rng.h" 17 | #include "httpgd_version.h" 18 | #include "httpgd_webserver.h" 19 | #include "unigd_impl.h" 20 | 21 | [[cpp11::register]] bool httpgd_(int devnum, std::string host, int port, bool cors, 22 | std::string token, bool silent, std::string wwwpath) 23 | { 24 | // wwwpath must be determined in R, because devtools overrides system.path 25 | // with a shim which results in an empty string *sometimes*. 26 | 27 | bool recording = true; 28 | bool use_token = token.length(); 29 | 30 | const httpgd::web::HttpgdServerConfig conf{host, port, wwwpath, 31 | cors, use_token, token, 32 | recording, silent, httpgd::rng::uuid()}; 33 | 34 | return (new httpgd::web::WebServer(conf))->attach(devnum); 35 | } 36 | 37 | [[cpp11::register]] cpp11::list httpgd_details_(int devnum) 38 | { 39 | if (httpgd::ugd::api == nullptr) 40 | { 41 | cpp11::stop("unigd not initialized."); 42 | } 43 | auto *client = httpgd::ugd::api->device_get(devnum, httpgd::ugd::httpgd_client_id); 44 | if (!client) 45 | { 46 | cpp11::stop("Device is not a unigd device with attached httpgd client."); 47 | } 48 | 49 | auto *server = static_cast(client); 50 | const auto svr_config = server->get_config(); 51 | 52 | using namespace cpp11::literals; 53 | return cpp11::writable::list{ 54 | "host"_nm = svr_config.host.c_str(), "port"_nm = server->port(), 55 | "token"_nm = svr_config.token.c_str(), "status"_nm = server->status_info()}; 56 | return cpp11::writable::list{}; 57 | } 58 | 59 | [[cpp11::register]] std::string httpgd_random_token_(int len) 60 | { 61 | if (len < 0) 62 | { 63 | cpp11::stop("Length needs to be 0 or higher."); 64 | } 65 | return httpgd::rng::token(len); 66 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # httpgd 2.0.4 2 | 3 | - Fix 'Clang 19' deprecation warning. 4 | 5 | # httpgd 2.0.3 6 | 7 | - Drop 'C++14' requirement. 8 | 9 | # httpgd 2.0.2 10 | 11 | - Fix 'gcc 14' compile warning. 12 | 13 | # httpgd 2.0.1 14 | 15 | - Fix compilation issues with 'clang 18' and 'libc++'. 16 | 17 | # httpgd 2.0.0 18 | 19 | - Split graphics rendering and R interface to seperate package 'unigd'. 20 | - Large refactoring and rewrite. 21 | - Numerous fixes and improvements in 'unigd' (see 'NEWS.md' there). 22 | - Migrate to crow web server library (due to belle being deprecated). 23 | - Add live reload feature. 24 | - Switched from 'Boost' to 'Asio' 25 | - Various library updates. 26 | - Various minor fixes and improvements. 27 | 28 | # httpgd 1.3.0 29 | 30 | - Fixes for R 4.2 UCRT support (thanks Tomas Kalibera and Uwe Ligges). 31 | - Added 'pkgdown' documentation and various vignettes (thanks eitsupi for the article on Docker). 32 | - Added RStudio viewer support. 33 | - Added version info API. 34 | - Added SVGZ support. 35 | - Improved client export dialog. 36 | - Minor fixes and internal changes. 37 | 38 | # httpgd 1.2.1 39 | 40 | - Fix macOS builds. 41 | - Minor documentation changes. 42 | 43 | # httpgd 1.2.0 44 | 45 | - Client rewrite and static build pipeline. 46 | - Added client export dialog. 47 | - Various client UI improvements. 48 | - Implemented modular rendering. 49 | - Cairo based renderers for PNG, PDF, EPS and PS. 50 | - Portable SVG renderer (for easier embedding and styling). 51 | - Special renderers: Serialized JSON, meta information and strings. 52 | - Additions to R and HTTP APIs for selecting and listing available renderers. 53 | - All startup default parameters can now be set as options. 54 | - Zoom level is now handled server side. 55 | - Fix some graphical errors of the SVG renderer. 56 | - Improved server URL generation. 57 | - Browser can be specified when the server URL is opened from R. 58 | - Dependency updates and UCRT support. 59 | 60 | # httpgd 1.1.1 61 | 62 | - Fixed font weight related rendering crash. 63 | - Small client adjustments. 64 | 65 | # httpgd 1.1.0 66 | 67 | - Added plot history sidebar. 68 | - Improved SVG rendering performance. 69 | - Added static plot ID API. 70 | - Font handling improvements. 71 | - Various client improvements. 72 | - Added API documentation vignette. 73 | - Fixed client device inactive desynchronisation. 74 | - Fixes for R devel. 75 | - Library updates. 76 | 77 | # httpgd 1.0.1 78 | 79 | - Fix memory allocation issues with graphics device creation and libpng. 80 | - Set `cpp11` as a compile time only dependency. 81 | 82 | # httpgd 1.0.0 83 | 84 | - First official version intended for release on CRAN. 85 | -------------------------------------------------------------------------------- /src/lib/crow/middleware_context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "crow/utility.h" 4 | #include "crow/http_request.h" 5 | #include "crow/http_response.h" 6 | 7 | namespace crow 8 | { 9 | namespace detail 10 | { 11 | 12 | 13 | template 14 | struct partial_context : public black_magic::pop_back::template rebind, public black_magic::last_element_type::type::context 15 | { 16 | using parent_context = typename black_magic::pop_back::template rebind<::crow::detail::partial_context>; 17 | template 18 | using partial = typename std::conditional>::type; 19 | 20 | template 21 | typename T::context& get() 22 | { 23 | return static_cast(*this); 24 | } 25 | }; 26 | 27 | 28 | 29 | template<> 30 | struct partial_context<> 31 | { 32 | template 33 | using partial = partial_context; 34 | }; 35 | 36 | 37 | template 38 | struct context : private partial_context 39 | //struct context : private Middlewares::context... // simple but less type-safe 40 | { 41 | template 42 | friend typename std::enable_if<(N == 0)>::type after_handlers_call_helper(const CallCriteria& cc, Container& middlewares, Context& ctx, request& req, response& res); 43 | template 44 | friend typename std::enable_if<(N > 0)>::type after_handlers_call_helper(const CallCriteria& cc, Container& middlewares, Context& ctx, request& req, response& res); 45 | 46 | template 47 | friend typename std::enable_if<(N < std::tuple_size::type>::value), bool>::type 48 | middleware_call_helper(const CallCriteria& cc, Container& middlewares, request& req, response& res, Context& ctx); 49 | 50 | template 51 | typename T::context& get() 52 | { 53 | return static_cast(*this); 54 | } 55 | 56 | template 57 | using partial = typename partial_context::template partial; 58 | }; 59 | } // namespace detail 60 | } // namespace crow 61 | -------------------------------------------------------------------------------- /vignettes/b03_docker.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Docker" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Docker} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Using `httpgd` to display R plots in a Docker container (Linux container) may be easier than the traditional and common method; linking the X11 window system. 11 | 12 | ## Basic usage 13 | 14 | ### Build a docker image 15 | 16 | See the `vignette("a00_installation")` for details on how to install `httpgd` on Linux. 17 | 18 | You can create a Docker image with `httpgd` installed by create a Dockerfile like below. 19 | 20 | ```Dockerfile 21 | FROM r-base:latest 22 | 23 | # Install httpgd and dependent packages. 24 | RUN apt-get update \ 25 | && apt-get install -y --no-install-recommends \ 26 | libfontconfig1-dev \ 27 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* \ 28 | && install2.r --error --skipinstalled --ncpu -1 \ 29 | httpgd \ 30 | && rm -rf /tmp/downloaded_packages 31 | ``` 32 | 33 | Run the `docker build` command from your shell to build a Docker image. 34 | 35 | ```sh 36 | docker build . -f Dockerfile -t httpgd:test 37 | ``` 38 | 39 | ### Create a container 40 | 41 | When creating a container with the `docker run` command, bind the port to be used by `httpgd` with the `-p` (`--publish`) option. 42 | 43 | If you run R in a container with a command like the following, the 8888 port of the container will be bound to the 8888 port of the Docker host. 44 | 45 | ```sh 46 | docker run --rm -it -p 8888:8888 httpgd:test R 47 | ``` 48 | 49 | ### Start httpgd server 50 | 51 | Running the following command in the R console will initialize the graphics device and start the server. 52 | 53 | ```R 54 | httpgd::hgd(host = "0.0.0.0", port = 8888) 55 | ``` 56 | 57 | Then, copy the displayed link in your browser. 58 | 59 | If you want to display the link again, execute the `hgd_url()` function as follows. 60 | The hostname can be replaced with any value (e.g. localhost). 61 | 62 | ```R 63 | httpgd::hgd_url(host = "localhost") 64 | ``` 65 | 66 | ## Advanced usage 67 | 68 | ### Set options in Rprofile 69 | 70 | By setting options `httpgd.host` and `httpgd.port` in the Rprofile, you can omit setting the arguments when starting the httpgd server by `hgd()`. 71 | 72 | For example, if you create a Dockerfile with the following contents, you can build an image with these options already set in the Rprofile. 73 | 74 | ```Dockerfile 75 | FROM r-base:latest 76 | 77 | # Install httpgd and dependent packages. 78 | RUN apt-get update \ 79 | && apt-get install -y --no-install-recommends \ 80 | libfontconfig1-dev \ 81 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* \ 82 | && install2.r --error --skipinstalled --ncpu -1 \ 83 | httpgd \ 84 | && rm -rf /tmp/downloaded_packages 85 | 86 | # Set default values used in the httpgd::hgd() function. 87 | RUN echo 'options(httpgd.host = "0.0.0.0", httpgd.port = 8888)' >> /etc/R/Rprofile.site 88 | 89 | EXPOSE 8888 90 | ``` 91 | -------------------------------------------------------------------------------- /client/src/views/toolbarView.ts: -------------------------------------------------------------------------------- 1 | import { HttpgdViewer } from '../viewer'; 2 | import { getById } from '../utils' 3 | 4 | interface ToolbarAction { 5 | id: string, 6 | keys: number[], 7 | altKey?: boolean, 8 | f: () => void, 9 | } 10 | 11 | export class ToolbarView { 12 | static readonly DELAY_FADE_OUT: number = 4000; 13 | private static readonly CSS_SHOW_DROPDOWN: string = "drop-open"; 14 | 15 | private viewer: HttpgdViewer; 16 | 17 | private timoutFade?: ReturnType; 18 | 19 | private elemToolbar: HTMLElement; 20 | private elemContainer: HTMLElement; 21 | 22 | private dropdown: HTMLElement; 23 | private zoomLabel: HTMLElement; 24 | private pageLabel: HTMLElement; 25 | 26 | constructor(viewer: HttpgdViewer) { 27 | this.viewer = viewer; 28 | this.dropdown = getById("tb-more").parentElement; 29 | this.dropdown.onmouseenter = () => this.showDropdown(); 30 | this.dropdown.onmouseleave = () => this.hideDropdown(); 31 | 32 | this.zoomLabel = getById("tb-zlvl"); 33 | this.pageLabel = getById("tb-pnum"); 34 | 35 | this.elemToolbar = getById("toolbar"); 36 | this.elemContainer = getById("container"); 37 | 38 | this.timoutFade = setTimeout(() => this.fadeOut(), ToolbarView.DELAY_FADE_OUT); 39 | this.elemContainer.onmousemove = () => { 40 | this.elemToolbar.classList.remove("fade-out"); 41 | clearTimeout(this.timoutFade); 42 | this.timoutFade = setTimeout(() => this.fadeOut(), ToolbarView.DELAY_FADE_OUT); 43 | } 44 | this.elemContainer.onmouseleave = () => this.fadeOut(); 45 | } 46 | 47 | public registerActions(actions: ToolbarAction[]): void { 48 | const shortcuts: { [id: number]: ToolbarAction | undefined } = {}; 49 | for (const a of actions) { 50 | getById(a.id).onclick = () => { 51 | a.f(); 52 | this.hideDropdown(); 53 | }; 54 | if (a.keys) { 55 | for (const k of a.keys) { 56 | shortcuts[k] = a; 57 | } 58 | } 59 | } 60 | 61 | window.addEventListener('keydown', (e) => { 62 | if (this.viewer.exportView.isVisible()) return; 63 | const a = shortcuts[e.keyCode]; 64 | if (a && (!a.altKey || e.altKey)) { 65 | a.f(); 66 | e.preventDefault(); 67 | return; 68 | } 69 | }); 70 | } 71 | 72 | private showDropdown() { 73 | this.dropdown.classList.add(ToolbarView.CSS_SHOW_DROPDOWN); 74 | } 75 | private hideDropdown() { 76 | this.dropdown.classList.remove(ToolbarView.CSS_SHOW_DROPDOWN); 77 | } 78 | 79 | public setZoomLabelText(s: string): void { 80 | this.zoomLabel.childNodes[0].nodeValue = s; 81 | } 82 | public setPageLabelText(s: string): void { 83 | this.pageLabel.childNodes[0].nodeValue = s; 84 | } 85 | 86 | private fadeOut() { 87 | this.elemToolbar.classList.add("fade-out"); 88 | } 89 | } -------------------------------------------------------------------------------- /client/src/views/toolbarData.ts: -------------------------------------------------------------------------------- 1 | interface ToolbarEntry { 2 | title: string, 3 | shortcut?: string, 4 | id: string, 5 | content: string, 6 | } 7 | 8 | interface ToolbarData { 9 | main: ToolbarEntry[][], 10 | more: ToolbarEntry, 11 | dropdown: ToolbarEntry[], 12 | } 13 | 14 | const icon = (id: string): string => require("../assets/icons/" + id + ".svg"); 15 | 16 | export const data: ToolbarData = { 17 | main: [ 18 | [ 19 | { 20 | title: "Previous", 21 | shortcut: "←", 22 | id: "tb-left", 23 | content: icon("arrow-left"), 24 | }, 25 | { 26 | title: "Newest", 27 | shortcut: "N", 28 | id: "tb-pnum", 29 | content: "0/0", 30 | }, 31 | { 32 | title: "Next", 33 | shortcut: "→", 34 | id: "tb-right", 35 | content: icon("arrow-right"), 36 | }, 37 | ], 38 | [ 39 | { 40 | title: "Zoom out", 41 | shortcut: "-", 42 | id: "tb-minus", 43 | content: icon("magnify-minus"), 44 | }, 45 | { 46 | title: "Reset zoom", 47 | shortcut: "0", 48 | id: "tb-zlvl", 49 | content: "100%", 50 | }, 51 | { 52 | title: "Zoom in", 53 | shortcut: "+", 54 | id: "tb-plus", 55 | content: icon("magnify-plus"), 56 | }, 57 | ], 58 | [ 59 | { 60 | title: "Delete", 61 | shortcut: "D", 62 | id: "tb-remove", 63 | content: icon("cross"), 64 | }, 65 | ], 66 | ], 67 | more: 68 | { 69 | title: "More", 70 | id: "tb-more", 71 | content: icon("vdots"), 72 | }, 73 | dropdown: [ 74 | { 75 | title: "Download SVG", 76 | shortcut: "S", 77 | id: "tb-save-svg", 78 | content: icon("download"), 79 | }, 80 | { 81 | title: "Download PNG", 82 | shortcut: "P", 83 | id: "tb-save-png", 84 | content: icon("image"), 85 | }, 86 | { 87 | title: "Copy PNG", 88 | shortcut: "C", 89 | id: "tb-copy-png", 90 | content: icon("copy"), 91 | }, 92 | { 93 | title: "Clear all plots", 94 | shortcut: "Alt+D", 95 | id: "tb-clear", 96 | content: icon("trash"), 97 | }, 98 | { 99 | title: "Export", 100 | shortcut: "E", 101 | id: "tb-export", 102 | content: icon("export"), 103 | }, 104 | { 105 | title: "Show history", 106 | shortcut: "H", 107 | id: "tb-history", 108 | content: icon("hamburger"), 109 | }, 110 | ] 111 | }; -------------------------------------------------------------------------------- /.github/workflows/rhub.yaml: -------------------------------------------------------------------------------- 1 | # R-hub's generic GitHub Actions workflow file. It's canonical location is at 2 | # https://github.com/r-hub/actions/blob/v1/workflows/rhub.yaml 3 | # You can update this file to a newer version using the rhub2 package: 4 | # 5 | # rhub::rhub_setup() 6 | # 7 | # It is unlikely that you need to modify this file manually. 8 | 9 | name: R-hub 10 | run-name: "${{ github.event.inputs.id }}: ${{ github.event.inputs.name || format('Manually run by {0}', github.triggering_actor) }}" 11 | 12 | on: 13 | workflow_dispatch: 14 | inputs: 15 | config: 16 | description: 'A comma separated list of R-hub platforms to use.' 17 | type: string 18 | default: 'linux,windows,macos' 19 | name: 20 | description: 'Run name. You can leave this empty now.' 21 | type: string 22 | id: 23 | description: 'Unique ID. You can leave this empty now.' 24 | type: string 25 | 26 | jobs: 27 | 28 | setup: 29 | runs-on: ubuntu-latest 30 | outputs: 31 | containers: ${{ steps.rhub-setup.outputs.containers }} 32 | platforms: ${{ steps.rhub-setup.outputs.platforms }} 33 | 34 | steps: 35 | # NO NEED TO CHECKOUT HERE 36 | - uses: r-hub/actions/setup@v1 37 | with: 38 | config: ${{ github.event.inputs.config }} 39 | id: rhub-setup 40 | 41 | linux-containers: 42 | needs: setup 43 | if: ${{ needs.setup.outputs.containers != '[]' }} 44 | runs-on: ubuntu-latest 45 | name: ${{ matrix.config.label }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | config: ${{ fromJson(needs.setup.outputs.containers) }} 50 | container: 51 | image: ${{ matrix.config.container }} 52 | 53 | steps: 54 | - uses: r-hub/actions/checkout@v1 55 | - uses: r-hub/actions/platform-info@v1 56 | with: 57 | token: ${{ secrets.RHUB_TOKEN }} 58 | job-config: ${{ matrix.config.job-config }} 59 | - uses: r-hub/actions/setup-deps@v1 60 | with: 61 | token: ${{ secrets.RHUB_TOKEN }} 62 | job-config: ${{ matrix.config.job-config }} 63 | - uses: r-hub/actions/run-check@v1 64 | with: 65 | token: ${{ secrets.RHUB_TOKEN }} 66 | job-config: ${{ matrix.config.job-config }} 67 | 68 | other-platforms: 69 | needs: setup 70 | if: ${{ needs.setup.outputs.platforms != '[]' }} 71 | runs-on: ${{ matrix.config.os }} 72 | name: ${{ matrix.config.label }} 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | config: ${{ fromJson(needs.setup.outputs.platforms) }} 77 | 78 | steps: 79 | - uses: r-hub/actions/checkout@v1 80 | - uses: r-hub/actions/setup-r@v1 81 | with: 82 | job-config: ${{ matrix.config.job-config }} 83 | token: ${{ secrets.RHUB_TOKEN }} 84 | - uses: r-hub/actions/platform-info@v1 85 | with: 86 | token: ${{ secrets.RHUB_TOKEN }} 87 | job-config: ${{ matrix.config.job-config }} 88 | - uses: r-hub/actions/setup-deps@v1 89 | with: 90 | job-config: ${{ matrix.config.job-config }} 91 | token: ${{ secrets.RHUB_TOKEN }} 92 | - uses: r-hub/actions/run-check@v1 93 | with: 94 | job-config: ${{ matrix.config.job-config }} 95 | token: ${{ secrets.RHUB_TOKEN }} 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `httpgd` 2 | 3 | 4 | [![R-CMD-check](https://github.com/nx10/httpgd/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/nx10/httpgd/actions/workflows/R-CMD-check.yaml) 5 | [![CRAN](https://www.r-pkg.org/badges/version/httpgd)](https://CRAN.R-project.org/package=httpgd) 6 | ![downloads](https://cranlogs.r-pkg.org/badges/grand-total/httpgd) 7 | [![Codecov test coverage](https://codecov.io/gh/nx10/httpgd/branch/master/graph/badge.svg)](https://app.codecov.io/gh/nx10/httpgd?branch=master) 8 | 9 | 10 | A graphics device for R that is accessible via network protocols. 11 | This package was created to make it easier to embed live R graphics in 12 | integrated development environments and other applications. 13 | The included HTML/JavaScript client (plot viewer) aims to provide a better overall user experience when dealing with R graphics. 14 | The device asynchronously serves graphics via HTTP and WebSockets. 15 | 16 | ## Features 17 | 18 | * Fast plotting 19 | * Interactive plot viewer (client) 20 | * Supports multiple clients concurrently 21 | * Plot resizing and history 22 | * Export various image formats (SVG, PNG, PDF, EPS, ...) 23 | * Powered by [`unigd`](https://github.com/nx10/unigd) 24 | * For developers: 25 | * Stateless asynchronous HTTP/WebSocket API 26 | * HTML/JavaScript client (TypeScript module) 27 | 28 | ## Demo 29 | 30 | ![](https://user-images.githubusercontent.com/33600480/113182768-92eeda80-9253-11eb-9505-79de107024f7.gif) 31 | 32 | ## Installation 33 | 34 | Install `httpgd` from CRAN: 35 | 36 | ```R 37 | install.packages("httpgd") 38 | ``` 39 | 40 | Or get the latest development version from GitHub: 41 | 42 | ```R 43 | remotes::install_github("nx10/httpgd") 44 | ``` 45 | 46 | See [system requirements](https://nx10.github.io/httpgd/articles/a00_installation.html#system-requirements) for troubleshooting. 47 | 48 | 49 | ### Documentation 50 | 51 | - For users: 52 | - [How to get started](https://nx10.github.io/httpgd/articles/a01_how-to-get-started.html) 53 | - [Plotting in `unigd`](https://nx10.github.io/unigd/articles/b00_guide.html) 54 | - [Function reference](https://nx10.github.io/httpgd/reference/index.html) 55 | 56 | - IDEs & evironments: 57 | - [VS Code](https://nx10.github.io/httpgd/articles/b01_vscode.html) 58 | - [RStudio](https://nx10.github.io/httpgd/articles/b02_rstudio.html) 59 | - [Docker](https://nx10.github.io/httpgd/articles/b03_docker.html) 60 | - For package developers: 61 | - [httpgd API](https://nx10.github.io/httpgd/articles/c01_httpgd-api.html) 62 | 63 | 64 | 65 | ## Contributions welcome! 66 | 67 | The various components of `httpgd` are written in C++, R and TypeScript. We welcome contributions of any kind. 68 | 69 | Other areas in need of improvement are testing and documentation. 70 | 71 | ## Links & Articles 72 | 73 | - [Using httpgd in VSCode: A web-based SVG graphics device](https://renkun.me/2020/06/16/using-httpgd-in-vscode-a-web-based-svg-graphics-device/) 74 | 75 | ## About & License 76 | 77 | Depends on `cpp11`. 78 | 79 | Webserver based on [`CrowCpp/Crow`](). 80 | 81 | This project is licensed GPL v2.0. 82 | 83 | The HTML client includes [Material Design icons by Google](https://github.com/google/material-design-icons) which are licensed under the [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt). 84 | 85 | Full copies of the license agreements used by these components are included in [`./inst/licenses`](https://github.com/nx10/httpgd/tree/master/inst/licenses). 86 | -------------------------------------------------------------------------------- /man/hgd.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/httpgd.R 3 | \name{hgd} 4 | \alias{hgd} 5 | \title{Asynchronous HTTP server graphics device.} 6 | \usage{ 7 | hgd( 8 | host = getOption("httpgd.host", "127.0.0.1"), 9 | port = getOption("httpgd.port", 0), 10 | cors = getOption("httpgd.cors", FALSE), 11 | token = getOption("httpgd.token", TRUE), 12 | silent = getOption("httpgd.silent", FALSE), 13 | width = getOption("httpgd.width", 720), 14 | height = getOption("httpgd.height", 576), 15 | zoom = getOption("httpgd.zoom", 1), 16 | bg = getOption("httpgd.bg", "white"), 17 | pointsize = getOption("httpgd.pointsize", 12), 18 | system_fonts = getOption("httpgd.system_fonts", list()), 19 | user_fonts = getOption("httpgd.user_fonts", list()), 20 | reset_par = getOption("httpgd.reset_par", FALSE) 21 | ) 22 | } 23 | \arguments{ 24 | \item{host}{Server hostname. Set to \code{"0.0.0.0"} to enable remote access. 25 | We recommend to \strong{only enable remote access in trusted networks}. 26 | The network security of httpgd has not yet been properly tested.} 27 | 28 | \item{port}{Server port. If this is set to \code{0}, an open port 29 | will be assigned.} 30 | 31 | \item{cors}{Toggles Cross-Origin Resource Sharing (CORS) header. 32 | When set to \code{TRUE}, CORS header will be set to \code{"*"}.} 33 | 34 | \item{token}{(Optional) security token. When set, all requests 35 | need to include a token to be allowed. (Either in a request header 36 | (\code{X-HTTPGD-TOKEN}) field or as a query parameter.) 37 | This parameter can be set to \code{TRUE} to generate a random 8 character 38 | alphanumeric token. A random token of the specified length is generated 39 | when it is set to a number. \code{FALSE} deactivates the token.} 40 | 41 | \item{silent}{When set to \code{FALSE} no information will be printed to console.} 42 | 43 | \item{width}{Initial plot width (pixels).} 44 | 45 | \item{height}{Initial plot height (pixels).} 46 | 47 | \item{zoom}{Initial plot zoom level (For example: 2 corresponds 48 | to 200\%, 0.5 would be 50\%.).} 49 | 50 | \item{bg}{Background color.} 51 | 52 | \item{pointsize}{Graphics device point size.} 53 | 54 | \item{system_fonts}{Named list of font names to be aliased with 55 | fonts installed on your system. If unspecified, the R default 56 | families \code{sans}, \code{serif}, \code{mono} and \code{symbol} 57 | are aliased to the family returned by 58 | \code{systemfonts::font_info()}.} 59 | 60 | \item{user_fonts}{Named list of fonts to be aliased with font files 61 | provided by the user rather than fonts properly installed on the 62 | system. The aliases can be fonts from the fontquiver package, 63 | strings containing a path to a font file, or a list containing 64 | \code{name} and \code{file} elements with \code{name} indicating 65 | the font alias in the SVG output and \code{file} the path to a 66 | font file.} 67 | 68 | \item{reset_par}{If set to \code{TRUE}, global graphics parameters will be saved 69 | on device start and reset every time the plots are cleared (see 70 | \code{\link[graphics:par]{graphics::par()}}).} 71 | } 72 | \value{ 73 | No return value, called to initialize graphics device. 74 | } 75 | \description{ 76 | This function initializes a httpgd graphics device and 77 | starts a local webserver, that allows for access via HTTP and WebSockets. 78 | A link will be printed by which the web client can be accessed using 79 | a browser. 80 | } 81 | \details{ 82 | All font settings and descriptions are adopted from the excellent 83 | 'svglite' package. 84 | } 85 | \examples{ 86 | \dontrun{ 87 | 88 | hgd() # Initialize graphics device and start server 89 | hgd_browse() # Or copy the displayed link in the browser 90 | 91 | # Plot something 92 | x <- seq(0, 3 * pi, by = 0.1) 93 | plot(x, sin(x), type = "l") 94 | 95 | dev.off() # alternatively: hgd_close() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/crow/http_request.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef ASIO_STANDALONE 4 | #define ASIO_STANDALONE 5 | #endif 6 | #include 7 | 8 | #include "crow/common.h" 9 | #include "crow/ci_map.h" 10 | #include "crow/query_string.h" 11 | 12 | namespace crow 13 | { 14 | /// Find and return the value associated with the key. (returns an empty string if nothing is found) 15 | template 16 | inline const std::string& get_header_value(const T& headers, const std::string& key) 17 | { 18 | if (headers.count(key)) 19 | { 20 | return headers.find(key)->second; 21 | } 22 | static std::string empty; 23 | return empty; 24 | } 25 | 26 | /// An HTTP request. 27 | struct request 28 | { 29 | HTTPMethod method; 30 | std::string raw_url; ///< The full URL containing the `?` and URL parameters. 31 | std::string url; ///< The endpoint without any parameters. 32 | query_string url_params; ///< The parameters associated with the request. (everything after the `?` in the URL) 33 | ci_map headers; 34 | std::string body; 35 | std::string remote_ip_address; ///< The IP address from which the request was sent. 36 | unsigned char http_ver_major, http_ver_minor; 37 | bool keep_alive, ///< Whether or not the server should send a `connection: Keep-Alive` header to the client. 38 | close_connection, ///< Whether or not the server should shut down the TCP connection once a response is sent. 39 | upgrade; ///< Whether or noth the server should change the HTTP connection to a different connection. 40 | 41 | void* middleware_context{}; 42 | void* middleware_container{}; 43 | asio::io_service* io_service{}; 44 | 45 | /// Construct an empty request. (sets the method to `GET`) 46 | request(): 47 | method(HTTPMethod::Get) 48 | {} 49 | 50 | /// Construct a request with all values assigned. 51 | request(HTTPMethod method, std::string raw_url, std::string url, query_string url_params, ci_map headers, std::string body, unsigned char http_major, unsigned char http_minor, bool has_keep_alive, bool has_close_connection, bool is_upgrade): 52 | method(method), raw_url(std::move(raw_url)), url(std::move(url)), url_params(std::move(url_params)), headers(std::move(headers)), body(std::move(body)), http_ver_major(http_major), http_ver_minor(http_minor), keep_alive(has_keep_alive), close_connection(has_close_connection), upgrade(is_upgrade) 53 | {} 54 | 55 | void add_header(std::string key, std::string value) 56 | { 57 | headers.emplace(std::move(key), std::move(value)); 58 | } 59 | 60 | const std::string& get_header_value(const std::string& key) const 61 | { 62 | return crow::get_header_value(headers, key); 63 | } 64 | 65 | bool check_version(unsigned char major, unsigned char minor) const 66 | { 67 | return http_ver_major == major && http_ver_minor == minor; 68 | } 69 | 70 | /// Get the body as parameters in QS format. 71 | 72 | /// 73 | /// This is meant to be used with requests of type "application/x-www-form-urlencoded" 74 | const query_string get_body_params() const 75 | { 76 | return query_string(body, false); 77 | } 78 | 79 | /// Send data to whoever made this request with a completion handler and return immediately. 80 | template 81 | void post(CompletionHandler handler) 82 | { 83 | io_service->post(handler); 84 | } 85 | 86 | /// Send data to whoever made this request with a completion handler. 87 | template 88 | void dispatch(CompletionHandler handler) 89 | { 90 | io_service->dispatch(handler); 91 | } 92 | }; 93 | } // namespace crow 94 | -------------------------------------------------------------------------------- /src/lib/crow/compression.h: -------------------------------------------------------------------------------- 1 | #ifdef CROW_ENABLE_COMPRESSION 2 | #pragma once 3 | 4 | #include 5 | #include 6 | 7 | // http://zlib.net/manual.html 8 | namespace crow 9 | { 10 | namespace compression 11 | { 12 | // Values used in the 'windowBits' parameter for deflateInit2. 13 | enum algorithm 14 | { 15 | // 15 is the default value for deflate 16 | DEFLATE = 15, 17 | // windowBits can also be greater than 15 for optional gzip encoding. 18 | // Add 16 to windowBits to write a simple gzip header and trailer around the compressed data instead of a zlib wrapper. 19 | GZIP = 15 | 16, 20 | }; 21 | 22 | inline std::string compress_string(std::string const& str, algorithm algo) 23 | { 24 | std::string compressed_str; 25 | z_stream stream{}; 26 | // Initialize with the default values 27 | if (::deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, algo, 8, Z_DEFAULT_STRATEGY) == Z_OK) 28 | { 29 | char buffer[8192]; 30 | 31 | stream.avail_in = str.size(); 32 | // zlib does not take a const pointer. The data is not altered. 33 | stream.next_in = const_cast(reinterpret_cast(str.c_str())); 34 | 35 | int code = Z_OK; 36 | do 37 | { 38 | stream.avail_out = sizeof(buffer); 39 | stream.next_out = reinterpret_cast(&buffer[0]); 40 | 41 | code = ::deflate(&stream, Z_FINISH); 42 | // Successful and non-fatal error code returned by deflate when used with Z_FINISH flush 43 | if (code == Z_OK || code == Z_STREAM_END) 44 | { 45 | std::copy(&buffer[0], &buffer[sizeof(buffer) - stream.avail_out], std::back_inserter(compressed_str)); 46 | } 47 | 48 | } while (code == Z_OK); 49 | 50 | if (code != Z_STREAM_END) 51 | compressed_str.clear(); 52 | 53 | ::deflateEnd(&stream); 54 | } 55 | 56 | return compressed_str; 57 | } 58 | 59 | inline std::string decompress_string(std::string const& deflated_string) 60 | { 61 | std::string inflated_string; 62 | Bytef tmp[8192]; 63 | 64 | z_stream zstream{}; 65 | zstream.avail_in = deflated_string.size(); 66 | // Nasty const_cast but zlib won't alter its contents 67 | zstream.next_in = const_cast(reinterpret_cast(deflated_string.c_str())); 68 | // Initialize with automatic header detection, for gzip support 69 | if (::inflateInit2(&zstream, MAX_WBITS | 32) == Z_OK) 70 | { 71 | do 72 | { 73 | zstream.avail_out = sizeof(tmp); 74 | zstream.next_out = &tmp[0]; 75 | 76 | auto ret = ::inflate(&zstream, Z_NO_FLUSH); 77 | if (ret == Z_OK || ret == Z_STREAM_END) 78 | { 79 | std::copy(&tmp[0], &tmp[sizeof(tmp) - zstream.avail_out], std::back_inserter(inflated_string)); 80 | } 81 | else 82 | { 83 | // Something went wrong with inflate; make sure we return an empty string 84 | inflated_string.clear(); 85 | break; 86 | } 87 | 88 | } while (zstream.avail_out == 0); 89 | 90 | // Free zlib's internal memory 91 | ::inflateEnd(&zstream); 92 | } 93 | 94 | return inflated_string; 95 | } 96 | } // namespace compression 97 | } // namespace crow 98 | 99 | #endif 100 | -------------------------------------------------------------------------------- /client/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function getById(id: string): T { 2 | const el = document.getElementById(id); 3 | if (!el) { 4 | throw new ReferenceError(id + " is not defined"); 5 | } 6 | return el as T; 7 | } 8 | 9 | export function strcmp(a: string, b: string): number { 10 | if (a < b) { 11 | return -1; 12 | } 13 | if (a > b) { 14 | return 1; 15 | } 16 | return 0; 17 | } 18 | 19 | export function downloadURL(url: string, filename?: string, tab?: boolean): void { 20 | const dl = document.createElement('a'); 21 | dl.href = url; 22 | if (filename) { dl.download = filename; } 23 | if (tab) { dl.target = '_blank'; } 24 | document.body.appendChild(dl); 25 | dl.click(); 26 | document.body.removeChild(dl); 27 | } 28 | 29 | export function copyClipboardPNG(url: string): void { 30 | if (!navigator.clipboard?.write) { 31 | console.warn("No clipboard API support!"); 32 | return; 33 | } 34 | 35 | fetch(url).then(res => res.blob()).then(blob => { 36 | if (!blob) 37 | return; 38 | navigator.clipboard.write([new ClipboardItem( 39 | Object.defineProperty({}, "image/png", { 40 | value: blob, 41 | enumerable: true 42 | }) 43 | )]); 44 | }) 45 | } 46 | 47 | export function imageTempCanvas(image: HTMLImageElement, fn: (canvas: HTMLCanvasElement) => Promise): void { 48 | const canvas = document.createElement('canvas'); 49 | canvas.style.display = "none"; 50 | document.body.appendChild(canvas); 51 | const rect = image.getBoundingClientRect(); 52 | canvas.width = rect.width; 53 | canvas.height = rect.height; 54 | const ctx = canvas.getContext('2d'); 55 | if (!ctx) return; 56 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height); 57 | fn(canvas).finally(() => document.body.removeChild(canvas)); 58 | } 59 | 60 | export function downloadImgSVG(image: HTMLImageElement, filename: string): void { 61 | fetch(image.src).then((response) => { 62 | return response.blob(); 63 | }).then(blob => { 64 | downloadURL(URL.createObjectURL(blob), filename); 65 | }); 66 | } 67 | 68 | export function downloadImgPNG(image: HTMLImageElement, filename: string): void { 69 | imageTempCanvas(image, async canvas => { 70 | const imgURI = canvas 71 | .toDataURL('image/png') 72 | .replace('image/png', 'image/octet-stream'); 73 | downloadURL(imgURI, filename); 74 | }); 75 | } 76 | 77 | export function copyImgSVGasPNG(image: HTMLImageElement): void { 78 | if (!navigator.clipboard?.write) { 79 | console.warn("No clipboard API support!"); 80 | return; 81 | } 82 | imageTempCanvas(image, async canvas => { 83 | const blob = await new Promise((resolve) => canvas.toBlob(resolve)); 84 | if (!blob) 85 | return; 86 | return await navigator.clipboard.write([new ClipboardItem( 87 | Object.defineProperty({}, blob.type, { 88 | value: blob, 89 | enumerable: true 90 | }) 91 | )]); 92 | }); 93 | 94 | } 95 | 96 | export function validNumberInput(input: HTMLInputElement, min: number, max: number): boolean { 97 | const s = input.value; 98 | if (!s.match(/^\d+$/)) return false; 99 | const v = parseInt(s); 100 | return (v >= min) && (v <= max); 101 | } 102 | 103 | export function setCssClass(element: HTMLElement, set: boolean, cssClass: string): boolean { 104 | if (set) { 105 | element.classList.add(cssClass); 106 | } else { 107 | element.classList.remove(cssClass); 108 | } 109 | return set; 110 | } 111 | 112 | const supportsNativeSmoothScroll = 'scrollBehavior' in document.documentElement.style; 113 | 114 | export function safeScrollTo(elem: Element, options: ScrollToOptions): void { 115 | if (supportsNativeSmoothScroll) { 116 | elem.scrollTo(options); 117 | } else { 118 | elem.scrollTo(options.left ? options.left : 0, options.top ? options.top : 0); 119 | } 120 | } -------------------------------------------------------------------------------- /client/src/views/sidebarView.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpgdViewer } from '../viewer'; 3 | import { HttpgdPlotsResponse } from 'httpgd/lib/types'; 4 | import { getById, safeScrollTo } from '../utils' 5 | 6 | export class SidebarView { 7 | private viewer: HttpgdViewer; 8 | 9 | //private elemContainer: HTMLElement; 10 | private elemPlotView: HTMLElement; 11 | private elemSidebar: HTMLElement; 12 | 13 | constructor(viewer: HttpgdViewer, hidden?: boolean) { 14 | this.viewer = viewer; 15 | this.elemPlotView = getById("plotview"); 16 | this.elemSidebar = getById("sidebar"); 17 | 18 | if (hidden) { 19 | this.elemSidebar.classList.add('notransition', 'nohist'); 20 | this.elemPlotView.classList.add('notransition', 'nohist'); 21 | setTimeout(() => { 22 | this.elemSidebar.classList.remove('notransition'); 23 | this.elemPlotView.classList.remove('notransition'); 24 | }, 300); 25 | } 26 | } 27 | 28 | /** 29 | * Loops throgh the plot list and adds or removes sidebar thumbnail cards as needed. 30 | * 31 | * @param plots 32 | */ 33 | public update(plots: HttpgdPlotsResponse): void { 34 | //this.sidebar.innerHTML = ''; 35 | 36 | let idx = 0; 37 | while (idx < this.elemSidebar.children.length) { 38 | if (idx >= plots.plots.length || this.elemSidebar.children[idx].getAttribute('data-pid') !== plots.plots[idx].id) { 39 | this.elemSidebar.removeChild(this.elemSidebar.children[idx]); 40 | } else { 41 | idx++; 42 | } 43 | } 44 | 45 | for (; idx < plots.plots.length; ++idx) { 46 | const p = plots.plots[idx]; 47 | const elem_card = document.createElement("div"); 48 | elem_card.setAttribute('data-pid', p.id); 49 | const elem_x = document.createElement("a"); 50 | elem_x.innerHTML = "✖" 51 | elem_x.onclick = () => { 52 | this.viewer.httpgd.removePlot({ id: p.id }) 53 | this.viewer.httpgd.updatePlots(); 54 | }; 55 | const elem_img = document.createElement("img"); 56 | elem_card.classList.add("history-item"); 57 | elem_img.setAttribute('src', this.viewer.httpgd.getPlotURL({ id: p.id })); 58 | elem_card.onclick = () => this.viewer.plotView.setPage(p.id); 59 | elem_card.appendChild(elem_img); 60 | elem_card.appendChild(elem_x); 61 | this.elemSidebar.appendChild(elem_card); 62 | } 63 | } 64 | 65 | public setSelected(plotId: string): void { 66 | 67 | let activeCard: Element; 68 | 69 | for (let i = 0; i < this.elemSidebar.children.length; ++i) { 70 | const card = this.elemSidebar.children[i]; 71 | if (card.getAttribute('data-pid') === plotId) { 72 | card.classList.add("history-selected"); 73 | activeCard = card; 74 | } else { 75 | card.classList.remove("history-selected"); 76 | } 77 | } 78 | 79 | if (activeCard) { // scroll to card 80 | const bb_card = activeCard.getBoundingClientRect(); 81 | const y_card = bb_card.y + this.elemSidebar.scrollTop; 82 | const h_card = bb_card.height; 83 | const h_sidebar = this.elemSidebar.getBoundingClientRect().height; 84 | const h = y_card - (h_sidebar/2 - h_card/2); 85 | safeScrollTo(this.elemSidebar, { 86 | top: h, 87 | behavior: 'smooth' 88 | }); 89 | } 90 | } 91 | 92 | public toggle(): void { 93 | this.elemSidebar.classList.toggle('nohist'); 94 | this.elemPlotView.classList.toggle('nohist'); 95 | setTimeout(() => this.viewer.plotView.update(), 300); 96 | } 97 | 98 | public hideWithoutAnimation(): void { 99 | this.elemSidebar.classList.add('notransition', 'nohist'); 100 | this.elemPlotView.classList.add('notransition', 'nohist'); 101 | this.viewer.plotView.update(); 102 | setTimeout(() => { 103 | this.elemSidebar.classList.remove('notransition'); 104 | this.elemPlotView.classList.remove('notransition'); 105 | }, 300); 106 | } 107 | } -------------------------------------------------------------------------------- /tests/testthat/test-server.R: -------------------------------------------------------------------------------- 1 | test_that("State status OK", { 2 | hgd(token = FALSE, silent = TRUE) 3 | res <- fetch_get(hgd_url("state")) 4 | dev.off() 5 | expect_equal(httr::status_code(res), 200) 6 | }) 7 | 8 | test_that("State token check", { 9 | tok <- "abc123" 10 | hgd(token = tok, silent = TRUE) 11 | res_no_token <- fetch_get( 12 | hgd_url("state", omit_token = TRUE)) 13 | res_wrong_token <- fetch_get( 14 | hgd_url("state", omit_token = TRUE, token = "xyz321")) 15 | res_correct_token <- fetch_get( 16 | hgd_url("state", omit_token = TRUE, token = tok)) 17 | dev.off() 18 | expect_equal(httr::status_code(res_no_token), 401) 19 | expect_equal(httr::status_code(res_wrong_token), 401) 20 | expect_equal(httr::status_code(res_correct_token), 200) 21 | }) 22 | 23 | test_that("live status OK", { 24 | hgd(token = FALSE, silent = TRUE) 25 | plot.new() 26 | res <- fetch_get(hgd_url("live")) 27 | dev.off() 28 | expect_equal(httr::status_code(res), 200) 29 | }) 30 | 31 | test_that("Render status OK", { 32 | hgd(token = FALSE, silent = TRUE) 33 | plot.new() 34 | res <- fetch_get(hgd_url("plot")) 35 | dev.off() 36 | expect_equal(httr::status_code(res), 200) 37 | }) 38 | 39 | test_that("All renderers OK", { 40 | hgd(token = FALSE, silent = TRUE) 41 | plot.new() 42 | vres <- vapply(unigd::ugd_renderers()$id, function(renderer_id) { 43 | httr::status_code(fetch_get(hgd_url("plot", renderer=renderer_id))) == 200 44 | }, logical(1)) 45 | dev.off() 46 | expect_true(all(vres)) 47 | }) 48 | 49 | test_that("Plot identical", { 50 | hgd(token = FALSE, silent = TRUE) 51 | plot.new() 52 | res <- fetch_get(hgd_url("plot", renderer = "json")) 53 | uplt <- unigd::ugd_render(as = "json") 54 | dev.off() 55 | expect_equal(httr::content(res, as = "text"), uplt) 56 | }) 57 | 58 | test_that("Renderer info identical", { 59 | hgd(token = FALSE, silent = TRUE) 60 | res <- fetch_get(hgd_url("renderers")) 61 | dev.off() 62 | df_res <- jsonlite::fromJSON(httr::content(res, as = "text"))$renderers 63 | df_ugd <- unigd::ugd_renderers() 64 | 65 | # this API should change at some point 66 | names(df_res)[names(df_res) == "bin"] <- "text" 67 | df_res$text <- !df_res$text 68 | 69 | sort_df <- function(df, id_col = "id") { 70 | df[order(df[[id_col]]), order(colnames(df))] 71 | } 72 | df_res <- sort_df(df_res) 73 | df_ugd <- sort_df(df_ugd) 74 | 75 | expect_equal(colnames(df_res), colnames(df_ugd)) 76 | for (cname in colnames(df_res)) { 77 | expect_equal(df_res[[cname]], df_ugd[[cname]]) 78 | } 79 | }) 80 | 81 | test_that("Clear plots", { 82 | fetch_json_hgd <- function(...) { 83 | jsonlite::fromJSON( 84 | httr::content( 85 | fetch_get( 86 | hgd_url(...)), as = "text"), simplifyVector = FALSE) 87 | } 88 | 89 | hgd(token = FALSE, silent = TRUE) 90 | for (i in seq_len(10)) { 91 | plot(0, main = sprintf("plot_%i", i)) 92 | } 93 | 94 | json_plots <- fetch_json_hgd("plots") 95 | 96 | expect_equal(length(json_plots$plots), 10) 97 | expect_equal(json_plots$state$hsize, 10) 98 | 99 | fetch_get(hgd_url("clear")) 100 | 101 | json_plots <- fetch_json_hgd("plots") 102 | 103 | expect_equal(length(json_plots$plots), 0) 104 | expect_equal(json_plots$state$hsize, 0) 105 | 106 | dev.off() 107 | }) 108 | 109 | test_that("Delete plot", { 110 | fetch_txt_hgd <- function(...) { 111 | httr::content( 112 | fetch_get( 113 | hgd_url(...)), as = "text") 114 | } 115 | 116 | hgd(token = FALSE, silent = TRUE) 117 | for (i in seq_len(10)) { 118 | plot(0, main = sprintf("plot_%i", i)) 119 | } 120 | 121 | json_p4 <- fetch_txt_hgd("plot", index = 4 - 1, renderer = "json") 122 | json_p5 <- fetch_txt_hgd("plot", index = 5 - 1, renderer = "json") 123 | 124 | expect_true(grepl("\"str\": \"plot_4\"", json_p4)) 125 | expect_true(grepl("\"str\": \"plot_5\"", json_p5)) 126 | 127 | fetch_get(paste0(hgd_url("remove", index = 5 - 1))) 128 | 129 | json_p4 <- fetch_txt_hgd("plot", index = 4 - 1, renderer = "json") 130 | json_p5 <- fetch_txt_hgd("plot", index = 5 - 1, renderer = "json") 131 | 132 | expect_true(grepl("\"str\": \"plot_4\"", json_p4)) 133 | expect_true(grepl("\"str\": \"plot_6\"", json_p5)) 134 | 135 | dev.off() 136 | }) 137 | 138 | test_that("Delete plot status", { 139 | hgd(token = FALSE, silent = TRUE) 140 | for (i in seq_len(10)) { 141 | plot(0, main = sprintf("plot_%i", i)) 142 | } 143 | 144 | expect_equal(httr::status_code(fetch_get(hgd_url("remove", index = 4))), 200) 145 | expect_equal(httr::status_code(fetch_get(hgd_url("remove", index = 99))), 404) 146 | 147 | dev.off() 148 | }) 149 | -------------------------------------------------------------------------------- /src/lib/crow/mime_types.h: -------------------------------------------------------------------------------- 1 | // This file is generated from nginx/conf/mime.types using nginx_mime2cpp.py on 2021-12-03. 2 | #include 3 | #include 4 | 5 | namespace crow 6 | { 7 | const std::unordered_map mime_types{ 8 | {"shtml", "text/html"}, 9 | {"htm", "text/html"}, 10 | {"html", "text/html"}, 11 | {"css", "text/css"}, 12 | {"xml", "text/xml"}, 13 | {"gif", "image/gif"}, 14 | {"jpg", "image/jpeg"}, 15 | {"jpeg", "image/jpeg"}, 16 | {"js", "application/javascript"}, 17 | {"atom", "application/atom+xml"}, 18 | {"rss", "application/rss+xml"}, 19 | {"mml", "text/mathml"}, 20 | {"txt", "text/plain"}, 21 | {"jad", "text/vnd.sun.j2me.app-descriptor"}, 22 | {"wml", "text/vnd.wap.wml"}, 23 | {"htc", "text/x-component"}, 24 | {"avif", "image/avif"}, 25 | {"png", "image/png"}, 26 | {"svgz", "image/svg+xml"}, 27 | {"svg", "image/svg+xml"}, 28 | {"tiff", "image/tiff"}, 29 | {"tif", "image/tiff"}, 30 | {"wbmp", "image/vnd.wap.wbmp"}, 31 | {"webp", "image/webp"}, 32 | {"ico", "image/x-icon"}, 33 | {"jng", "image/x-jng"}, 34 | {"bmp", "image/x-ms-bmp"}, 35 | {"woff", "font/woff"}, 36 | {"woff2", "font/woff2"}, 37 | {"ear", "application/java-archive"}, 38 | {"war", "application/java-archive"}, 39 | {"jar", "application/java-archive"}, 40 | {"json", "application/json"}, 41 | {"hqx", "application/mac-binhex40"}, 42 | {"doc", "application/msword"}, 43 | {"pdf", "application/pdf"}, 44 | {"ai", "application/postscript"}, 45 | {"eps", "application/postscript"}, 46 | {"ps", "application/postscript"}, 47 | {"rtf", "application/rtf"}, 48 | {"m3u8", "application/vnd.apple.mpegurl"}, 49 | {"kml", "application/vnd.google-earth.kml+xml"}, 50 | {"kmz", "application/vnd.google-earth.kmz"}, 51 | {"xls", "application/vnd.ms-excel"}, 52 | {"eot", "application/vnd.ms-fontobject"}, 53 | {"ppt", "application/vnd.ms-powerpoint"}, 54 | {"odg", "application/vnd.oasis.opendocument.graphics"}, 55 | {"odp", "application/vnd.oasis.opendocument.presentation"}, 56 | {"ods", "application/vnd.oasis.opendocument.spreadsheet"}, 57 | {"odt", "application/vnd.oasis.opendocument.text"}, 58 | {"pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, 59 | {"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, 60 | {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, 61 | {"wmlc", "application/vnd.wap.wmlc"}, 62 | {"wasm", "application/wasm"}, 63 | {"7z", "application/x-7z-compressed"}, 64 | {"cco", "application/x-cocoa"}, 65 | {"jardiff", "application/x-java-archive-diff"}, 66 | {"jnlp", "application/x-java-jnlp-file"}, 67 | {"run", "application/x-makeself"}, 68 | {"pm", "application/x-perl"}, 69 | {"pl", "application/x-perl"}, 70 | {"pdb", "application/x-pilot"}, 71 | {"prc", "application/x-pilot"}, 72 | {"rar", "application/x-rar-compressed"}, 73 | {"rpm", "application/x-redhat-package-manager"}, 74 | {"sea", "application/x-sea"}, 75 | {"swf", "application/x-shockwave-flash"}, 76 | {"sit", "application/x-stuffit"}, 77 | {"tk", "application/x-tcl"}, 78 | {"tcl", "application/x-tcl"}, 79 | {"crt", "application/x-x509-ca-cert"}, 80 | {"pem", "application/x-x509-ca-cert"}, 81 | {"der", "application/x-x509-ca-cert"}, 82 | {"xpi", "application/x-xpinstall"}, 83 | {"xhtml", "application/xhtml+xml"}, 84 | {"xspf", "application/xspf+xml"}, 85 | {"zip", "application/zip"}, 86 | {"dll", "application/octet-stream"}, 87 | {"exe", "application/octet-stream"}, 88 | {"bin", "application/octet-stream"}, 89 | {"deb", "application/octet-stream"}, 90 | {"dmg", "application/octet-stream"}, 91 | {"img", "application/octet-stream"}, 92 | {"iso", "application/octet-stream"}, 93 | {"msm", "application/octet-stream"}, 94 | {"msp", "application/octet-stream"}, 95 | {"msi", "application/octet-stream"}, 96 | {"kar", "audio/midi"}, 97 | {"midi", "audio/midi"}, 98 | {"mid", "audio/midi"}, 99 | {"mp3", "audio/mpeg"}, 100 | {"ogg", "audio/ogg"}, 101 | {"m4a", "audio/x-m4a"}, 102 | {"ra", "audio/x-realaudio"}, 103 | {"3gp", "video/3gpp"}, 104 | {"3gpp", "video/3gpp"}, 105 | {"ts", "video/mp2t"}, 106 | {"mp4", "video/mp4"}, 107 | {"mpg", "video/mpeg"}, 108 | {"mpeg", "video/mpeg"}, 109 | {"mov", "video/quicktime"}, 110 | {"webm", "video/webm"}, 111 | {"flv", "video/x-flv"}, 112 | {"m4v", "video/x-m4v"}, 113 | {"mng", "video/x-mng"}, 114 | {"asf", "video/x-ms-asf"}, 115 | {"asx", "video/x-ms-asf"}, 116 | {"wmv", "video/x-ms-wmv"}, 117 | {"avi", "video/x-msvideo"}}; 118 | } 119 | -------------------------------------------------------------------------------- /client/src/viewer.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Httpgd } from 'httpgd'; 3 | import { HttpgdPlotsResponse } from 'httpgd/lib/types'; 4 | import { ExportView } from './views/exportView'; 5 | import { OverlayView } from './views/overlayView'; 6 | import { PlotView } from './views/plotView'; 7 | 8 | export class HttpgdViewer { 9 | static readonly COOLDOWN_DEVICE_INACTIVE: number = 1000; 10 | 11 | public httpgd: Httpgd; 12 | public plotView?: PlotView; 13 | public overlayView?: OverlayView; 14 | public exportView?: ExportView; 15 | public sidebarHidden: boolean = false; 16 | 17 | private deviceInactiveDelayed?: ReturnType; 18 | 19 | public onDeviceActiveChange?: (deviceActive: boolean) => void; 20 | public onDisconnectedChange?: (disconnected: boolean) => void; 21 | public onIndexStringChange?: (indexString: string) => void; 22 | public onZoomStringChange?: (zoomString: string) => void; 23 | 24 | public constructor(host: string, token?: string, allowWebsockets?: boolean, sidebarHidden?: boolean) { 25 | this.httpgd = new Httpgd(host, token, allowWebsockets); 26 | 27 | if (sidebarHidden) { this.sidebarHidden = true; } 28 | 29 | this.httpgd.onPlotsChanged((newState) => this.plotsChanged(newState)); 30 | this.httpgd.onConnectionChanged((newState) => this.connectionChanged(newState)); 31 | this.httpgd.onDeviceActiveChanged((newState) => this.deviceActiveChanged(newState)); 32 | } 33 | 34 | private plotsChanged(newState: HttpgdPlotsResponse): void { 35 | this.plotView?.updatePlots(newState); 36 | this.plotView?.update(); 37 | } 38 | 39 | private connectionChanged(newState: boolean): void { 40 | if (newState) { 41 | this.overlayView?.show(OverlayView.TEXT_CONNECTION_LOST); 42 | } else { 43 | this.overlayView?.hide(); 44 | } 45 | } 46 | 47 | private deviceActiveChanged(active: boolean): void { 48 | if (this.deviceInactiveDelayed) { 49 | clearTimeout(this.deviceInactiveDelayed); 50 | } 51 | if (!active) { 52 | this.deviceInactiveDelayed = setTimeout( 53 | () => this.overlayView?.show(OverlayView.TEXT_DEVICE_INACTIVE), 54 | HttpgdViewer.COOLDOWN_DEVICE_INACTIVE); 55 | } else { 56 | this.overlayView?.hide(); 57 | } 58 | } 59 | 60 | public init(): void { 61 | 62 | this.plotView = new PlotView(this, this.sidebarHidden); 63 | this.overlayView = new OverlayView(this); 64 | this.exportView = new ExportView(this); 65 | 66 | this.httpgd.connect().then(() => { 67 | console.log('Connected to httpgd ' + this.httpgd.getInfo().version.httpgd); 68 | this.exportView.initRenderers(); 69 | }); 70 | 71 | this.plotView.toolbar.registerActions([ 72 | { 73 | keys: [37, 40], 74 | f: () => this.plotView.prevPage(), 75 | id: "tb-left", 76 | }, 77 | { 78 | keys: [39, 38], 79 | f: () => this.plotView.nextPage(), 80 | id: "tb-right", 81 | }, 82 | { 83 | keys: [78], 84 | f: () => this.plotView.newestPage(), 85 | id: "tb-pnum", 86 | }, 87 | { 88 | keys: [187], 89 | f: () => this.plotView.zoomIn(), 90 | id: "tb-plus", 91 | }, 92 | { 93 | keys: [189], 94 | f: () => this.plotView.zoomOut(), 95 | id: "tb-minus", 96 | }, 97 | { 98 | keys: [48], 99 | f: () => this.plotView.zoomReset(), 100 | id: "tb-zlvl", 101 | }, 102 | { 103 | id: "tb-clear", 104 | altKey: true, 105 | keys: [68], 106 | f: () => this.plotView.clearPlots(), 107 | }, 108 | { 109 | id: "tb-remove", 110 | keys: [46, 68], 111 | f: () => this.plotView.removePlot(), 112 | }, 113 | { 114 | id: "tb-save-svg", 115 | keys: [83], 116 | f: () => this.plotView.downloadSVG(), 117 | }, 118 | { 119 | id: "tb-save-png", 120 | keys: [80], 121 | f: () => this.plotView.downloadPNG(), 122 | }, 123 | { 124 | id: "tb-copy-png", 125 | keys: [67], 126 | f: () => this.plotView.copyPNG(), 127 | }, 128 | { 129 | id: "tb-history", 130 | keys: [72], 131 | f: () => this.plotView.sidebar.toggle(), 132 | }, 133 | { 134 | id: "tb-export", 135 | keys: [69], 136 | f: () => this.exportView.show(), 137 | }, 138 | ]); 139 | } 140 | } -------------------------------------------------------------------------------- /src/lib/crow/socket_adaptors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef ASIO_STANDALONE 3 | #define ASIO_STANDALONE 4 | #endif 5 | #include 6 | #ifdef CROW_ENABLE_SSL 7 | #include 8 | #endif 9 | #include "crow/settings.h" 10 | #include 11 | #if ASIO_VERSION >= 101300 // 1.13.0 12 | #define GET_IO_SERVICE(s) ((asio::io_context&)(s).get_executor().context()) 13 | #else 14 | #define GET_IO_SERVICE(s) ((s).get_io_service()) 15 | #endif 16 | namespace crow 17 | { 18 | using tcp = asio::ip::tcp; 19 | 20 | /// A wrapper for the asio::ip::tcp::socket and asio::ssl::stream 21 | struct SocketAdaptor 22 | { 23 | using context = void; 24 | SocketAdaptor(asio::io_service& io_service, context*): 25 | socket_(io_service) 26 | {} 27 | 28 | asio::io_service& get_io_service() 29 | { 30 | return GET_IO_SERVICE(socket_); 31 | } 32 | 33 | /// Get the TCP socket handling data trasfers, regardless of what layer is handling transfers on top of the socket. 34 | tcp::socket& raw_socket() 35 | { 36 | return socket_; 37 | } 38 | 39 | /// Get the object handling data transfers, this can be either a TCP socket or an SSL stream (if SSL is enabled). 40 | tcp::socket& socket() 41 | { 42 | return socket_; 43 | } 44 | 45 | tcp::endpoint remote_endpoint() 46 | { 47 | return socket_.remote_endpoint(); 48 | } 49 | 50 | bool is_open() 51 | { 52 | return socket_.is_open(); 53 | } 54 | 55 | void close() 56 | { 57 | asio::error_code ec; 58 | socket_.close(ec); 59 | } 60 | 61 | void shutdown_readwrite() 62 | { 63 | asio::error_code ec; 64 | socket_.shutdown(asio::socket_base::shutdown_type::shutdown_both, ec); 65 | } 66 | 67 | void shutdown_write() 68 | { 69 | asio::error_code ec; 70 | socket_.shutdown(asio::socket_base::shutdown_type::shutdown_send, ec); 71 | } 72 | 73 | void shutdown_read() 74 | { 75 | asio::error_code ec; 76 | socket_.shutdown(asio::socket_base::shutdown_type::shutdown_receive, ec); 77 | } 78 | 79 | template 80 | void start(F f) 81 | { 82 | f(asio::error_code()); 83 | } 84 | 85 | tcp::socket socket_; 86 | }; 87 | 88 | #ifdef CROW_ENABLE_SSL 89 | struct SSLAdaptor 90 | { 91 | using context = asio::ssl::context; 92 | using ssl_socket_t = asio::ssl::stream; 93 | SSLAdaptor(asio::io_service& io_service, context* ctx): 94 | ssl_socket_(new ssl_socket_t(io_service, *ctx)) 95 | {} 96 | 97 | asio::ssl::stream& socket() 98 | { 99 | return *ssl_socket_; 100 | } 101 | 102 | tcp::socket::lowest_layer_type& 103 | raw_socket() 104 | { 105 | return ssl_socket_->lowest_layer(); 106 | } 107 | 108 | tcp::endpoint remote_endpoint() 109 | { 110 | return raw_socket().remote_endpoint(); 111 | } 112 | 113 | bool is_open() 114 | { 115 | return ssl_socket_ ? raw_socket().is_open() : false; 116 | } 117 | 118 | void close() 119 | { 120 | if (is_open()) 121 | { 122 | asio::error_code ec; 123 | raw_socket().close(ec); 124 | } 125 | } 126 | 127 | void shutdown_readwrite() 128 | { 129 | if (is_open()) 130 | { 131 | asio::error_code ec; 132 | raw_socket().shutdown(asio::socket_base::shutdown_type::shutdown_both, ec); 133 | } 134 | } 135 | 136 | void shutdown_write() 137 | { 138 | if (is_open()) 139 | { 140 | asio::error_code ec; 141 | raw_socket().shutdown(asio::socket_base::shutdown_type::shutdown_send, ec); 142 | } 143 | } 144 | 145 | void shutdown_read() 146 | { 147 | if (is_open()) 148 | { 149 | asio::error_code ec; 150 | raw_socket().shutdown(asio::socket_base::shutdown_type::shutdown_receive, ec); 151 | } 152 | } 153 | 154 | asio::io_service& get_io_service() 155 | { 156 | return GET_IO_SERVICE(raw_socket()); 157 | } 158 | 159 | template 160 | void start(F f) 161 | { 162 | ssl_socket_->async_handshake(asio::ssl::stream_base::server, 163 | [f](const asio::error_code& ec) { 164 | f(ec); 165 | }); 166 | } 167 | 168 | std::unique_ptr> ssl_socket_; 169 | }; 170 | #endif 171 | } // namespace crow 172 | -------------------------------------------------------------------------------- /inst/www/style.css: -------------------------------------------------------------------------------- 1 | body{margin:0}#container{height:100%;display:relative}.plotview{height:100%;width:80%;position:relative;transition:width .3s;box-shadow:0 4px 8px 0 rgba(0,0,0,.2);z-index:1}.plotview.nohist{width:100%;transition:width .3s}#drawing{width:100%;height:100%}#overlay{position:fixed;display:none;width:100%;height:100%;background-color:rgba(92,92,92,.685);z-index:2}#overlay-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);font-size:40px;font-family:Helvetica,Arial,sans-serif;color:#fff;text-shadow:2px 2px 8px #2e2e2e}.history{width:20%;height:100%;overflow-y:scroll;overflow-x:hidden;position:fixed;top:0;right:0;transition:right .3s;background-color:#fcfcfc}.history.nohist{right:-20%;visibility:hidden;transition:right .3s,visibility .3s}.history-item{box-shadow:0 4px 8px 0 rgba(0,0,0,.2);margin:8;cursor:pointer;position:relative;background-color:#fff}.history-item img{width:100%;height:12vw}.history-item a{position:absolute;top:0;right:0;color:rgba(0,0,0,.5);text-decoration:none;font-weight:bold;font-size:22px;padding:0px 4px 0px 0px;margin:-2px 0px 0px 0px;display:none}.history-item a:hover{color:#da4567}.history-item:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,.2)}.history-item:hover a{display:inline}.history-selected{outline:rgba(0,119,204,.3) solid 3px}#toolbar{float:left;position:absolute;right:0;top:0px;font-family:Helvetica,Arial,sans-serif;font-size:18px;font-weight:bold;-webkit-user-select:none;-ms-user-select:none;user-select:none;background-color:rgba(253,253,254,.8);border-color:rgba(253,253,254,.8);border-radius:2px}#tb-tools{white-space:nowrap}#tb-tools>span{padding:2px 2px 2px 2px;margin:0px 2px 0px 2px;color:rgba(0,0,0,.5);cursor:pointer}#tb-tools>span>a{margin:0px -2px 0px -2px}#tb-tools>span>a:hover{color:#07c}.icon{width:24px;height:24px;position:relative;top:5px;fill:rgba(0,0,0,.5)}.icon:hover{fill:#07c}.icon-warn:hover{fill:#da4567}.tooltip{position:relative;display:inline-block}.tooltip .tooltiptext{visibility:hidden;font-size:12px;font-weight:normal;width:80px;background-color:#474747;color:#fff;text-align:center;border-radius:6px;padding:5px 0;position:absolute;z-index:1;top:150%;left:50%;margin-left:-40px;opacity:0;transition:opacity .2s}.tooltip .tooltiptext::after{content:"";position:absolute;bottom:100%;left:50%;margin-left:-5px;border-width:5px;border-style:solid;border-color:rgba(0,0,0,0) rgba(0,0,0,0) #474747 rgba(0,0,0,0)}.tooltip:hover .tooltiptext{visibility:visible;opacity:1}.fade-out{opacity:0;transform:translateY(-100px);transition:opacity .5s,transform .5s step-end .5s}.drop{position:relative;display:inline-block;height:34px}.drop ul{position:absolute;right:-2px;top:20px;transition:all .3s ease;transform:scale(0);transform-origin:top right;box-shadow:0 2px 4px 0 rgba(0,0,0,.16),0 2px 8px 0 rgba(0,0,0,.12);padding:0px;background-color:#fff;font-size:16px;font-weight:bold}.drop ul li{display:block;width:100%}.drop ul li a{width:100%;padding:10px 14px 10px 12px;display:flex;white-space:pre;box-sizing:border-box;fill:rgba(0,0,0,.5)}.drop ul li a svg{padding-right:10px;margin-top:-4px;padding-bottom:4px}.drop ul li a span{position:relative;top:4px}.drop ul li a span.drop-kbd{margin-left:auto;padding-left:12px;color:rgba(0,0,0,.3)}.drop ul li a:hover{background:#ebebeb;color:#07c}.drop ul li a:hover svg{fill:#07c}.drop ul li a.warn-hover:hover{color:#da4567;fill:#da4567}.drop-open ul{transform:scale(1)}.notransition{transition:none !important}.modal{display:none;position:fixed;z-index:10;padding-top:10vh;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgba(0,0,0,.4)}.modal-content{background-color:#fefefe;margin:auto;padding:8 20 20 20;width:80%;height:70%;display:flex;flex-direction:column}#exp-modal-close{color:#aaa;float:right;font-size:32px;font-weight:bold;display:flex;flex-direction:column;text-align:right}#exp-modal-close:hover,#exp-modal-close:focus{color:#000;text-decoration:none;cursor:pointer}.export-preview{text-align:center;display:flex;flex-direction:column;flex:1;min-height:0;align-items:center;justify-content:center}.export-preview img{background-color:rgba(0,0,0,.2);width:auto;max-width:100%;max-height:100%;max-height:-webkit-fill-available;box-shadow:0 2px 4px 0 rgba(0,0,0,.16),0 2px 8px 0 rgba(0,0,0,.12)}.export-options{font-family:Helvetica,Arial,sans-serif;font-size:16px;display:flex;flex-direction:row;justify-content:space-between;flex-wrap:wrap;margin-bottom:-14px;margin-top:6px}.export-options .export-tools span{vertical-align:middle;font-size:24;color:#888}.export-options .export-tools .num-input{width:80}.export-options .export-tools #ie-btn-open{margin-left:10px}.num-input,select{padding:8px 16px;margin:8px 0;display:inline-block;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}.invalid-input{background-color:#faa}.num-input:hover,.num-input:focus{border-color:#07c;outline-color:#07c}.but-input{background-color:#888;color:#fff;padding:8px 16px;margin:8px 0;border:none;border-radius:4px;cursor:pointer}.but-input:hover{background-color:#07c}.loader{position:absolute;left:50%;top:50%;z-index:3;border:12px solid #f3f3f3;border-color:#4190c9 #fbfbfb #4190c9 #fbfbfb;border-radius:50%;width:14vh;height:14vh;animation:spin 2s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} 2 | -------------------------------------------------------------------------------- /src/lib/crow/logging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "crow/settings.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace crow 13 | { 14 | enum class LogLevel 15 | { 16 | #ifndef ERROR 17 | #ifndef DEBUG 18 | DEBUG = 0, 19 | INFO, 20 | WARNING, 21 | ERROR, 22 | CRITICAL, 23 | #endif 24 | #endif 25 | 26 | Debug = 0, 27 | Info, 28 | Warning, 29 | Error, 30 | Critical, 31 | }; 32 | 33 | class ILogHandler 34 | { 35 | public: 36 | virtual ~ILogHandler() = default; 37 | 38 | virtual void log(std::string message, LogLevel level) = 0; 39 | }; 40 | 41 | class CerrLogHandler : public ILogHandler 42 | { 43 | public: 44 | void log(std::string message, LogLevel level) override 45 | { 46 | std::string prefix; 47 | switch (level) 48 | { 49 | case LogLevel::Debug: 50 | prefix = "DEBUG "; 51 | break; 52 | case LogLevel::Info: 53 | prefix = "INFO "; 54 | break; 55 | case LogLevel::Warning: 56 | prefix = "WARNING "; 57 | break; 58 | case LogLevel::Error: 59 | prefix = "ERROR "; 60 | break; 61 | case LogLevel::Critical: 62 | prefix = "CRITICAL"; 63 | break; 64 | } 65 | //std::cerr << std::string("(") + timestamp() + std::string(") [") + prefix + std::string("] ") + message << std::endl; 66 | } 67 | 68 | private: 69 | static std::string timestamp() 70 | { 71 | char date[32]; 72 | time_t t = time(0); 73 | 74 | tm my_tm; 75 | 76 | #if defined(_MSC_VER) || defined(__MINGW32__) 77 | #ifdef CROW_USE_LOCALTIMEZONE 78 | localtime_s(&my_tm, &t); 79 | #else 80 | gmtime_s(&my_tm, &t); 81 | #endif 82 | #else 83 | #ifdef CROW_USE_LOCALTIMEZONE 84 | localtime_r(&t, &my_tm); 85 | #else 86 | gmtime_r(&t, &my_tm); 87 | #endif 88 | #endif 89 | 90 | size_t sz = strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", &my_tm); 91 | return std::string(date, date + sz); 92 | } 93 | }; 94 | 95 | class logger 96 | { 97 | public: 98 | logger(LogLevel level): 99 | level_(level) 100 | {} 101 | ~logger() 102 | { 103 | #ifdef CROW_ENABLE_LOGGING 104 | if (level_ >= get_current_log_level()) 105 | { 106 | get_handler_ref()->log(stringstream_.str(), level_); 107 | } 108 | #endif 109 | } 110 | 111 | // 112 | template 113 | logger& operator<<(T const& value) 114 | { 115 | #ifdef CROW_ENABLE_LOGGING 116 | if (level_ >= get_current_log_level()) 117 | { 118 | stringstream_ << value; 119 | } 120 | #endif 121 | return *this; 122 | } 123 | 124 | // 125 | static void setLogLevel(LogLevel level) { get_log_level_ref() = level; } 126 | 127 | static void setHandler(ILogHandler* handler) { get_handler_ref() = handler; } 128 | 129 | static LogLevel get_current_log_level() { return get_log_level_ref(); } 130 | 131 | private: 132 | // 133 | static LogLevel& get_log_level_ref() 134 | { 135 | static LogLevel current_level = static_cast(CROW_LOG_LEVEL); 136 | return current_level; 137 | } 138 | static ILogHandler*& get_handler_ref() 139 | { 140 | static CerrLogHandler default_handler; 141 | static ILogHandler* current_handler = &default_handler; 142 | return current_handler; 143 | } 144 | 145 | // 146 | std::ostringstream stringstream_; 147 | LogLevel level_; 148 | }; 149 | } // namespace crow 150 | 151 | #define CROW_LOG_CRITICAL \ 152 | if (crow::logger::get_current_log_level() <= crow::LogLevel::Critical) \ 153 | crow::logger(crow::LogLevel::Critical) 154 | #define CROW_LOG_ERROR \ 155 | if (crow::logger::get_current_log_level() <= crow::LogLevel::Error) \ 156 | crow::logger(crow::LogLevel::Error) 157 | #define CROW_LOG_WARNING \ 158 | if (crow::logger::get_current_log_level() <= crow::LogLevel::Warning) \ 159 | crow::logger(crow::LogLevel::Warning) 160 | #define CROW_LOG_INFO \ 161 | if (crow::logger::get_current_log_level() <= crow::LogLevel::Info) \ 162 | crow::logger(crow::LogLevel::Info) 163 | #define CROW_LOG_DEBUG \ 164 | if (crow::logger::get_current_log_level() <= crow::LogLevel::Debug) \ 165 | crow::logger(crow::LogLevel::Debug) 166 | -------------------------------------------------------------------------------- /inst/www/index.html: -------------------------------------------------------------------------------- 1 | R Plot -------------------------------------------------------------------------------- /src/lib/crow/task_timer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef ASIO_STANDALONE 4 | #define ASIO_STANDALONE 5 | #endif 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "crow/logging.h" 15 | 16 | namespace crow 17 | { 18 | namespace detail 19 | { 20 | 21 | /// A class for scheduling functions to be called after a specific amount of ticks. A tick is equal to 1 second. 22 | class task_timer 23 | { 24 | public: 25 | using task_type = std::function; 26 | using identifier_type = size_t; 27 | 28 | private: 29 | using clock_type = std::chrono::steady_clock; 30 | using time_type = clock_type::time_point; 31 | 32 | public: 33 | task_timer(asio::io_service& io_service): 34 | io_service_(io_service), timer_(io_service_) 35 | { 36 | timer_.expires_after(std::chrono::seconds(1)); 37 | timer_.async_wait( 38 | std::bind(&task_timer::tick_handler, this, std::placeholders::_1)); 39 | } 40 | 41 | ~task_timer() { timer_.cancel(); } 42 | 43 | void cancel(identifier_type id) 44 | { 45 | tasks_.erase(id); 46 | CROW_LOG_DEBUG << "task_timer cancelled: " << this << ' ' << id; 47 | } 48 | 49 | /// Schedule the given task to be executed after the default amount of ticks. 50 | 51 | /// 52 | /// \return identifier_type Used to cancel the thread. 53 | /// It is not bound to this task_timer instance and in some cases could lead to 54 | /// undefined behavior if used with other task_timer objects or after the task 55 | /// has been successfully executed. 56 | identifier_type schedule(const task_type& task) 57 | { 58 | tasks_.insert( 59 | {++highest_id_, 60 | {clock_type::now() + std::chrono::seconds(get_default_timeout()), 61 | task}}); 62 | CROW_LOG_DEBUG << "task_timer scheduled: " << this << ' ' << highest_id_; 63 | return highest_id_; 64 | } 65 | 66 | /// Schedule the given task to be executed after the given time. 67 | 68 | /// 69 | /// \param timeout The amount of ticks (seconds) to wait before execution. 70 | /// 71 | /// \return identifier_type Used to cancel the thread. 72 | /// It is not bound to this task_timer instance and in some cases could lead to 73 | /// undefined behavior if used with other task_timer objects or after the task 74 | /// has been successfully executed. 75 | identifier_type schedule(const task_type& task, std::uint8_t timeout) 76 | { 77 | tasks_.insert({++highest_id_, 78 | {clock_type::now() + std::chrono::seconds(timeout), task}}); 79 | CROW_LOG_DEBUG << "task_timer scheduled: " << this << ' ' << highest_id_; 80 | return highest_id_; 81 | } 82 | 83 | /// Set the default timeout for this task_timer instance. (Default: 5) 84 | 85 | /// 86 | /// \param timeout The amount of ticks (seconds) to wait before execution. 87 | void set_default_timeout(std::uint8_t timeout) { default_timeout_ = timeout; } 88 | 89 | /// Get the default timeout. (Default: 5) 90 | std::uint8_t get_default_timeout() const { return default_timeout_; } 91 | 92 | private: 93 | void process_tasks() 94 | { 95 | time_type current_time = clock_type::now(); 96 | std::vector finished_tasks; 97 | 98 | for (const auto& task : tasks_) 99 | { 100 | if (task.second.first < current_time) 101 | { 102 | (task.second.second)(); 103 | finished_tasks.push_back(task.first); 104 | CROW_LOG_DEBUG << "task_timer called: " << this << ' ' << task.first; 105 | } 106 | } 107 | 108 | for (const auto& task : finished_tasks) 109 | tasks_.erase(task); 110 | 111 | // If no task is currently scheduled, reset the issued ids back to 0. 112 | if (tasks_.empty()) highest_id_ = 0; 113 | } 114 | 115 | void tick_handler(const asio::error_code& ec) 116 | { 117 | if (ec) return; 118 | 119 | process_tasks(); 120 | 121 | timer_.expires_after(std::chrono::seconds(1)); 122 | timer_.async_wait( 123 | std::bind(&task_timer::tick_handler, this, std::placeholders::_1)); 124 | } 125 | 126 | private: 127 | std::uint8_t default_timeout_{5}; 128 | asio::io_service& io_service_; 129 | asio::basic_waitable_timer timer_; 130 | std::map> tasks_; 131 | 132 | // A continuosly increasing number to be issued to threads to identify them. 133 | // If no tasks are scheduled, it will be reset to 0. 134 | identifier_type highest_id_{0}; 135 | }; 136 | } // namespace detail 137 | } // namespace crow 138 | -------------------------------------------------------------------------------- /src/optional_lex.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_OPTIONAL_LEX_H__ 2 | #define __UNIGD_OPTIONAL_LEX_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace httpgd 9 | { 10 | 11 | /** 12 | * @brief Convert a string to a std::experimental::optional. 13 | * 14 | * @tparam T The type to convert to. 15 | * @param t_param The string to convert. 16 | * @return std::experimental::optional The converted value. 17 | */ 18 | template 19 | inline std::experimental::optional param_to(const char* t_param) 20 | { 21 | static_assert(sizeof(T) == 0, "Unsupported type"); 22 | return std::experimental::nullopt; 23 | } 24 | 25 | // Specializations 26 | 27 | template <> 28 | inline std::experimental::optional param_to(const char* t_param) 29 | { 30 | if (t_param == nullptr) 31 | { 32 | return std::experimental::nullopt; 33 | } 34 | try 35 | { 36 | return std::stoi(t_param); 37 | } 38 | catch (const std::invalid_argument& ia) 39 | { 40 | return std::experimental::nullopt; 41 | } 42 | catch (const std::out_of_range& oor) 43 | { 44 | return std::experimental::nullopt; 45 | } 46 | } 47 | 48 | template <> 49 | inline std::experimental::optional param_to(const char* t_param) 50 | { 51 | if (t_param == nullptr) 52 | { 53 | return std::experimental::nullopt; 54 | } 55 | try 56 | { 57 | return std::stoul(t_param); 58 | } 59 | catch (const std::invalid_argument& ia) 60 | { 61 | return std::experimental::nullopt; 62 | } 63 | catch (const std::out_of_range& oor) 64 | { 65 | return std::experimental::nullopt; 66 | } 67 | } 68 | 69 | template <> 70 | inline std::experimental::optional param_to(const char* t_param) 71 | { 72 | if (t_param == nullptr) 73 | { 74 | return std::experimental::nullopt; 75 | } 76 | if (strcmp(t_param, "true") == 0) 77 | { 78 | return true; 79 | } 80 | if (strcmp(t_param, "false") == 0) 81 | { 82 | return false; 83 | } 84 | return std::experimental::nullopt; 85 | } 86 | 87 | template <> 88 | inline std::experimental::optional param_to(const char* t_param) 89 | { 90 | if (t_param == nullptr) 91 | { 92 | return std::experimental::nullopt; 93 | } 94 | try 95 | { 96 | return std::stod(t_param); 97 | } 98 | catch (const std::invalid_argument& ia) 99 | { 100 | return std::experimental::nullopt; 101 | } 102 | catch (const std::out_of_range& oor) 103 | { 104 | return std::experimental::nullopt; 105 | } 106 | } 107 | 108 | template <> 109 | inline std::experimental::optional param_to(const char* t_param) 110 | { 111 | if (t_param == nullptr) 112 | { 113 | return std::experimental::nullopt; 114 | } 115 | try 116 | { 117 | return std::stof(t_param); 118 | } 119 | catch (const std::invalid_argument& ia) 120 | { 121 | return std::experimental::nullopt; 122 | } 123 | catch (const std::out_of_range& oor) 124 | { 125 | return std::experimental::nullopt; 126 | } 127 | } 128 | 129 | template <> 130 | inline std::experimental::optional param_to(const char* t_param) 131 | { 132 | if (t_param == nullptr) 133 | { 134 | return std::experimental::nullopt; 135 | } 136 | try 137 | { 138 | return std::stol(t_param); 139 | } 140 | catch (const std::invalid_argument& ia) 141 | { 142 | return std::experimental::nullopt; 143 | } 144 | catch (const std::out_of_range& oor) 145 | { 146 | return std::experimental::nullopt; 147 | } 148 | } 149 | 150 | template <> 151 | inline std::experimental::optional param_to(const char* t_param) 152 | { 153 | if (t_param == nullptr) 154 | { 155 | return std::experimental::nullopt; 156 | } 157 | try 158 | { 159 | return std::stoul(t_param); 160 | } 161 | catch (const std::invalid_argument& ia) 162 | { 163 | return std::experimental::nullopt; 164 | } 165 | catch (const std::out_of_range& oor) 166 | { 167 | return std::experimental::nullopt; 168 | } 169 | } 170 | 171 | template <> 172 | inline std::experimental::optional param_to(const char* t_param) 173 | { 174 | if (t_param == nullptr) 175 | { 176 | return std::experimental::nullopt; 177 | } 178 | try 179 | { 180 | return std::stoll(t_param); 181 | } 182 | catch (const std::invalid_argument& ia) 183 | { 184 | return std::experimental::nullopt; 185 | } 186 | catch (const std::out_of_range& oor) 187 | { 188 | return std::experimental::nullopt; 189 | } 190 | } 191 | 192 | template <> 193 | inline std::experimental::optional param_to(const char* t_param) 194 | { 195 | if (t_param == nullptr) 196 | { 197 | return std::experimental::nullopt; 198 | } 199 | try 200 | { 201 | return std::stoull(t_param); 202 | } 203 | catch (const std::invalid_argument& ia) 204 | { 205 | return std::experimental::nullopt; 206 | } 207 | catch (const std::out_of_range& oor) 208 | { 209 | return std::experimental::nullopt; 210 | } 211 | } 212 | 213 | template <> 214 | inline std::experimental::optional param_to(const char* t_param) 215 | { 216 | if (t_param == nullptr) 217 | { 218 | return std::experimental::nullopt; 219 | } 220 | return std::string(t_param); 221 | } 222 | 223 | template <> 224 | inline std::experimental::optional param_to(const char* t_param) 225 | { 226 | if (t_param == nullptr) 227 | { 228 | return std::experimental::nullopt; 229 | } 230 | return t_param; 231 | } 232 | } // namespace httpgd 233 | 234 | #endif /* __UNIGD_OPTIONAL_LEX_H__ */ 235 | -------------------------------------------------------------------------------- /client/src/views/plotView.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-inferrable-types */ 2 | import { HttpgdViewer } from '../viewer'; 3 | import { HttpgdPlotsResponse } from 'httpgd/lib/types'; 4 | import { getById, downloadImgSVG, downloadImgPNG, copyImgSVGasPNG } from '../utils' 5 | import { ASSET_PLOT_NONE } from '../resources'; 6 | import { ToolbarView } from './toolbarView'; 7 | import { SidebarView } from './sidebarView'; 8 | 9 | export class PlotView { 10 | static readonly COOLDOWN_RESIZE: number = 200; 11 | static readonly SCALE_DEFAULT: number = 1.25; 12 | static readonly SCALE_STEP: number = PlotView.SCALE_DEFAULT / 12.0; 13 | static readonly SCALE_MIN: number = 0.5; 14 | 15 | private viewer: HttpgdViewer; 16 | public toolbar: ToolbarView; 17 | public sidebar: SidebarView; 18 | 19 | private image: HTMLImageElement; 20 | 21 | private resizeBlocked: boolean = false; 22 | private scale: number = PlotView.SCALE_DEFAULT; // zoom level 23 | private page: number = 1; 24 | private plots?: HttpgdPlotsResponse; 25 | 26 | constructor(viewer: HttpgdViewer, sidebarHidden?: boolean) { 27 | this.viewer = viewer; 28 | this.toolbar = new ToolbarView(viewer); 29 | this.sidebar = new SidebarView(viewer, sidebarHidden); 30 | this.image = getById("drawing"); 31 | this.image.src = ASSET_PLOT_NONE; 32 | window.addEventListener("resize", () => this.resize()); 33 | 34 | // TODO 35 | /* 36 | // Force reload on visibility change 37 | // Firefox otherwise shows a blank screen on tab change 38 | document.addEventListener('visibilitychange', () => { 39 | if (!document.hidden) { 40 | this.updateImage('v'); 41 | } 42 | }, false);*/ 43 | } 44 | 45 | public updatePlots(newState: HttpgdPlotsResponse): void { 46 | this.plots = newState; 47 | this.page = newState.plots.length; 48 | this.updatePageLabel(); 49 | this.sidebar.update(newState); 50 | } 51 | 52 | public getCurrentPlotId(): string | null { 53 | if (!this.plots || this.plots.plots.length == 0) return null; 54 | while (this.page < 1) { 55 | this.page += this.plots.plots.length; 56 | } 57 | while (this.page > this.plots.plots.length) { 58 | this.page -= this.plots.plots.length; 59 | } 60 | return this.plots.plots[this.page - 1].id; 61 | } 62 | 63 | public update(): void { 64 | if (!this.plots || this.plots.plots.length == 0) { 65 | this.image.src = ASSET_PLOT_NONE; 66 | return; 67 | } 68 | 69 | const plotId = this.getCurrentPlotId(); 70 | const rect = this.image.getBoundingClientRect(); 71 | const url = this.viewer.httpgd.getPlotURL({ 72 | id: plotId, 73 | width: rect.width, 74 | height: rect.height, 75 | zoom: this.scale, 76 | }) 77 | if (url) { 78 | this.image.src = url; 79 | } 80 | 81 | this.updatePageLabel(); 82 | this.sidebar.setSelected(plotId); 83 | } 84 | 85 | private resize() { 86 | if (this.resizeBlocked) return; 87 | this.resizeBlocked = true; 88 | setTimeout(() => { 89 | this.update(); 90 | this.resizeBlocked = false; 91 | }, PlotView.COOLDOWN_RESIZE); 92 | } 93 | 94 | // Zooming 95 | public zoomOut(): void { 96 | if (this.scale - PlotView.SCALE_STEP > PlotView.SCALE_MIN) { 97 | this.scale -= PlotView.SCALE_STEP; 98 | } 99 | this.updateZoomLabel(); 100 | this.resize(); 101 | } 102 | public zoomIn(): void { 103 | this.scale += PlotView.SCALE_STEP; 104 | this.updateZoomLabel(); 105 | this.resize(); 106 | } 107 | public zoomReset(): void { 108 | this.scale = PlotView.SCALE_DEFAULT; 109 | this.updateZoomLabel(); 110 | this.resize(); 111 | } 112 | private getZoomString(): string { 113 | return Math.ceil(this.scale / PlotView.SCALE_DEFAULT * 100) + '%'; 114 | } 115 | private updateZoomLabel() { 116 | this.toolbar.setZoomLabelText(this.getZoomString()); 117 | } 118 | 119 | // Pagenation 120 | public nextPage(): void { 121 | this.page++; 122 | this.update(); 123 | } 124 | public prevPage(): void { 125 | this.page--; 126 | this.update(); 127 | } 128 | public newestPage(): void { 129 | if (!this.plots) return; 130 | if (this.page == this.plots.plots.length) return; 131 | this.page = this.plots.plots.length; 132 | this.update(); 133 | } 134 | public setPage(plotId: string): void { 135 | if (!this.plots) return; 136 | for (let i = 0; i < this.plots.plots.length; ++i) { 137 | if (this.plots.plots[i].id === plotId) { 138 | this.page = i + 1; 139 | this.update(); 140 | return; 141 | } 142 | } 143 | } 144 | private getPageString(): string { 145 | return this.plots ? (this.page + "/" + this.plots.plots.length) : "0/0"; 146 | } 147 | private updatePageLabel() { 148 | this.toolbar.setPageLabelText(this.getPageString()); 149 | } 150 | 151 | public downloadSVG(): void { 152 | downloadImgSVG(this.image, 'plot.svg'); 153 | } 154 | 155 | public downloadPNG(): void { 156 | // todo: download server side rendered PNG if available 157 | downloadImgPNG(this.image, 'plot.png'); 158 | } 159 | 160 | public copyPNG(): void { 161 | // todo: copy server side rendered PNG if available 162 | copyImgSVGasPNG(this.image); 163 | } 164 | public removePlot(): void { 165 | this.viewer.httpgd.removePlot({ id: this.getCurrentPlotId() }); 166 | } 167 | public clearPlots(): void { 168 | this.viewer.httpgd.clearPlots(); 169 | } 170 | } -------------------------------------------------------------------------------- /client/src/views/exportView.ts: -------------------------------------------------------------------------------- 1 | import { HttpgdViewer } from '../viewer'; 2 | import { HttpgdRendererResponse } from 'httpgd/lib/types'; 3 | import { downloadURL, getById, validNumberInput, setCssClass, strcmp } from '../utils' 4 | import { ASSET_PLOT_NONE } from '../resources'; 5 | 6 | export class ExportView { 7 | static readonly MIN_PREVIEW_SIZE: number = 1; 8 | static readonly MAX_PREVIEW_SIZE: number = 10000; 9 | static readonly MIN_PREVIEW_ZOOM: number = 0.01; 10 | static readonly MAX_PREVIEW_ZOOM: number = 10000; 11 | 12 | private viewer: HttpgdViewer; 13 | 14 | private renderers: HttpgdRendererResponse[]; 15 | 16 | private elemModal: HTMLElement; 17 | private imgPreview: HTMLImageElement; 18 | private inputWidth: HTMLInputElement; 19 | private inputHeight: HTMLInputElement; 20 | private inputZoom: HTMLInputElement; 21 | private btnOpen: HTMLButtonElement; 22 | private btnDownload: HTMLButtonElement; 23 | private selectFormat: HTMLSelectElement; 24 | private btnClose: HTMLElement; 25 | 26 | constructor(viewer: HttpgdViewer) { 27 | this.viewer = viewer; 28 | 29 | this.elemModal = getById("exp-modal"); 30 | this.imgPreview = getById("exp-image"); 31 | this.inputWidth = getById("ie-width"); 32 | this.inputHeight = getById("ie-height"); 33 | this.inputZoom = getById("ie-scale"); 34 | this.btnOpen = getById("ie-btn-open"); 35 | this.btnDownload = getById("ie-btn-download"); 36 | this.selectFormat = getById("ie-format"); 37 | this.btnClose = getById("exp-modal-close"); 38 | 39 | this.btnClose.onclick = () => this.hide(); 40 | window.onmousedown = (event: MouseEvent) => { 41 | if (event.target == this.elemModal) { 42 | this.hide(); 43 | } 44 | } 45 | 46 | this.inputWidth.addEventListener('input', () => this.update()); 47 | this.inputHeight.addEventListener('input', () => this.update()); 48 | this.inputZoom.addEventListener('input', () => this.update()); 49 | this.btnDownload.onclick = () => this.clickDownload(); 50 | this.btnOpen.onclick = () => this.clickOpen(); 51 | } 52 | 53 | public initRenderers(): void { 54 | this.renderers = this.viewer.httpgd.getRenderers(); 55 | this.renderers.sort((a, b) => strcmp(a.name, b.name)).forEach((r) => { 56 | const o = document.createElement("option"); 57 | o.value = r.id; 58 | o.text = r.name + " (*"+ r.ext + ")"; 59 | o.title = r.descr; 60 | this.selectFormat.add(o); 61 | }); 62 | this.selectFormat.value = "svg"; 63 | } 64 | 65 | private hide() { 66 | this.elemModal.style.display = "none"; 67 | } 68 | 69 | public isVisible(): boolean { 70 | return this.elemModal.style.display && 71 | this.elemModal.style.display !== "none"; 72 | } 73 | 74 | private validWidth(): boolean { 75 | return validNumberInput(this.inputWidth, ExportView.MIN_PREVIEW_SIZE, ExportView.MAX_PREVIEW_SIZE); 76 | } 77 | 78 | private getWidth(): number { 79 | return Math.min(parseInt(this.inputWidth.value), ExportView.MAX_PREVIEW_SIZE); 80 | } 81 | 82 | private validHeight(): boolean { 83 | return validNumberInput(this.inputHeight, ExportView.MIN_PREVIEW_SIZE, ExportView.MAX_PREVIEW_SIZE); 84 | } 85 | 86 | private getHeight(): number { 87 | return Math.min(parseInt(this.inputHeight.value), ExportView.MAX_PREVIEW_SIZE) 88 | } 89 | 90 | private validZoom(): boolean { 91 | return validNumberInput(this.inputZoom, ExportView.MIN_PREVIEW_ZOOM, ExportView.MAX_PREVIEW_ZOOM); 92 | } 93 | 94 | private getZoom(): number { 95 | return Math.max(parseInt(this.inputZoom.value) / 100, ExportView.MIN_PREVIEW_ZOOM); 96 | } 97 | 98 | private getRenderer(): HttpgdRendererResponse { 99 | return this.viewer.httpgd.getRenderers().find((r) => r.id == this.selectFormat.value); 100 | } 101 | 102 | private clickDownload() { 103 | const plotId = this.viewer.plotView.getCurrentPlotId(); 104 | if (!plotId) return; 105 | const renderer = this.getRenderer(); 106 | const url = this.viewer.httpgd.getPlotURL({ 107 | width: this.getWidth(), 108 | height: this.getHeight(), 109 | zoom: this.getZoom(), 110 | id: plotId, 111 | renderer: renderer.id, 112 | download: "plot_" + plotId + renderer.ext, 113 | }); 114 | downloadURL(url); 115 | } 116 | 117 | private clickOpen() { 118 | const plotId = this.viewer.plotView.getCurrentPlotId(); 119 | if (!plotId) return; 120 | const renderer = this.getRenderer(); 121 | const url = this.viewer.httpgd.getPlotURL({ 122 | width: this.getWidth(), 123 | height: this.getHeight(), 124 | zoom: this.getZoom(), 125 | id: plotId, 126 | renderer: renderer.id, 127 | }); 128 | downloadURL(url, null, true); 129 | } 130 | 131 | private update() { 132 | if (setCssClass(this.inputWidth, !this.validWidth(), "invalid-input") || 133 | setCssClass(this.inputHeight, !this.validHeight(), "invalid-input") || 134 | setCssClass(this.inputZoom, !this.validZoom(), "invalid-input")) 135 | return; 136 | 137 | const plotId = this.viewer.plotView.getCurrentPlotId(); 138 | if (!plotId) { 139 | this.imgPreview.src = ASSET_PLOT_NONE; 140 | return; 141 | } 142 | const url = this.viewer.httpgd.getPlotURL({ 143 | width: this.getWidth(), 144 | height: this.getHeight(), 145 | zoom: this.getZoom(), 146 | id: plotId, 147 | }); 148 | this.imgPreview.src = url; 149 | } 150 | 151 | public show(): void { 152 | this.elemModal.style.display = "block"; 153 | this.update(); 154 | } 155 | } -------------------------------------------------------------------------------- /src/lib/crow/TinySHA1.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * TinySHA1 - a header only implementation of the SHA1 algorithm in C++. Based 4 | * on the implementation in boost::uuid::details. 5 | * 6 | * SHA1 Wikipedia Page: http://en.wikipedia.org/wiki/SHA-1 7 | * 8 | * Copyright (c) 2012-22 SAURAV MOHAPATRA 9 | * 10 | * Permission to use, copy, modify, and distribute this software for any 11 | * purpose with or without fee is hereby granted, provided that the above 12 | * copyright notice and this permission notice appear in all copies. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 15 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 17 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 20 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | */ 22 | #ifndef _TINY_SHA1_HPP_ 23 | #define _TINY_SHA1_HPP_ 24 | #include 25 | #include 26 | #include 27 | #include 28 | namespace sha1 29 | { 30 | class SHA1 31 | { 32 | public: 33 | typedef uint32_t digest32_t[5]; 34 | typedef uint8_t digest8_t[20]; 35 | inline static uint32_t LeftRotate(uint32_t value, size_t count) { 36 | return (value << count) ^ (value >> (32-count)); 37 | } 38 | SHA1(){ reset(); } 39 | virtual ~SHA1() {} 40 | SHA1(const SHA1& s) { *this = s; } 41 | const SHA1& operator = (const SHA1& s) { 42 | memcpy(m_digest, s.m_digest, 5 * sizeof(uint32_t)); 43 | memcpy(m_block, s.m_block, 64); 44 | m_blockByteIndex = s.m_blockByteIndex; 45 | m_byteCount = s.m_byteCount; 46 | return *this; 47 | } 48 | SHA1& reset() { 49 | m_digest[0] = 0x67452301; 50 | m_digest[1] = 0xEFCDAB89; 51 | m_digest[2] = 0x98BADCFE; 52 | m_digest[3] = 0x10325476; 53 | m_digest[4] = 0xC3D2E1F0; 54 | m_blockByteIndex = 0; 55 | m_byteCount = 0; 56 | return *this; 57 | } 58 | SHA1& processByte(uint8_t octet) { 59 | this->m_block[this->m_blockByteIndex++] = octet; 60 | ++this->m_byteCount; 61 | if(m_blockByteIndex == 64) { 62 | this->m_blockByteIndex = 0; 63 | processBlock(); 64 | } 65 | return *this; 66 | } 67 | SHA1& processBlock(const void* const start, const void* const end) { 68 | const uint8_t* begin = static_cast(start); 69 | const uint8_t* finish = static_cast(end); 70 | while(begin != finish) { 71 | processByte(*begin); 72 | begin++; 73 | } 74 | return *this; 75 | } 76 | SHA1& processBytes(const void* const data, size_t len) { 77 | const uint8_t* block = static_cast(data); 78 | processBlock(block, block + len); 79 | return *this; 80 | } 81 | const uint32_t* getDigest(digest32_t digest) { 82 | size_t bitCount = this->m_byteCount * 8; 83 | processByte(0x80); 84 | if (this->m_blockByteIndex > 56) { 85 | while (m_blockByteIndex != 0) { 86 | processByte(0); 87 | } 88 | while (m_blockByteIndex < 56) { 89 | processByte(0); 90 | } 91 | } else { 92 | while (m_blockByteIndex < 56) { 93 | processByte(0); 94 | } 95 | } 96 | processByte(0); 97 | processByte(0); 98 | processByte(0); 99 | processByte(0); 100 | processByte( static_cast((bitCount>>24) & 0xFF)); 101 | processByte( static_cast((bitCount>>16) & 0xFF)); 102 | processByte( static_cast((bitCount>>8 ) & 0xFF)); 103 | processByte( static_cast((bitCount) & 0xFF)); 104 | 105 | memcpy(digest, m_digest, 5 * sizeof(uint32_t)); 106 | return digest; 107 | } 108 | const uint8_t* getDigestBytes(digest8_t digest) { 109 | digest32_t d32; 110 | getDigest(d32); 111 | size_t di = 0; 112 | digest[di++] = ((d32[0] >> 24) & 0xFF); 113 | digest[di++] = ((d32[0] >> 16) & 0xFF); 114 | digest[di++] = ((d32[0] >> 8) & 0xFF); 115 | digest[di++] = ((d32[0]) & 0xFF); 116 | 117 | digest[di++] = ((d32[1] >> 24) & 0xFF); 118 | digest[di++] = ((d32[1] >> 16) & 0xFF); 119 | digest[di++] = ((d32[1] >> 8) & 0xFF); 120 | digest[di++] = ((d32[1]) & 0xFF); 121 | 122 | digest[di++] = ((d32[2] >> 24) & 0xFF); 123 | digest[di++] = ((d32[2] >> 16) & 0xFF); 124 | digest[di++] = ((d32[2] >> 8) & 0xFF); 125 | digest[di++] = ((d32[2]) & 0xFF); 126 | 127 | digest[di++] = ((d32[3] >> 24) & 0xFF); 128 | digest[di++] = ((d32[3] >> 16) & 0xFF); 129 | digest[di++] = ((d32[3] >> 8) & 0xFF); 130 | digest[di++] = ((d32[3]) & 0xFF); 131 | 132 | digest[di++] = ((d32[4] >> 24) & 0xFF); 133 | digest[di++] = ((d32[4] >> 16) & 0xFF); 134 | digest[di++] = ((d32[4] >> 8) & 0xFF); 135 | digest[di++] = ((d32[4]) & 0xFF); 136 | return digest; 137 | } 138 | 139 | protected: 140 | void processBlock() { 141 | uint32_t w[80]; 142 | for (size_t i = 0; i < 16; i++) { 143 | w[i] = (m_block[i*4 + 0] << 24); 144 | w[i] |= (m_block[i*4 + 1] << 16); 145 | w[i] |= (m_block[i*4 + 2] << 8); 146 | w[i] |= (m_block[i*4 + 3]); 147 | } 148 | for (size_t i = 16; i < 80; i++) { 149 | w[i] = LeftRotate((w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]), 1); 150 | } 151 | 152 | uint32_t a = m_digest[0]; 153 | uint32_t b = m_digest[1]; 154 | uint32_t c = m_digest[2]; 155 | uint32_t d = m_digest[3]; 156 | uint32_t e = m_digest[4]; 157 | 158 | for (std::size_t i=0; i<80; ++i) { 159 | uint32_t f = 0; 160 | uint32_t k = 0; 161 | 162 | if (i<20) { 163 | f = (b & c) | (~b & d); 164 | k = 0x5A827999; 165 | } else if (i<40) { 166 | f = b ^ c ^ d; 167 | k = 0x6ED9EBA1; 168 | } else if (i<60) { 169 | f = (b & c) | (b & d) | (c & d); 170 | k = 0x8F1BBCDC; 171 | } else { 172 | f = b ^ c ^ d; 173 | k = 0xCA62C1D6; 174 | } 175 | uint32_t temp = LeftRotate(a, 5) + f + e + k + w[i]; 176 | e = d; 177 | d = c; 178 | c = LeftRotate(b, 30); 179 | b = a; 180 | a = temp; 181 | } 182 | 183 | m_digest[0] += a; 184 | m_digest[1] += b; 185 | m_digest[2] += c; 186 | m_digest[3] += d; 187 | m_digest[4] += e; 188 | } 189 | private: 190 | digest32_t m_digest; 191 | uint8_t m_block[64]; 192 | size_t m_blockByteIndex; 193 | size_t m_byteCount; 194 | }; 195 | } 196 | #endif 197 | -------------------------------------------------------------------------------- /src/lib/crow/middlewares/cors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "crow/http_request.h" 3 | #include "crow/http_response.h" 4 | #include "crow/routing.h" 5 | 6 | namespace crow 7 | { 8 | struct CORSHandler; 9 | 10 | /// Used for tuning CORS policies 11 | struct CORSRules 12 | { 13 | friend struct crow::CORSHandler; 14 | 15 | /// Set Access-Control-Allow-Origin. Default is "*" 16 | CORSRules& origin(const std::string& origin) 17 | { 18 | origin_ = origin; 19 | return *this; 20 | } 21 | 22 | /// Set Access-Control-Allow-Methods. Default is "*" 23 | CORSRules& methods(crow::HTTPMethod method) 24 | { 25 | add_list_item(methods_, crow::method_name(method)); 26 | return *this; 27 | } 28 | 29 | /// Set Access-Control-Allow-Methods. Default is "*" 30 | template 31 | CORSRules& methods(crow::HTTPMethod method, Methods... method_list) 32 | { 33 | add_list_item(methods_, crow::method_name(method)); 34 | methods(method_list...); 35 | return *this; 36 | } 37 | 38 | /// Set Access-Control-Allow-Headers. Default is "*" 39 | CORSRules& headers(const std::string& header) 40 | { 41 | add_list_item(headers_, header); 42 | return *this; 43 | } 44 | 45 | /// Set Access-Control-Allow-Headers. Default is "*" 46 | template 47 | CORSRules& headers(const std::string& header, Headers... header_list) 48 | { 49 | add_list_item(headers_, header); 50 | headers(header_list...); 51 | return *this; 52 | } 53 | 54 | /// Set Access-Control-Max-Age. Default is none 55 | CORSRules& max_age(int max_age) 56 | { 57 | max_age_ = std::to_string(max_age); 58 | return *this; 59 | } 60 | 61 | /// Enable Access-Control-Allow-Credentials 62 | CORSRules& allow_credentials() 63 | { 64 | allow_credentials_ = true; 65 | return *this; 66 | } 67 | 68 | /// Ignore CORS and don't send any headers 69 | void ignore() 70 | { 71 | ignore_ = true; 72 | } 73 | 74 | /// Handle CORS on specific prefix path 75 | CORSRules& prefix(const std::string& prefix); 76 | 77 | /// Handle CORS for specific blueprint 78 | CORSRules& blueprint(const Blueprint& bp); 79 | 80 | /// Global CORS policy 81 | CORSRules& global(); 82 | 83 | private: 84 | CORSRules() = delete; 85 | CORSRules(CORSHandler* handler): 86 | handler_(handler) {} 87 | 88 | /// build comma separated list 89 | void add_list_item(std::string& list, const std::string& val) 90 | { 91 | if (list == "*") list = ""; 92 | if (list.size() > 0) list += ", "; 93 | list += val; 94 | } 95 | 96 | /// Set header `key` to `value` if it is not set 97 | void set_header_no_override(const std::string& key, const std::string& value, crow::response& res) 98 | { 99 | if (value.size() == 0) return; 100 | if (!get_header_value(res.headers, key).empty()) return; 101 | res.add_header(key, value); 102 | } 103 | 104 | /// Set response headers 105 | void apply(crow::response& res) 106 | { 107 | if (ignore_) return; 108 | set_header_no_override("Access-Control-Allow-Origin", origin_, res); 109 | set_header_no_override("Access-Control-Allow-Methods", methods_, res); 110 | set_header_no_override("Access-Control-Allow-Headers", headers_, res); 111 | set_header_no_override("Access-Control-Max-Age", max_age_, res); 112 | if (allow_credentials_) set_header_no_override("Access-Control-Allow-Credentials", "true", res); 113 | } 114 | 115 | bool ignore_ = false; 116 | // TODO: support multiple origins that are dynamically selected 117 | std::string origin_ = "*"; 118 | std::string methods_ = "*"; 119 | std::string headers_ = "*"; 120 | std::string max_age_; 121 | bool allow_credentials_ = false; 122 | 123 | CORSHandler* handler_; 124 | }; 125 | 126 | /// CORSHandler is a global middleware for setting CORS headers. 127 | 128 | /// 129 | /// By default, it sets Access-Control-Allow-Origin/Methods/Headers to "*". 130 | /// The default behaviour can be changed with the `global()` cors rule. 131 | /// Additional rules for prexies can be added with `prefix()`. 132 | struct CORSHandler 133 | { 134 | struct context 135 | {}; 136 | 137 | void before_handle(crow::request& /*req*/, crow::response& /*res*/, context& /*ctx*/) 138 | {} 139 | 140 | void after_handle(crow::request& req, crow::response& res, context& /*ctx*/) 141 | { 142 | auto& rule = find_rule(req.url); 143 | rule.apply(res); 144 | } 145 | 146 | /// Handle CORS on a specific prefix path 147 | CORSRules& prefix(const std::string& prefix) 148 | { 149 | rules.emplace_back(prefix, CORSRules(this)); 150 | return rules.back().second; 151 | } 152 | 153 | /// Handle CORS for a specific blueprint 154 | CORSRules& blueprint(const Blueprint& bp) 155 | { 156 | rules.emplace_back(bp.prefix(), CORSRules(this)); 157 | return rules.back().second; 158 | } 159 | 160 | /// Get the global CORS policy 161 | CORSRules& global() 162 | { 163 | return default_; 164 | } 165 | 166 | private: 167 | CORSRules& find_rule(const std::string& path) 168 | { 169 | // TODO: use a trie in case of many rules 170 | for (auto& rule : rules) 171 | { 172 | // Check if path starts with a rules prefix 173 | if (path.rfind(rule.first, 0) == 0) 174 | { 175 | return rule.second; 176 | } 177 | } 178 | return default_; 179 | } 180 | 181 | std::vector> rules; 182 | CORSRules default_ = CORSRules(this); 183 | }; 184 | 185 | inline CORSRules& CORSRules::prefix(const std::string& prefix) 186 | { 187 | return handler_->prefix(prefix); 188 | } 189 | 190 | inline CORSRules& CORSRules::blueprint(const Blueprint& bp) 191 | { 192 | return handler_->blueprint(bp); 193 | } 194 | 195 | inline CORSRules& CORSRules::global() 196 | { 197 | return handler_->global(); 198 | } 199 | 200 | } // namespace crow 201 | -------------------------------------------------------------------------------- /src/lib/crow/parser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "crow/http_request.h" 8 | #include "crow/http_parser_merged.h" 9 | 10 | namespace crow 11 | { 12 | /// A wrapper for `nodejs/http-parser`. 13 | 14 | /// 15 | /// Used to generate a \ref crow.request from the TCP socket buffer. 16 | template 17 | struct HTTPParser : public http_parser 18 | { 19 | static int on_message_begin(http_parser*) 20 | { 21 | return 0; 22 | } 23 | static int on_method(http_parser* self_) 24 | { 25 | HTTPParser* self = static_cast(self_); 26 | self->req.method = static_cast(self->method); 27 | 28 | return 0; 29 | } 30 | static int on_url(http_parser* self_, const char* at, size_t length) 31 | { 32 | HTTPParser* self = static_cast(self_); 33 | self->req.raw_url.insert(self->req.raw_url.end(), at, at + length); 34 | self->req.url_params = query_string(self->req.raw_url); 35 | self->req.url = self->req.raw_url.substr(0, self->qs_point != 0 ? self->qs_point : std::string::npos); 36 | 37 | self->process_url(); 38 | 39 | return 0; 40 | } 41 | static int on_header_field(http_parser* self_, const char* at, size_t length) 42 | { 43 | HTTPParser* self = static_cast(self_); 44 | switch (self->header_building_state) 45 | { 46 | case 0: 47 | if (!self->header_value.empty()) 48 | { 49 | self->req.headers.emplace(std::move(self->header_field), std::move(self->header_value)); 50 | } 51 | self->header_field.assign(at, at + length); 52 | self->header_building_state = 1; 53 | break; 54 | case 1: 55 | self->header_field.insert(self->header_field.end(), at, at + length); 56 | break; 57 | } 58 | return 0; 59 | } 60 | static int on_header_value(http_parser* self_, const char* at, size_t length) 61 | { 62 | HTTPParser* self = static_cast(self_); 63 | switch (self->header_building_state) 64 | { 65 | case 0: 66 | self->header_value.insert(self->header_value.end(), at, at + length); 67 | break; 68 | case 1: 69 | self->header_building_state = 0; 70 | self->header_value.assign(at, at + length); 71 | break; 72 | } 73 | return 0; 74 | } 75 | static int on_headers_complete(http_parser* self_) 76 | { 77 | HTTPParser* self = static_cast(self_); 78 | if (!self->header_field.empty()) 79 | { 80 | self->req.headers.emplace(std::move(self->header_field), std::move(self->header_value)); 81 | } 82 | 83 | self->set_connection_parameters(); 84 | 85 | self->process_header(); 86 | return 0; 87 | } 88 | static int on_body(http_parser* self_, const char* at, size_t length) 89 | { 90 | HTTPParser* self = static_cast(self_); 91 | self->req.body.insert(self->req.body.end(), at, at + length); 92 | return 0; 93 | } 94 | static int on_message_complete(http_parser* self_) 95 | { 96 | HTTPParser* self = static_cast(self_); 97 | 98 | self->message_complete = true; 99 | self->process_message(); 100 | return 0; 101 | } 102 | HTTPParser(Handler* handler): 103 | handler_(handler) 104 | { 105 | http_parser_init(this); 106 | } 107 | 108 | // return false on error 109 | /// Parse a buffer into the different sections of an HTTP request. 110 | bool feed(const char* buffer, int length) 111 | { 112 | if (message_complete) 113 | return true; 114 | 115 | const static http_parser_settings settings_{ 116 | on_message_begin, 117 | on_method, 118 | on_url, 119 | on_header_field, 120 | on_header_value, 121 | on_headers_complete, 122 | on_body, 123 | on_message_complete, 124 | }; 125 | 126 | int nparsed = http_parser_execute(this, &settings_, buffer, length); 127 | if (http_errno != CHPE_OK) 128 | { 129 | return false; 130 | } 131 | return nparsed == length; 132 | } 133 | 134 | bool done() 135 | { 136 | return feed(nullptr, 0); 137 | } 138 | 139 | void clear() 140 | { 141 | req = crow::request(); 142 | header_field.clear(); 143 | header_value.clear(); 144 | header_building_state = 0; 145 | qs_point = 0; 146 | message_complete = false; 147 | state = CROW_NEW_MESSAGE(); 148 | } 149 | 150 | inline void process_url() 151 | { 152 | handler_->handle_url(); 153 | } 154 | 155 | inline void process_header() 156 | { 157 | handler_->handle_header(); 158 | } 159 | 160 | inline void process_message() 161 | { 162 | handler_->handle(); 163 | } 164 | 165 | inline void set_connection_parameters() 166 | { 167 | req.http_ver_major = http_major; 168 | req.http_ver_minor = http_minor; 169 | 170 | //NOTE(EDev): it seems that the problem is with crow's policy on closing the connection for HTTP_VERSION < 1.0, the behaviour for that in crow is "don't close the connection, but don't send a keep-alive either" 171 | 172 | // HTTP1.1 = always send keep_alive, HTTP1.0 = only send if header exists, HTTP?.? = never send 173 | req.keep_alive = (http_major == 1 && http_minor == 0) ? 174 | ((flags & F_CONNECTION_KEEP_ALIVE) ? true : false) : 175 | ((http_major == 1 && http_minor == 1) ? true : false); 176 | 177 | // HTTP1.1 = only close if close header exists, HTTP1.0 = always close unless keep_alive header exists, HTTP?.?= never close 178 | req.close_connection = (http_major == 1 && http_minor == 0) ? 179 | ((flags & F_CONNECTION_KEEP_ALIVE) ? false : true) : 180 | ((http_major == 1 && http_minor == 1) ? ((flags & F_CONNECTION_CLOSE) ? true : false) : false); 181 | req.upgrade = static_cast(upgrade); 182 | } 183 | 184 | /// The final request that this parser outputs. 185 | /// 186 | /// Data parsed is put directly into this object as soon as the related callback returns. (e.g. the request will have the cooorect method as soon as on_method() returns) 187 | request req; 188 | 189 | private: 190 | int header_building_state = 0; 191 | bool message_complete = false; 192 | std::string header_field; 193 | std::string header_value; 194 | 195 | Handler* handler_; ///< This is currently an HTTP connection object (\ref crow.Connection). 196 | }; 197 | } // namespace crow 198 | 199 | #undef CROW_NEW_MESSAGE 200 | #undef CROW_start_state 201 | -------------------------------------------------------------------------------- /vignettes/c01_httpgd-api.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "httpgd API" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{httpgd API} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | [httpgd](https://github.com/nx10/httpgd/blob/master/README.md) can be accessed both from R and from HTTP/WebSockets. 11 | 12 | ## Overview 13 | 14 | | R | HTTP | Description | 15 | | ----------------------------------- | ------------------------------ | ----------------------------------- | 16 | | `hgd()` | | Initialize device and start server. | 17 | | `hgd_close()` | | Helper: Close device. | 18 | | `hgd_url()` | | Helper: URL generation. | 19 | | `hgd_browse()` | | Helper: Open browser. | 20 | | [`ugd_state()`](#get-state) | [`/state`](#get-state) | Get current server state. | 21 | | [`ugd_renderers()`](#get-renderers) | [`/renderers`](#get-renderers) | Get list of available renderers. | 22 | | [`ugd_render()`](#render-plot) | [`/plot`](#render-plot) | Get rendered plot (any format). | 23 | | [`ugd_clear()`](#remove-plots) | [`/clear`](#remove-plots) | Remove all plots. | 24 | | [`ugd_remove()`](#remove-plots) | [`/remove`](#remove-plots) | Remove a single plot. | 25 | | [`ugd_id()`](#get-static-ids) | [`/plots`](#get-static-ids) | Get static plot IDs. | 26 | | | `/live` | Live server page. | 27 | 28 | ## Get state 29 | 30 | While all the APIs can be accessed stateless, the graphics device does have a state defined by. 31 | 32 | | Field | Type | Description | 33 | | -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 34 | | `upid` | `int` | Update id. Changes when plots are removed or when something is drawn. | 35 | | `hsize` | `int` | Number of plots in the history. | 36 | | `active` | `bool` | Whether the graphics device is active. When another graphics device is activated, the device will become inactive and not be able to render any plots that are not cached (no resizes). | 37 | 38 | To receive state changes as they happen [WebSockets can be used](#from-websockets). Alternatively `/state` may be polled repeatedly. 39 | 40 | ### From R 41 | 42 | ```R 43 | unigd::ugd_state() 44 | ``` 45 | Note: Prior to `httpgd 2.0` this function also returned `host`, `port` and security `token` of the server. These fields are now accessed via `hgd_details()`. 46 | 47 | ### From HTTP 48 | 49 | ``` 50 | /state 51 | ``` 52 | 53 | | Key | Value | Default | 54 | | ------- | ---------------------------- | ------------------------------------------------------- | 55 | | `token` | [Security token](#security). | (The `X-HTTPGD-TOKEN` header can be set alternatively.) | 56 | 57 | Will respond with a JSON object. 58 | 59 | ### From WebSockets 60 | 61 | httpgd accepts WebSocket connections on the same port as the HTTP server. [Server state](#Server-state) changes will be broadcasted immediately to all connected clients in JSON format. 62 | 63 | ## Get Renderers 64 | 65 | httpgd includes multiple renderers that can dynamically render plots to different target formats. As new formats may be added as the development on httpgd continues, and some depend on optional system dependencies, a list of available renderers can be obtained during runtime. 66 | 67 | The following is a complete list of renderers. 68 | 69 | ```{r echo=FALSE} 70 | df <- unigd::ugd_renderers() 71 | df <- df[order(df$id),] 72 | knitr::kable(data.frame( 73 | sprintf("`%s`", df$id), 74 | sprintf("`%s`", df$mime), 75 | df$descr, 76 | ifelse(df$text, "Text", "Binary") 77 | ), col.names = c("ID", "Mime-Type", "Renderer", "Format")) 78 | ``` 79 | 80 | 81 | ### From R 82 | 83 | ```R 84 | unigd::ugd_renderers() 85 | ``` 86 | 87 | Returns a data frame. 88 | 89 | ### From HTTP 90 | 91 | ``` 92 | /renderers 93 | ``` 94 | 95 | | Key | Value | Default | 96 | | ------- | ---------------------------- | ------------------------------------------------------- | 97 | | `token` | [Security token](#security). | (The `X-HTTPGD-TOKEN` header can be set alternatively.) | 98 | 99 | ## Render plot 100 | 101 | Plots can be rendered in various file formats from both R and HTTP. The actual plot construction in R is relatively slow so httpgd caches the plot in the last requested size. Subsequent calls with the same width and height or without a size specified will always be fast. (This way "flipping" through plot pages is very fast.) 102 | 103 | ### From R 104 | 105 | Example: 106 | ```R 107 | unigd::ugd_render(page = 3, width = 800, height = 600) # Get plot at index 3 with 800*600 108 | unigd::ugd_render() # Get last plot with cached size 109 | ``` 110 | 111 | `page` can either be a number to indicate a plot index or a static plot ID (see: hgd_id()). 112 | 113 | This function returns the plot as a string. The `file` attribute can be used to save the SVG directly to disk. 114 | 115 | ### From HTTP 116 | 117 | Example: 118 | ``` 119 | /plot?index=2&width=800&height=600 120 | ``` 121 | 122 | Parameters: 123 | 124 | | Key | Value | Default | 125 | | ---------- | ---------------------------- | ------------------------------------------------------- | 126 | | `width` | With in pixels. | Last rendered width. (Initially device width.) | 127 | | `height` | Height in pixels. | Last rendered height. (Initially device height.) | 128 | | `zoom` | Zoom level. | `1` (No zoom). `0.5` would be 50% and `2` 200%. | 129 | | `index` | Plot history index. | Newest plot. | 130 | | `id` | Static plot ID. | `index` will be used. | 131 | | `renderer` | Renderer. | `svg`. | 132 | | `token` | [Security token](#security). | (The `X-HTTPGD-TOKEN` header can be set alternatively.) | 133 | 134 | > Note that the HTTP API uses 0-based indexing and the R API 1-based indexing. This is done to conform to R and JavaScript on both ends. (This means the the first plot is accessed with `/plot?index=0` and `unigd::ugd_render(page = 1)`.) 135 | 136 | 137 | 138 | ## Remove plots 139 | 140 | ### From R 141 | 142 | Examples: 143 | ```R 144 | unigd::ugd_remove(page = 2) # Remove the second page 145 | unigd::ugd_clear() # Clear all pages 146 | ``` 147 | 148 | ### From HTTP 149 | 150 | Examples: 151 | ``` 152 | /remove?index=2 153 | /clear 154 | ``` 155 | 156 | | Key | Value | Default | 157 | | ------- | ---------------------------- | ------------------------------------------------------- | 158 | | `index` | Plot history index. | Newest plot. | 159 | | `id` | Static plot ID. | `index` will be used. | 160 | | `token` | [Security token](#security). | (The `X-HTTPGD-TOKEN` header can be set alternatively.) | 161 | 162 | 163 | 164 | 165 | ## Get static IDs 166 | 167 | The problem with requesting individual plots by index is, that a plots index will change when earlier plots are removed from the plot history. 168 | To circumvent this, each plot also is assigned a static ID. 169 | 170 | All APIs that access individual plots can also be called with static IDs instead of indices. 171 | 172 | ### From R 173 | 174 | Examples: 175 | ```R 176 | unigd::ugd_id(index = 2) # Static ID of the second plot 177 | unigd::ugd_id() # Static ID of the last plot 178 | ``` 179 | 180 | Note: The `limit` parameter can be adjusted to obtain multiple or all plot IDs. 181 | 182 | ### From HTTP 183 | 184 | Examples: 185 | ``` 186 | /plots?index=2 187 | /plots 188 | ``` 189 | 190 | | Key | Value | Default | 191 | | ------- | ------------------------------ | ------------------------------------------------------- | 192 | | `index` | Plot history index. | Newest plot. | 193 | | `limit` | Number of subsequent plot IDs. | 1 | 194 | | `token` | [Security token](#security). | (The `X-HTTPGD-TOKEN` header can be set alternatively.) | 195 | 196 | 197 | Notes: 198 | 199 | - The `limit` parameter can be specified to support pagination. 200 | - The JSON response will contain the [state](#get-state) to allow checking for desynchronisation. 201 | 202 | ## Security 203 | 204 | A security token can be set when starting the device: 205 | 206 | ```R 207 | hgd(..., token = "secret") 208 | ``` 209 | 210 | When set, each API request has to include this token inside the header `X-HTTPGD-TOKEN` or as a query param `?token=secret`. 211 | `token` is by default set to `TRUE` to generate a random 8 character alphanumeric token. If it is set to a number, a random token of that length will be generated. `FALSE` deactivates the security token. 212 | 213 | CORS is off by default but can be enabled on startup: 214 | 215 | ```R 216 | hgd(..., cors = TRUE) 217 | ``` 218 | -------------------------------------------------------------------------------- /client/src/style/style.scss: -------------------------------------------------------------------------------- 1 | $highlight-color: rgb(0, 119, 204); 2 | $highlight-color-light: rgb(65, 144, 201); 3 | $warn-color: rgb(218, 69, 103); 4 | $warn-color-light: rgb(255, 170, 170); 5 | $sidebar-width: 20%; 6 | $font-family: Helvetica, Arial, sans-serif; 7 | $ttime: 0.3s; 8 | $default-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16), 0 2px 8px 0 rgba(0, 0, 0, 0.12); 9 | 10 | // layout 11 | 12 | body { 13 | margin: 0; 14 | } 15 | 16 | #container { 17 | height: 100%; 18 | display: relative; 19 | } 20 | 21 | .plotview { 22 | height: 100%; 23 | width: 100% - $sidebar-width; 24 | position: relative; 25 | transition: width $ttime; 26 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 27 | z-index: 1; 28 | } 29 | 30 | .plotview.nohist { 31 | width: 100%; 32 | transition: width $ttime; 33 | } 34 | 35 | #drawing { 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | // overlay 41 | 42 | #overlay { 43 | position: fixed; 44 | display: none; 45 | width: 100%; 46 | height: 100%; 47 | background-color: rgba(92, 92, 92, 0.685); 48 | z-index: 2; 49 | } 50 | 51 | #overlay-text { 52 | position: absolute; 53 | top: 50%; 54 | left: 50%; 55 | transform: translate(-50%, -50%); 56 | -ms-transform: translate(-50%, -50%); 57 | font-size: 40px; 58 | font-family: $font-family; 59 | color: rgb(255, 255, 255); 60 | text-shadow: 2px 2px 8px rgb(46, 46, 46); 61 | } 62 | 63 | // history 64 | 65 | .history { 66 | width: $sidebar-width; 67 | height: 100%; 68 | overflow-y: scroll; 69 | overflow-x: hidden; 70 | position: fixed; 71 | top: 0; 72 | right: 0; 73 | transition: right $ttime; 74 | background-color: rgb(252, 252, 252); 75 | } 76 | 77 | .history.nohist { 78 | right: -$sidebar-width; 79 | visibility: hidden; 80 | transition: right $ttime, visibility $ttime; 81 | } 82 | 83 | .history-item { 84 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 85 | margin: 8; 86 | cursor: pointer; 87 | position: relative; 88 | background-color: rgb(255, 255, 255); 89 | 90 | img { 91 | width: 100%; 92 | height: 12vw; 93 | } 94 | a { 95 | position: absolute; 96 | top: 0; 97 | right: 0; 98 | color: rgba(0, 0, 0, 0.5); 99 | text-decoration: none; 100 | font-weight: bold; 101 | font-size: 22px; 102 | padding: 0px 4px 0px 0px; 103 | margin: -2px 0px 0px 0px; 104 | display: none; 105 | } 106 | a:hover { 107 | color: $warn-color; 108 | } 109 | 110 | &:hover { 111 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 112 | a { 113 | display: inline; 114 | } 115 | } 116 | } 117 | 118 | .history-selected { 119 | outline: rgba(0, 119, 204, 0.3) solid 3px; 120 | } 121 | 122 | // toolbar 123 | 124 | #toolbar { 125 | float: left; 126 | position: absolute; 127 | right: 0; 128 | top: 0px; 129 | font-family: $font-family; 130 | font-size: 18px; 131 | font-weight: bold; 132 | -webkit-user-select: none; 133 | -ms-user-select: none; 134 | user-select: none; 135 | background-color: rgba(253, 253, 254, 0.8); 136 | border-color: rgba(253, 253, 254, 0.8); 137 | border-radius: 2px; 138 | } 139 | 140 | #tb-tools { 141 | white-space: nowrap; 142 | & > span { 143 | padding: 2px 2px 2px 2px; 144 | margin: 0px 2px 0px 2px; 145 | color: rgba(0, 0, 0, 0.5); 146 | cursor: pointer; 147 | 148 | & > a { 149 | margin: 0px -2px 0px -2px; 150 | 151 | &:hover { 152 | color: $highlight-color; 153 | } 154 | } 155 | } 156 | } 157 | 158 | .icon { 159 | width: 24px; 160 | height: 24px; 161 | position: relative; 162 | top: 5px; 163 | fill: rgba(0, 0, 0, 0.5); 164 | 165 | &:hover { 166 | fill: $highlight-color; 167 | } 168 | } 169 | 170 | .icon-warn:hover { 171 | fill: $warn-color; 172 | } 173 | 174 | .tooltip { 175 | position: relative; 176 | display: inline-block; 177 | 178 | .tooltiptext { 179 | visibility: hidden; 180 | font-size: 12px; 181 | font-weight: normal; 182 | width: 80px; 183 | background-color: rgb(71, 71, 71); 184 | color: #fff; 185 | text-align: center; 186 | border-radius: 6px; 187 | padding: 5px 0; 188 | position: absolute; 189 | z-index: 1; 190 | top: 150%; 191 | left: 50%; 192 | margin-left: -40px; 193 | opacity: 0; 194 | transition: opacity 0.2s; 195 | 196 | &::after { 197 | content: ""; 198 | position: absolute; 199 | bottom: 100%; 200 | left: 50%; 201 | margin-left: -5px; 202 | border-width: 5px; 203 | border-style: solid; 204 | border-color: transparent transparent rgb(71, 71, 71) transparent; 205 | } 206 | } 207 | 208 | &:hover .tooltiptext { 209 | visibility: visible; 210 | opacity: 1; 211 | } 212 | } 213 | 214 | .fade-out { 215 | opacity: 0; 216 | transform: translateY(-100px); 217 | transition: opacity 0.5s, transform 0.5s step-end 0.5s; 218 | } 219 | 220 | .drop { 221 | position: relative; 222 | display: inline-block; 223 | height: 34px; 224 | ul { 225 | position: absolute; 226 | right: -2px; 227 | top: 20px; 228 | transition: all $ttime ease; 229 | transform: scale(0); 230 | transform-origin: top right; 231 | box-shadow: $default-shadow; 232 | padding: 0px; 233 | background-color: #fff; 234 | font-size: 16px; 235 | font-weight: bold; 236 | 237 | li { 238 | display: block; 239 | width: 100%; 240 | 241 | a { 242 | width: 100%; 243 | padding: 10px 14px 10px 12px; 244 | display: flex; 245 | white-space: pre; 246 | box-sizing: border-box; 247 | fill: rgba(0, 0, 0, 0.5); 248 | 249 | svg { 250 | padding-right: 10px; 251 | margin-top: -4px; 252 | padding-bottom: 4px; 253 | } 254 | 255 | span { 256 | position: relative; 257 | top: 4px; 258 | 259 | &.drop-kbd { 260 | margin-left: auto; 261 | padding-left: 12px; 262 | color: rgba(0, 0, 0, 0.3); 263 | } 264 | } 265 | 266 | &:hover { 267 | background: #ebebeb; 268 | color: $highlight-color; 269 | svg { 270 | fill: $highlight-color; 271 | } 272 | } 273 | 274 | &.warn-hover:hover { 275 | color: $warn-color; 276 | fill: $warn-color; 277 | } 278 | } 279 | } 280 | } 281 | } 282 | 283 | .drop-open ul { 284 | transform: scale(1); 285 | } 286 | 287 | .notransition { 288 | transition: none !important; 289 | } 290 | 291 | // export dialog 292 | 293 | .modal { 294 | display: none; 295 | position: fixed; 296 | z-index: 10; 297 | padding-top: 10vh; 298 | left: 0; 299 | top: 0; 300 | width: 100%; 301 | height: 100%; 302 | overflow: auto; 303 | background-color: rgba(0, 0, 0, 0.4); 304 | } 305 | 306 | .modal-content { 307 | background-color: #fefefe; 308 | margin: auto; 309 | padding: 8 20 20 20; 310 | width: 80%; 311 | height: 70%; 312 | 313 | display: flex; 314 | flex-direction: column; 315 | } 316 | 317 | #exp-modal-close { 318 | color: #aaaaaa; 319 | float: right; 320 | font-size: 32px; 321 | font-weight: bold; 322 | 323 | display: flex; 324 | flex-direction: column; 325 | text-align: right; 326 | 327 | &:hover, 328 | &:focus { 329 | color: #000; 330 | text-decoration: none; 331 | cursor: pointer; 332 | } 333 | } 334 | 335 | .export-preview { 336 | text-align: center; 337 | 338 | display: flex; 339 | flex-direction: column; 340 | flex: 1; 341 | min-height: 0; 342 | align-items: center; 343 | justify-content: center; 344 | 345 | img { 346 | background-color: rgba(0, 0, 0, 0.2); 347 | width: auto; 348 | max-width: 100%; 349 | max-height: 100%; 350 | max-height: -webkit-fill-available; // Fixes RStudio webview. Remove when they update their web engine. 351 | box-shadow: $default-shadow; 352 | } 353 | 354 | 355 | } 356 | 357 | .export-options { 358 | font-family: $font-family; 359 | font-size: 16px; 360 | 361 | display: flex; 362 | flex-direction: row; 363 | justify-content: space-between; 364 | flex-wrap: wrap; 365 | 366 | margin-bottom: -14px; 367 | margin-top: 6px; 368 | 369 | .export-tools { 370 | span { 371 | vertical-align: middle; 372 | font-size: 24; 373 | color: #888; 374 | } 375 | 376 | .num-input { 377 | width: 80; 378 | } 379 | 380 | #ie-btn-open { 381 | margin-left: 10px; 382 | } 383 | } 384 | } 385 | 386 | .num-input, 387 | select { 388 | padding: 8px 16px; 389 | margin: 8px 0; 390 | display: inline-block; 391 | border: 1px solid #ccc; 392 | border-radius: 4px; 393 | box-sizing: border-box; 394 | } 395 | 396 | .invalid-input { 397 | background-color: $warn-color-light; 398 | } 399 | 400 | .num-input:hover, 401 | .num-input:focus { 402 | border-color: $highlight-color; 403 | outline-color: $highlight-color; 404 | } 405 | 406 | .but-input { 407 | background-color: #888; 408 | color: white; 409 | padding: 8px 16px; 410 | margin: 8px 0; 411 | border: none; 412 | border-radius: 4px; 413 | cursor: pointer; 414 | 415 | &:hover { 416 | background-color: $highlight-color; 417 | } 418 | } 419 | 420 | // loader 421 | 422 | .loader { 423 | position: absolute; 424 | left: 50%; 425 | top: 50%; 426 | z-index: 3; 427 | border: 12px solid #f3f3f3; 428 | border-color: $highlight-color-light #fbfbfb $highlight-color-light #fbfbfb; 429 | border-radius: 50%; 430 | width: 14vh; 431 | height: 14vh; 432 | animation: spin 2s linear infinite; 433 | } 434 | 435 | @keyframes spin { 436 | 0% { 437 | transform: rotate(0deg); 438 | } 439 | 440 | 441 | 100% { 442 | transform: rotate(360deg); 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /src/lib/crow/middlewares/cookie_parser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "crow/utility.h" 5 | #include "crow/http_request.h" 6 | #include "crow/http_response.h" 7 | 8 | namespace crow 9 | { 10 | // Any middleware requires following 3 members: 11 | 12 | // struct context; 13 | // storing data for the middleware; can be read from another middleware or handlers 14 | 15 | // before_handle 16 | // called before handling the request. 17 | // if res.end() is called, the operation is halted. 18 | // (still call after_handle of this middleware) 19 | // 2 signatures: 20 | // void before_handle(request& req, response& res, context& ctx) 21 | // if you only need to access this middlewares context. 22 | // template 23 | // void before_handle(request& req, response& res, context& ctx, AllContext& all_ctx) 24 | // you can access another middlewares' context by calling `all_ctx.template get()' 25 | // ctx == all_ctx.template get() 26 | 27 | // after_handle 28 | // called after handling the request. 29 | // void after_handle(request& req, response& res, context& ctx) 30 | // template 31 | // void after_handle(request& req, response& res, context& ctx, AllContext& all_ctx) 32 | 33 | struct CookieParser 34 | { 35 | // Cookie stores key, value and attributes 36 | struct Cookie 37 | { 38 | enum class SameSitePolicy 39 | { 40 | Strict, 41 | Lax, 42 | None 43 | }; 44 | 45 | template 46 | Cookie(const std::string& key, U&& value): 47 | Cookie() 48 | { 49 | key_ = key; 50 | value_ = std::forward(value); 51 | } 52 | 53 | Cookie(const std::string& key): 54 | Cookie(key, "") {} 55 | 56 | // format cookie to HTTP header format 57 | std::string dump() const 58 | { 59 | const static char* HTTP_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"; 60 | 61 | std::stringstream ss; 62 | ss << key_ << '='; 63 | ss << (value_.empty() ? "\"\"" : value_); 64 | dumpString(ss, !domain_.empty(), "Domain=", domain_); 65 | dumpString(ss, !path_.empty(), "Path=", path_); 66 | dumpString(ss, secure_, "Secure"); 67 | dumpString(ss, httponly_, "HttpOnly"); 68 | if (expires_at_) 69 | { 70 | ss << DIVIDER << "Expires=" 71 | << std::put_time(expires_at_.get(), HTTP_DATE_FORMAT); 72 | } 73 | if (max_age_) 74 | { 75 | ss << DIVIDER << "Max-Age=" << *max_age_; 76 | } 77 | if (same_site_) 78 | { 79 | ss << DIVIDER << "SameSite="; 80 | switch (*same_site_) 81 | { 82 | case SameSitePolicy::Strict: 83 | ss << "Strict"; 84 | break; 85 | case SameSitePolicy::Lax: 86 | ss << "Lax"; 87 | break; 88 | case SameSitePolicy::None: 89 | ss << "None"; 90 | break; 91 | } 92 | } 93 | return ss.str(); 94 | } 95 | 96 | const std::string& name() 97 | { 98 | return key_; 99 | } 100 | 101 | template 102 | Cookie& value(U&& value) 103 | { 104 | value_ = std::forward(value); 105 | return *this; 106 | } 107 | 108 | // Expires attribute 109 | Cookie& expires(const std::tm& time) 110 | { 111 | expires_at_ = std::unique_ptr(new std::tm(time)); 112 | return *this; 113 | } 114 | 115 | // Max-Age attribute 116 | Cookie& max_age(long long seconds) 117 | { 118 | max_age_ = std::unique_ptr(new long long(seconds)); 119 | return *this; 120 | } 121 | 122 | // Domain attribute 123 | Cookie& domain(const std::string& name) 124 | { 125 | domain_ = name; 126 | return *this; 127 | } 128 | 129 | // Path attribute 130 | Cookie& path(const std::string& path) 131 | { 132 | path_ = path; 133 | return *this; 134 | } 135 | 136 | // Secured attribute 137 | Cookie& secure() 138 | { 139 | secure_ = true; 140 | return *this; 141 | } 142 | 143 | // HttpOnly attribute 144 | Cookie& httponly() 145 | { 146 | httponly_ = true; 147 | return *this; 148 | } 149 | 150 | // SameSite attribute 151 | Cookie& same_site(SameSitePolicy ssp) 152 | { 153 | same_site_ = std::unique_ptr(new SameSitePolicy(ssp)); 154 | return *this; 155 | } 156 | 157 | Cookie(const Cookie& c): 158 | key_(c.key_), 159 | value_(c.value_), 160 | domain_(c.domain_), 161 | path_(c.path_), 162 | secure_(c.secure_), 163 | httponly_(c.httponly_) 164 | { 165 | if (c.max_age_) 166 | max_age_ = std::unique_ptr(new long long(*c.max_age_)); 167 | 168 | if (c.expires_at_) 169 | expires_at_ = std::unique_ptr(new std::tm(*c.expires_at_)); 170 | 171 | if (c.same_site_) 172 | same_site_ = std::unique_ptr(new SameSitePolicy(*c.same_site_)); 173 | } 174 | 175 | private: 176 | Cookie() = default; 177 | 178 | static void dumpString(std::stringstream& ss, bool cond, const char* prefix, 179 | const std::string& value = "") 180 | { 181 | if (cond) 182 | { 183 | ss << DIVIDER << prefix << value; 184 | } 185 | } 186 | 187 | private: 188 | std::string key_; 189 | std::string value_; 190 | std::unique_ptr max_age_{}; 191 | std::string domain_ = ""; 192 | std::string path_ = ""; 193 | bool secure_ = false; 194 | bool httponly_ = false; 195 | std::unique_ptr expires_at_{}; 196 | std::unique_ptr same_site_{}; 197 | 198 | static constexpr const char* DIVIDER = "; "; 199 | }; 200 | 201 | 202 | struct context 203 | { 204 | std::unordered_map jar; 205 | 206 | std::string get_cookie(const std::string& key) const 207 | { 208 | auto cookie = jar.find(key); 209 | if (cookie != jar.end()) 210 | return cookie->second; 211 | return {}; 212 | } 213 | 214 | template 215 | Cookie& set_cookie(const std::string& key, U&& value) 216 | { 217 | cookies_to_add.emplace_back(key, std::forward(value)); 218 | return cookies_to_add.back(); 219 | } 220 | 221 | Cookie& set_cookie(Cookie cookie) 222 | { 223 | cookies_to_add.push_back(std::move(cookie)); 224 | return cookies_to_add.back(); 225 | } 226 | 227 | private: 228 | friend struct CookieParser; 229 | std::vector cookies_to_add; 230 | }; 231 | 232 | void before_handle(request& req, response& res, context& ctx) 233 | { 234 | // TODO(dranikpg): remove copies, use string_view with c++17 235 | int count = req.headers.count("Cookie"); 236 | if (!count) 237 | return; 238 | if (count > 1) 239 | { 240 | res.code = 400; 241 | res.end(); 242 | return; 243 | } 244 | std::string cookies = req.get_header_value("Cookie"); 245 | size_t pos = 0; 246 | while (pos < cookies.size()) 247 | { 248 | size_t pos_equal = cookies.find('=', pos); 249 | if (pos_equal == cookies.npos) 250 | break; 251 | std::string name = cookies.substr(pos, pos_equal - pos); 252 | name = utility::trim(name); 253 | pos = pos_equal + 1; 254 | if (pos == cookies.size()) 255 | break; 256 | 257 | size_t pos_semicolon = cookies.find(';', pos); 258 | std::string value = cookies.substr(pos, pos_semicolon - pos); 259 | 260 | value = utility::trim(value); 261 | if (value[0] == '"' && value[value.size() - 1] == '"') 262 | { 263 | value = value.substr(1, value.size() - 2); 264 | } 265 | 266 | ctx.jar.emplace(std::move(name), std::move(value)); 267 | 268 | pos = pos_semicolon; 269 | if (pos == cookies.npos) 270 | break; 271 | pos++; 272 | } 273 | } 274 | 275 | void after_handle(request& /*req*/, response& res, context& ctx) 276 | { 277 | for (const auto& cookie : ctx.cookies_to_add) 278 | { 279 | res.add_header("Set-Cookie", cookie.dump()); 280 | } 281 | } 282 | }; 283 | 284 | /* 285 | App app; 286 | A B C 287 | A::context 288 | int aa; 289 | 290 | ctx1 : public A::context 291 | ctx2 : public ctx1, public B::context 292 | ctx3 : public ctx2, public C::context 293 | 294 | C depends on A 295 | 296 | C::handle 297 | context.aaa 298 | 299 | App::context : private CookieParser::context, ... 300 | { 301 | jar 302 | 303 | } 304 | 305 | SimpleApp 306 | */ 307 | } // namespace crow 308 | --------------------------------------------------------------------------------