diff --git a/component/headless-chromium/buildout.cfg b/component/headless-chromium/buildout.cfg index 2001b0fff0fd1a922c32e32b8d87b3f007d79483..4bd2d6deb03dbc6786e626d363dd10159ebbc13d 100644 --- a/component/headless-chromium/buildout.cfg +++ b/component/headless-chromium/buildout.cfg @@ -1,90 +1,141 @@ [buildout] extends = - ../binutils/buildout.cfg - ../bison/buildout.cfg - ../pkgconfig/buildout.cfg - ../gperf/buildout.cfg - ../ninja/buildout.cfg - ../freetype/buildout.cfg - ../cmake/buildout.cfg - ../git/buildout.cfg - ../pkgconfig/buildout.cfg - ../cups/buildout.cfg +# Build dependencies: ../coreutils/buildout.cfg + ../curl/buildout.cfg ../depot_tools/buildout.cfg - ../findutils/buildout.cfg - ../fontconfig/buildout.cfg - ../gettext/buildout.cfg - ../glib/buildout.cfg - ../gtk-2/buildout.cfg - ../libexpat/buildout.cfg - ../libffi/buildout.cfg - ../libpng/buildout.cfg - ../libxml2/buildout.cfg - ../mesa/buildout.cfg + ../git/buildout.cfg + ../gperf/buildout.cfg + ../pkgconfig/buildout.cfg +# Runtime dependencies: ../nspr/buildout.cfg ../nss/buildout.cfg - ../pcre/buildout.cfg - ../sqlite3/buildout.cfg - ../xorg/buildout.cfg - ../zlib/buildout.cfg parts = - chromium + headless-chromium-wrapper -[gconf] -recipe = slapos.recipe.cmmi -url = http://ftp.gnome.org/pub/gnome/sources/GConf/3.2/GConf-3.2.6.tar.xz -md5sum = 2b16996d0e4b112856ee5c59130e822c -configure-options = --disable-orbit --disable-static -environment = - PATH=${pkgconfig:location}/bin:${intltool:location}/bin:${gettext:location}/bin:${glib:location}/bin:%(PATH)s - PKG_CONFIG_PATH=${glib:location}/lib/pkgconfig:${pcre:location}/lib/pkgconfig:${libxml2:location}/lib/pkgconfig:${dbus:location}/lib/pkgconfig:${dbus-glib:location}/lib/pkgconfig:$PKG_CONFIG_PATH - -[chromedriver] -recipe = hexagonit.recipe.download -url = https://chromedriver.storage.googleapis.com/2.28/chromedriver_linux64.zip -md5sum = a72088c0a6b018ded2c0fff616da8f65 - -[chromium-download] +# The Chromium project recommends using their own `fetch' tool rather +# than doing a `git clone', but this is a little more flexible and works +# fine. +# +# Setting depth=1000 is a middle ground between cloning no history +# (causes some scripts to break) and cloning the full history (takes a +# really long time). +# +# Note: we should add a `depth' option to slapos.recipe.build:gitclone +# and then migrate this part to use that recipe at some point. +[chromium-source] recipe = plone.recipe.command -# This revision is 56.0.2924.122. Because the chromedriver only support some certain -# version. Which version 56 is in the middle of 55-57. -revision = faf03429d9c3dbd483700dd42316b20776cbbd3c -path = ${buildout:parts-directory}/${:_buildout_section_name_} command = - set -e - PATH=${depot_tools:location}:${git:location}/bin:$PATH - [ -d ${:path} ] && rm -r ${:path} - mkdir -p ${:path} - cd ${:path} - # Do never use `fetch` with the `--no-history` option unless you find an - # option to fetch directly at the wanted revision. `--no-history` could - # reduce the download size significantly but even if it retrieves enough - # commits at the time you test this section, development continues upstream - # and at some point the next command (gclient) would break. - # ... - # This command only could work in an empty dir. - fetch --nohooks chromium - gclient sync --revision ${:revision} --with_branch_heads + export PATH=$PATH:${git:location}/bin + git clone ${:repository} ${:location} \ + --branch ${:version} \ + --depth 1000 +repository = https://chromium.googlesource.com/chromium/src.git stop-on-error = true -[chromium] +# We should place the .gclient file in the parent directory of the +# checkout/repository itself. +location = ${:gclient-location}/${:name} +gclient-location = ${buildout:parts-directory}/${:_buildout_section_name_} + +# Theoretically the checkout can be put anywhere as long as you specify +# "name" appropriately in the .gclient file, but in practice it seems +# that some automated tools break. It's safest to put it in a directory +# called "src". +name = src + +# There's nothing special about version 92.0.4515.107. It just happened +# to be the current Chromium stable version at the time of writing. +version = 92.0.4515.107 + + +[headless-chromium] recipe = slapos.recipe.cmmi -path = ${chromium-download:path}/src -location = ${buildout:parts-directory}/${:_buildout_section_name_} +path = ${chromium-source:location} +location = ${:path}/out/headless + +# Configuration file for GN, the tool to build the actual compilation +# configuration file. +default-gclient-config = + solutions = [ + { + "name": "${chromium-source:name}", + "url": "${chromium-source:repository}", + "managed": False, + "custom_deps": {}, + "custom_vars": {}, + }, + ] + +# Configuration for a headless build. +build-config-options = + import("//build/args/headless.gn") + is_debug = false + symbol_level = 0 + blink_symbol_level = 0 + + # We need to unbundle the build toolchain in order to set our own + # LDFLAGS. + custom_toolchain = "//build/toolchain/linux/unbundle:default" + host_toolchain = "//build/toolchain/linux/unbundle:default" + current_os = "linux" + current_cpu = "x64" + +# Chromium bundles its own LLVM toolchain, so we might as well use it. +llvm-toolchain = ${:path}/third_party/llvm-build/Release+Asserts/bin + configure-command = - gclient runhooks +# Sync build dependencies---this is a little finnicky. + echo '${:default-gclient-config}' \ + > ${chromium-source:gclient-location}/.gclient + gclient sync --no-history +# Generate build configuration files. mkdir -p ${:location} - echo 'use_udev = true - is_debug = false - enable_nacl = false' > ${:location}/args.gn + echo '${:build-config-options}' > ${:location}/args.gn gn gen ${:location} -# Note you can run Chromium manually by: ${:location}/chrome --headless --no-sandbox --disable-gpu -make-binary = ninja -C ${:location} chrome +# You can run the headless Chromium shell using +# ${:binary} --remote-debugging-port=1234 +make-binary = + autoninja -C ${:location} headless_shell +# By building our own version of Chromedriver, we can ensure version +# compatibility. The build is quite cheap compared to Chromium, anyway. + autoninja -C ${:location} chromedriver environment = - PKG_CONFIG_PATH=${freetype:location}/lib/pkgconfig:${zlib:location}/lib/pkgconfig:${libpng:location}/lib/pkgconfig:${randrproto:location}/lib/pkgconfig:$PKG_CONFIG_PATH - PATH=${chromedriver:location}:${dbus:location}/bin:${depot_tools:location}:${pkgconfig:location}/bin:${ninja:location}/bin:${bison:location}/bin:${gperf:location}/bin:${xserver:location}/bin:%(PATH)s - CPATH=${dbus:location}/include/dbus-1.0:${dbus:location}/lib/dbus-1.0/include/:${freetype:location}/include/freetype2:${libffi:location}/include:${mpfr:location}/include:${ncurses:location}/include:${openssl:location}/include:${readline:location}/include:${sqlite3:location}/include:${zlib:location}/include:${bzip2:location}/include:$CPATH - LD_LIBRARY_PATH=${alsa:location}/lib:${gconf:location}/lib:${libXScrnSaver:location}/lib:${glib:location}/lib:${atk:location}/lib:${cairo:location}/lib:${cups:location}/lib:${dbus:location}/lib:${dbus-glib:location}/lib:${fontconfig:location}/lib/:${gdk-pixbuf:location}/lib:${gettext:location}/lib:${glib:location}/lib:${gtk-2:location}/lib:${harfbuzz:location}/lib:${libX11:location}/lib:${libXau:location}/lib:${libXcomposite:location}/lib:${libXcursor:location}/lib:${libXext:location}/lib:${libXi:location}/lib:${libXrender:location}/lib/:${libXtst:location}/lib:${libexpat:location}/lib:${libffi:location}/lib:${libpng:location}/lib:${libpng12:location}/lib:${libxcb:location}/lib:${libxml2:location}/lib:${mesa:location}/lib:${nspr:location}/lib:${nss:location}/lib:${pango:location}/lib:${pcre:location}/lib:${pixman:location}/lib:${sqlite3:location}/lib:${xdamage:location}/lib:${xfixes:location}/lib:${zlib:location}/lib:$LD_LIBRARY_PATH + PATH=${depot_tools:location}:${gperf:location}/bin:${pkgconfig:location}/bin:${coreutils:location}/bin:${git:location}/bin:${curl:location}/bin:%(PATH)s + LDFLAGS="-Wl,-rpath=${nss:location}/lib,-rpath=${nspr:location}/lib" + CC="${:llvm-toolchain}/clang" + CXX="${:llvm-toolchain}/clang++" + AR="${:llvm-toolchain}/llvm-ar" + NM="${:llvm-toolchain}/llvm-nm" + +# Expose devtools frontend location. +devtools-frontend = ${:location}/gen/third_party/devtools-frontend/src/front_end + +binary = ${:location}/headless_shell +chromedriver = ${:location}/chromedriver +promises = + ${:binary} + ${:chromedriver} + + +# At runtime, Chromium tries to dynamically load the NSS certificate +# database from "libnssckbi.so". But Chromium does this through NSPR, +# which doesn't know where SlapOS installed NSS. Since we don't want to +# modify the NSPR component from the Chromium component, we just set +# LD_LIBRARY_PATH in a wrapper script so that NSPR knows where NSS is. +# +# Alternatively, we could patch crypto/nss_util.cc in the Chromium +# source code to use an absolute path for libnssckbi.so, but this is not +# as future-proof against new versions of Chromium. +[headless-chromium-wrapper] +recipe = slapos.recipe.template:jinja2 +template = + inline:#!/bin/sh + export LD_LIBRARY_PATH="{{ nss_location }}/lib:$LD_LIBRARY_PATH" + exec {{ chromium_binary }} "$@" +rendered = ${buildout:bin-directory}/headless-chromium +context = + key nss_location nss:location + key chromium_binary headless-chromium:binary diff --git a/component/nginx/buildout.cfg b/component/nginx/buildout.cfg index 845e0ead180203912b73dfadf2214fae70099b55..c3e092863b68c0aac6585cb80fe84c076bf469d6 100644 --- a/component/nginx/buildout.cfg +++ b/component/nginx/buildout.cfg @@ -24,6 +24,7 @@ configure-options= --with-http_v2_module --with-http_gzip_static_module --with-http_realip_module + --with-http_sub_module --with-mail --with-mail_ssl_module --with-ld-opt="-L ${openssl:location}/lib -L ${pcre:location}/lib -L ${zlib:location}/lib -Wl,-rpath=${openssl:location}/lib -Wl,-rpath=${pcre:location}/lib -Wl,-rpath=${zlib:location}/lib" diff --git a/software/headless-chromium/README.md b/software/headless-chromium/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8dea379c92ea114f0d9f94c6b121d589cda6fa87 --- /dev/null +++ b/software/headless-chromium/README.md @@ -0,0 +1,30 @@ +# Headless Chromium +This software release compiles and runs a headless Chromium shell and +exposes an interface to connect to it remotely from another browser. + +After deployment, the instance is configured like this: + +``` + Caddy frontend + | + (HTTPS, IPv6) + | + Nginx proxy, basic authentication + | + (HTTP, IPv4) + | + Chromium shell +``` + +The proxy is necessary because Chromium only accepts local connections +for remote debugging. + +## Parameters +The following instance parameters can be configured: + +- target-url: URL for Chromium to load on startup. +- remote-debugging-port: Port for Chromium to listen on. +- nginx-proxy-port: Port for Ningx proxy to listen on. +- monitor-httpd-port: Port for monitor. + +See `instance-headless-chromium-input-schema.json` for default values. diff --git a/software/headless-chromium/buildout.hash.cfg b/software/headless-chromium/buildout.hash.cfg new file mode 100644 index 0000000000000000000000000000000000000000..d6323ccc7a41b4fb78ff395f73646667ae3e1d48 --- /dev/null +++ b/software/headless-chromium/buildout.hash.cfg @@ -0,0 +1,15 @@ +[template-cfg] +filename = instance.cfg.in +md5sum = 5dfeeb5eca125dcaa5f9e537f941dd41 + +[instance-headless-chromium] +_update_hash_filename_ = instance-headless-chromium.cfg.in +md5sum = fad685238b26ca20537c12ce7432e7e7 + +[template-nginx-conf] +_update_hash_filename_ = templates/nginx.conf.in +md5sum = c4d09d2b819f624087ef4c38551dfe2f + +[template-mime-types] +_update_hash_filename_ = templates/mime-types.in +md5sum = 4ef94a7b458d885cd79ba0b930a5727e diff --git a/software/headless-chromium/instance-headless-chromium-input-schema.json b/software/headless-chromium/instance-headless-chromium-input-schema.json new file mode 100644 index 0000000000000000000000000000000000000000..9ee95425d43736f5723ca968062c2269bcba49b8 --- /dev/null +++ b/software/headless-chromium/instance-headless-chromium-input-schema.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "$schema": "http://json-schema.org/draft-04/schema", + "title": "Input Parameters", + "properties": { + "target-url": { + "description": "URL for Chromium to load on startup.", + "title": "Target URL", + "type": "string", + "default": "https://www.example.com" + }, + "remote-debugging-port": { + "description": "Port for Chromium to listen on.", + "title": "Remote Debugging Port", + "type": "integer", + "default": 8081 + }, + "nginx-proxy-port": { + "description": "Port for Nginx proxy to listen on.", + "title": "Nginx Proxy Port", + "type": "integer", + "default": 8082 + }, + "monitor-httpd-port": { + "description": "Port for monitor frontend.", + "title": "Monitor Httpd Port", + "type": "integer", + "default": 8083 + } + } +} diff --git a/software/headless-chromium/instance-headless-chromium-output-schema.json b/software/headless-chromium/instance-headless-chromium-output-schema.json new file mode 100644 index 0000000000000000000000000000000000000000..5595a1798521a432ff5e7a5096f4eaf7e8f4b7b4 --- /dev/null +++ b/software/headless-chromium/instance-headless-chromium-output-schema.json @@ -0,0 +1,35 @@ +{ + "type": "object", + "$schema": "http://json-schema.org/draft-04/schema", + "title": "Values returned by headless Chromium instantiation", + "properties": { + "frontend-url": { + "description": "URL to access remote debugging interface", + "type": "string" + }, + "username": { + "description": "Username for remote debugging interface", + "type": "string" + }, + "password": { + "description": "Password for remote debugging interface", + "type": "string" + }, + "monitor-base-url": { + "description": "Base URL used by monitor", + "type": "string" + }, + "monitor-setup-url": { + "description": "One-click link to setup and monitor feeds", + "type": "string" + }, + "proxy-url": { + "description": "Raw IPv6 address used by Nginx proxy", + "type": "string" + }, + "remote-debug-url": { + "description": "Local IPv4 address used by Chromium", + "type": "string" + } + } +} diff --git a/software/headless-chromium/instance-headless-chromium.cfg.in b/software/headless-chromium/instance-headless-chromium.cfg.in new file mode 100644 index 0000000000000000000000000000000000000000..36fd69d74a5d38bf8a778676346d106f464199cf --- /dev/null +++ b/software/headless-chromium/instance-headless-chromium.cfg.in @@ -0,0 +1,192 @@ +{% set parameter_dict = dict(default_parameter_dict, **slapparameter_dict) %} + +[buildout] +parts = + chromium-launcher + generate-passwd-file + nginx-config + nginx-mime-types + nginx-launcher + logrotate-entry-nginx + publish-connection-information + frontend-ok-promise + frontend-secure-promise + +eggs-directory = {{ buildout['eggs-directory'] }} +develop-eggs-directory = {{ buildout['develop-eggs-directory'] }} +offline = true + +extends = {{ parameter_list['template-monitor'] }} + +# Create necessary directories. +[directory] +recipe = slapos.cookbook:mkdirectory +home = ${buildout:directory} +tmp = ${:home}/tmp +log = ${:home}/log +etc = ${:home}/etc +ssl = ${:etc}/ssl +service = ${:etc}/service + +# Options for instance configuration. See README.md for a list of +# options that can be configured when requesting an instance. +[headless-chromium] +ipv4 = {{ partition_ipv4 }} +ipv6 = {{ partition_ipv6 }} +remote-debugging-port = {{ parameter_dict['remote-debugging-port'] }} +url = {{ parameter_dict['target-url'] }} +remote-debugging-address = ${:ipv4}:${:remote-debugging-port} +devtools-frontend-root = {{ parameter_list['devtools-frontend'] }} + +nginx-port = {{ parameter_dict['nginx-proxy-port'] }} +proxy-address = [${:ipv6}]:${:nginx-port} +nginx-config-target = ${directory:etc}/nginx.conf +nginx-pid-path = ${directory:log}/nginx.pid +nginx-temp-path = ${directory:tmp} +nginx-error-log = ${directory:log}/nginx-error.log +nginx-access-log = ${directory:log}/nginx-access.log +nginx-htpasswd-file = ${directory:etc}/.htpasswd +nginx-key-file = ${frontend-instance-certificate:key-file} +nginx-cert-file = ${frontend-instance-certificate:cert-file} +nginx-mime-types = ${directory:etc}/mime-types + + +# Create a launcher script in /etc/service for the headless shell +# executable. +[chromium-launcher] +recipe = slapos.recipe.template:jinja2 +template = + inline:#!/bin/sh + + export FONTCONFIG_FILE=${font-config:rendered} + exec {{ parameter_list['chromium-wrapper'] }} \ + --remote-debugging-address=${headless-chromium:ipv4} \ + --remote-debugging-port=${headless-chromium:remote-debugging-port} \ + ${headless-chromium:url} +rendered = ${directory:service}/chromium + + +# Configure and launch the proxy server. +[nginx-config] +recipe = slapos.recipe.template:jinja2 +template = {{ parameter_list['template-nginx-config'] }} +rendered = ${headless-chromium:nginx-config-target} +mode = 700 +context = + section param_headless_chromium headless-chromium + +[nginx-mime-types] +recipe = slapos.recipe.template:jinja2 +template = {{ parameter_list['template-mime-types'] }} +rendered = ${headless-chromium:nginx-mime-types} + +[nginx-launcher] +recipe = slapos.cookbook:wrapper +command-line = {{ parameter_list['nginx-location'] }}/sbin/nginx -c ${headless-chromium:nginx-config-target} +wrapper-path = ${directory:service}/nginx + +[logrotate-entry-nginx] +<= logrotate-entry-base +name = nginx +log = ${headless-chromium:nginx-error-log} ${headless-chromium:nginx-access-log} + +[frontend-instance-password] +recipe = slapos.cookbook:generate.password +username = admin +bytes = 12 + +[generate-passwd-file] +recipe = plone.recipe.command +command = + echo -n '${frontend-instance-password:username}:' > ${headless-chromium:nginx-htpasswd-file} + openssl passwd -apr1 '${frontend-instance-password:passwd}' >> ${headless-chromium:nginx-htpasswd-file} +environment = + PATH={{ parameter_list['openssl-location'] }}/bin:%(PATH)s + +# Generate a self-signed TLS certificate. +[frontend-instance-certificate] +recipe = plone.recipe.command +command = + if [ ! -e ${:key-file} ] + then + openssl req -x509 -nodes -days 3650 \ + -subj "/C=AA/ST=X/L=X/O=Dis/CN=${:common-name}" \ + -newkey rsa:1024 -keyout ${:key-file} \ + -out ${:cert-file} + openssl x509 -addtrust serverAuth \ + -in ${:cert-file} \ + -out ${:cert-file} + fi +update-command = ${:command} +key-file = ${directory:ssl}/${:_buildout_section_name_}.key +cert-file = ${directory:ssl}/${:_buildout_section_name_}.cert +common-name = ${headless-chromium:ipv6} +environment = + PATH={{ parameter_list['openssl-location'] }}/bin:%(PATH)s + + +# Generate a fonts.conf file. +[font-config] +recipe = slapos.recipe.template:jinja2 +template = {{ parameter_list['template-fonts-conf'] }} +rendered = ${directory:etc}/fonts.conf +context = + key cachedir :cache-dir + key fonts :fonts + key includes :includes +cache-dir = + ${directory:etc}/.fontconfig.cache +fonts = + {{ parameter_list['liberation-fonts-location'] }} +includes = + {{ parameter_list['fontconfig-location'] }}/etc/fonts/conf.d + + +[publish-connection-information] +recipe = slapos.cookbook:publish +<= monitor-publish +remote-debug-url = http://${headless-chromium:remote-debugging-address} +proxy-url = https://${headless-chromium:proxy-address} +frontend-url = ${remote-debugging-frontend:connection-secure_access} +username = ${frontend-instance-password:username} +password = ${frontend-instance-password:passwd} + +# Request a frontend URL from the CDN for the remote debugging interface. +[remote-debugging-frontend] +<= slap-connection +recipe = slapos.cookbook:requestoptional +name = Headless Chromium Remote Debugging Frontend +software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg +slave = true +config-url = https://${headless-chromium:proxy-address} +config-https-only = true +config-type = websocket +config-websocket-path-list = /devtools +return = domain secure_access + + +# Monitoring: check that the Chromium process is alive and responding to +# requests through the proxy. +[monitor-instance-parameter] +monitor-httpd-port = {{ parameter_dict['monitor-httpd-port'] }} + +# Promise to make sure the remote debugging frontend returns 200 when +# queried with the correct credentials. +[frontend-ok-promise] +<= monitor-promise-base +module = check_url_available +name = headless-chromium-frontend-ok.py +url = ${remote-debugging-frontend:connection-secure_access} +config-url = ${:url} +config-username = ${frontend-instance-password:username} +config-password = ${frontend-instance-password:passwd} + +# Promise to make sure that the remote debugging frontend returns 401 +# when queried with no credentials. +[frontend-secure-promise] +<= monitor-promise-base +module = check_url_available +name = headless-chromium-frontend-secure.py +url = ${remote-debugging-frontend:connection-secure_access} +config-url = ${:url} +config-http-code = 401 diff --git a/software/headless-chromium/instance.cfg.in b/software/headless-chromium/instance.cfg.in new file mode 100644 index 0000000000000000000000000000000000000000..51a6c6acdff5c42b6fb0c3181d2d934c31e8bb55 --- /dev/null +++ b/software/headless-chromium/instance.cfg.in @@ -0,0 +1,52 @@ +[buildout] +parts = + switch-softwaretype + +eggs-directory = {{ buildout['eggs-directory'] }} +develop-eggs-directory = {{ buildout['develop-eggs-directory'] }} +offline = true + +[profile-common] +openssl-location = {{ openssl_location }} +nginx-location = {{ nginx_location }} +liberation-fonts-location = {{ liberation_fonts_location }} +fontconfig-location = {{ fontconfig_location }} +chromium-wrapper = {{ chromium_wrapper }} +devtools-frontend = {{ devtools_frontend }} +template-nginx-config = {{ template_nginx_config_target }} +template-fonts-conf = {{ template_fonts_conf_target }} +template-monitor = {{ template_monitor }} +template-mime-types = {{ template_mime_types_target }} + +[instance-headless-chromium] +recipe = slapos.recipe.template:jinja2 +template = {{ template_instance_headless_chromium_target }} +rendered = ${buildout:directory}/${:filename} +filename = instance-headless-chromium.cfg +context = + section buildout buildout + section parameter_list profile-common + key partition_ipv4 slap-configuration:ipv4-random + key partition_ipv6 slap-configuration:ipv6-random + key slapparameter_dict slap-configuration:configuration + jsonkey default_parameter_dict :default-parameters +default-parameters = + { + "remote-debugging-port": 8081, + "nginx-proxy-port": 8082, + "target-url": "https://www.example.com", + "monitor-httpd-port": 8083 + } + +[switch-softwaretype] +recipe = slapos.cookbook:switch-softwaretype +RootSoftwareInstance = ${:default} +default = instance-headless-chromium:rendered + +[slap-configuration] +recipe = slapos.cookbook:slapconfiguration +computer = ${slap-connection:computer-id} +partition = ${slap-connection:partition-id} +url = ${slap-connection:server-url} +key = ${slap-connection:key-file} +cert = ${slap-connection:cert-file} diff --git a/software/headless-chromium/software.cfg b/software/headless-chromium/software.cfg new file mode 100644 index 0000000000000000000000000000000000000000..8950608e3a30bc02d329e2e4d99a90f1c84fd7e3 --- /dev/null +++ b/software/headless-chromium/software.cfg @@ -0,0 +1,50 @@ +[buildout] +extends = + buildout.hash.cfg + ../../stack/slapos.cfg + ../../stack/monitor/buildout.cfg + ../../component/headless-chromium/buildout.cfg + ../../component/openssl/buildout.cfg + ../../component/nginx/buildout.cfg + ../../component/fonts/buildout.cfg + ../../component/fontconfig/buildout.cfg + +parts = + slapos-cookbook + template-cfg + +[python] +part = python3 + +[template-cfg] +recipe = slapos.recipe.template:jinja2 +rendered = ${buildout:directory}/template.cfg +template = ${:_profile_base_location_}/${:filename} +mode = 0644 +context = + section buildout buildout + key openssl_location openssl:location + key nginx_location nginx:location + key liberation_fonts_location liberation-fonts:location + key fontconfig_location fontconfig:location + key chromium_wrapper headless-chromium-wrapper:rendered + key devtools_frontend headless-chromium:devtools-frontend + key template_nginx_config_target template-nginx-conf:target + key template_mime_types_target template-mime-types:target + key template_fonts_conf_target template-fonts-conf:output + key template_instance_headless_chromium_target instance-headless-chromium:target + key template_monitor monitor2-template:rendered + +[download-base] +recipe = slapos.recipe.build:download +url = ${:_profile_base_location_}/${:_update_hash_filename_} +mode = 0644 + +[instance-headless-chromium] +<= download-base + +[template-nginx-conf] +<= download-base + +[template-mime-types] +<= download-base diff --git a/software/headless-chromium/software.cfg.json b/software/headless-chromium/software.cfg.json new file mode 100644 index 0000000000000000000000000000000000000000..0cc8a09a5a203d6addbecd3e2e6cf3f4feda74ee --- /dev/null +++ b/software/headless-chromium/software.cfg.json @@ -0,0 +1,14 @@ +{ + "name": "Headless Chromium", + "description": "Headless (stripped-down) Chromium shell", + "serialization": "xml", + "software-type": { + "default": { + "title": "Default", + "description": "Standalone headless shell", + "request": "instance-headless-chromium-input-schema.json", + "response": "instance-headless-chromium-output-schema.json", + "index": 0 + } + } +} diff --git a/software/headless-chromium/templates/mime-types.in b/software/headless-chromium/templates/mime-types.in new file mode 100644 index 0000000000000000000000000000000000000000..f07db332b6dce975e457a8f9ba3bbd8ec1c39b49 --- /dev/null +++ b/software/headless-chromium/templates/mime-types.in @@ -0,0 +1,78 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml rss; + image/gif gif; + image/jpeg jpeg jpg; + application/x-javascript js; + application/atom+xml atom; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + + application/java-archive jar war ear; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream eot; + application/octet-stream iso img; + application/octet-stream msi msp msm; + application/ogg ogx; + + audio/midi mid midi kar; + audio/mpeg mpga mpega mp2 mp3 m4a; + audio/ogg oga ogg spx; + audio/x-realaudio ra; + audio/webm weba; + + video/3gpp 3gpp 3gp; + video/mp4 mp4; + video/mpeg mpeg mpg mpe; + video/ogg ogv; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/software/headless-chromium/templates/nginx.conf.in b/software/headless-chromium/templates/nginx.conf.in new file mode 100644 index 0000000000000000000000000000000000000000..0b812f16ce0d8127af404a8fe24847ef0661e2b2 --- /dev/null +++ b/software/headless-chromium/templates/nginx.conf.in @@ -0,0 +1,81 @@ +pid {{ param_headless_chromium['nginx-pid-path'] }}; +error_log {{ param_headless_chromium['nginx-error-log'] }}; + +events { + worker_connections 1024; +} + +http { + access_log {{ param_headless_chromium['nginx-access-log'] }}; + + include {{ param_headless_chromium['nginx-mime-types'] }}; + default_type application/octet-stream; + + types { + text/html html; + text/css css; + application/javascript js; + } + + server { + listen {{ param_headless_chromium['proxy-address'] }} ssl; + + # Require username/password to access remote debugging port. + auth_basic "Remote Debugging"; + auth_basic_user_file {{ param_headless_chromium['nginx-htpasswd-file'] }}; + + # Use self-signed SSL certificate. + ssl_certificate {{ param_headless_chromium['nginx-cert-file'] }}; + ssl_certificate_key {{ param_headless_chromium['nginx-key-file'] }}; + + client_body_temp_path {{ param_headless_chromium['nginx-temp-path'] }}; + proxy_temp_path {{ param_headless_chromium['nginx-temp-path'] }}; + fastcgi_temp_path {{ param_headless_chromium['nginx-temp-path'] }}; + uwsgi_temp_path {{ param_headless_chromium['nginx-temp-path'] }}; + scgi_temp_path {{ param_headless_chromium['nginx-temp-path'] }}; + + # All websocket connections are served from /devtools. + location /devtools { + proxy_http_version 1.1; + proxy_set_header Host {{ param_headless_chromium['remote-debugging-address'] }}; + proxy_pass http://{{ param_headless_chromium['remote-debugging-address'] }}; + proxy_set_header Upgrade "websocket"; + proxy_set_header Connection "Upgrade"; + } + + # The DevTools frontend is served from /serve_file/@{version_hash}. + location ~ "^\/serve_file\/@[0-9a-f]{5,40}\/(.*)" { + alias {{ param_headless_chromium['devtools-frontend-root'] }}/$1; + } + + location / { + proxy_http_version 1.1; + + # The proxy must set the Host header to an IP address, since the + # headless Chromium shell refuses to run otherwise, for security + # reasons. + # See https://bugs.chromium.org/p/chromium/issues/detail?id=813540. + proxy_set_header Host {{ param_headless_chromium['remote-debugging-address'] }}; + proxy_pass http://{{ param_headless_chromium['remote-debugging-address'] }}; + + # The browser security policy will prevent us from loading the + # Websocket connection without TLS, so we have to go through the + # frontend CDN URL. The tricky thing is that the frontend URL is + # not available yet when this file is built; what we do instead is + # use the given Host header. + sub_filter "ws={{ param_headless_chromium['remote-debugging-address'] }}" "wss=$host"; + sub_filter_once on; + sub_filter_types application/json; + + sub_filter "ws://{{ param_headless_chromium['remote-debugging-address'] }}" "wss://$host"; + sub_filter_types application/json; + + # We want to use our own DevTools frontend rather than + # https://chrome-devtools-frontend.appspot.com. There should be a + # --custom-devtools-frontend flag for Chromium, but it doesn't + # seem to work with the remote debugging port. + sub_filter "chrome-devtools-frontend.appspot.com" "$host"; + sub_filter_types *; + } + } +} diff --git a/software/headless-chromium/test/README.md b/software/headless-chromium/test/README.md new file mode 100644 index 0000000000000000000000000000000000000000..81a3023d3310b023af91489c8f40c1086bbf7a56 --- /dev/null +++ b/software/headless-chromium/test/README.md @@ -0,0 +1 @@ +Tests for headless Chromium software release diff --git a/software/headless-chromium/test/setup.py b/software/headless-chromium/test/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..49cef6de2bf96cd4a31683db6dc540db32389c6c --- /dev/null +++ b/software/headless-chromium/test/setup.py @@ -0,0 +1,50 @@ +############################################################################## +# +# Copyright (c) 2021 Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsibility of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# guarantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## +from setuptools import setup, find_packages + +version = '0.0.1.dev0' +name = 'slapos.test.headless-chromium' +long_description = open("README.md").read() + +setup( + name=name, + version=version, + description="Test for SlapOS headless Chromium", + long_description=long_description, + long_description_content_type='text/markdown', + maintainer="Nexedi", + maintainer_email="info@nexedi.com", + url="https://lab.nexedi.com/nexedi/slapos", + packages=find_packages(), + install_requires=[ + 'slapos.core', + 'slapos.libnetworkcache', + 'requests', + ], + zip_safe=True, + test_suite='test', +) diff --git a/software/headless-chromium/test/test.py b/software/headless-chromium/test/test.py new file mode 100644 index 0000000000000000000000000000000000000000..db7be48b4bbc08d39b135576fc3b8e340c3a89b2 --- /dev/null +++ b/software/headless-chromium/test/test.py @@ -0,0 +1,70 @@ +############################################################################## +# +# Copyright (c) 2021 Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsibility of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# guarantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import os +import requests + +from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass + + +setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass( + os.path.abspath( + os.path.join(os.path.dirname(__file__), '../software.cfg'))) + +class TestHeadlessChromium(SlapOSInstanceTestCase): + def setUp(self): + self.connection_parameters = self.requestDefaultInstance().getConnectionParameterDict() + + def test_remote_debugging_port(self): + # The headless browser should respond at /json with a nonempty list + # of available pages, each of which has a webSocketDebuggerUrl and a + # devtoolsFrontendUrl. + url = self.connection_parameters['remote-debug-url'] + response = requests.get('%s/json' % url) + + # Check that request was successful and the response was a nonempty + # list. + self.assertEqual(requests.codes['ok'], response.status_code) + self.assertTrue(len(response.json()) > 0) + + # Check that the first page has the correct fields. + first_page = response.json()[0] + self.assertIn('webSocketDebuggerUrl', first_page) + self.assertIn('devtoolsFrontendUrl', first_page) + + def test_devtools_frontend_ok(self): + # The proxy should serve the DevTools frontend from + # /serve_file/@{hash}/inspector.html, where {hash} is a 5-32 digit + # hash. + proxyURL = self.connection_parameters['proxy-url'] + username = self.connection_parameters['username'] + password = self.connection_parameters['password'] + frontend = '/serve_file/@aaaaa/inspector.html' + + response = requests.get(proxyURL + frontend, verify=False, + auth=(username, password)) + self.assertEqual(requests.codes['ok'], response.status_code) diff --git a/software/slapos-sr-testing/software-py3.cfg b/software/slapos-sr-testing/software-py3.cfg index 4c1f721c80c7529870ed69914b781150c937da15..4de703b57d2228aaa1d37e72a13df22d10b75694 100644 --- a/software/slapos-sr-testing/software-py3.cfg +++ b/software/slapos-sr-testing/software-py3.cfg @@ -22,3 +22,4 @@ extra = proftpd ${slapos.test.proftpd-setup:setup} repman ${slapos.test.repman-setup:setup} restic-rest-server ${slapos.test.restic_rest_server-setup:setup} + headless-chromium ${slapos.test.headless-chromium-setup:setup} diff --git a/software/slapos-sr-testing/software.cfg b/software/slapos-sr-testing/software.cfg index ff8c9ae38a80d2cc1b0d645066de484ed7fbeb01..403efee1386694ef35c7e08281e5767f55320349 100644 --- a/software/slapos-sr-testing/software.cfg +++ b/software/slapos-sr-testing/software.cfg @@ -194,6 +194,11 @@ setup = ${slapos-repository:location}/software/jscrawler/test/ egg = slapos.test.galene setup = ${slapos-repository:location}/software/galene/test/ +[slapos.test.headless-chromium-setup] +<= setup-develop-egg +egg = slapos.test.headless-chromium +setup = ${slapos-repository:location}/software/headless-chromium/test/ + [slapos.core-repository] <= git-clone-repository repository = https://lab.nexedi.com/nexedi/slapos.core.git @@ -254,6 +259,7 @@ extra-eggs = ${slapos.test.html5as-setup:egg} ${slapos.test.html5as-base-setup:egg} ${slapos.test.fluentd-setup:egg} + ${slapos.test.headless-chromium-setup:egg} # We don't name this interpreter `python`, so that when we run slapos node # software, installation scripts running `python` use a python without any