From 9f2d51be3365b6ea49efc88e4335a93bf00de3cb Mon Sep 17 00:00:00 2001 From: Gabriele Facciolo Date: Wed, 24 Aug 2016 01:34:02 +0200 Subject: [PATCH] initial commit --- Dockerfile | 107 ++++++ README.md | 47 ++- apache/dot.htaccess | 30 ++ apache/sites-zotero.conf | 46 +++ apache/zotero.cert | 24 ++ apache/zotero.key | 41 +++ cert_override.txt | 3 + dataserver/config.inc.php | 85 +++++ dataserver/dbconnect.inc.php | 45 +++ dataserver/sv/zotero-download/log/run | 3 + dataserver/sv/zotero-download/run | 5 + dataserver/sv/zotero-error/log/run | 3 + dataserver/sv/zotero-error/run | 5 + dataserver/sv/zotero-upload/log/run | 3 + dataserver/sv/zotero-upload/run | 5 + mysql/setup_db | 33 ++ mysql/zotero.cnf | 6 + patches/add_user | 22 ++ patches/uwsgi | 143 +++++++ zss/ZSS.pm | 512 ++++++++++++++++++++++++++ zss/ZSS/Store.pm | 164 +++++++++ zss/zss.psgi | 11 + zss/zss.yaml | 3 + 23 files changed, 1345 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 apache/dot.htaccess create mode 100644 apache/sites-zotero.conf create mode 100644 apache/zotero.cert create mode 100644 apache/zotero.key create mode 100644 cert_override.txt create mode 100644 dataserver/config.inc.php create mode 100644 dataserver/dbconnect.inc.php create mode 100755 dataserver/sv/zotero-download/log/run create mode 100755 dataserver/sv/zotero-download/run create mode 100755 dataserver/sv/zotero-error/log/run create mode 100755 dataserver/sv/zotero-error/run create mode 100755 dataserver/sv/zotero-upload/log/run create mode 100755 dataserver/sv/zotero-upload/run create mode 100755 mysql/setup_db create mode 100644 mysql/zotero.cnf create mode 100755 patches/add_user create mode 100755 patches/uwsgi create mode 100644 zss/ZSS.pm create mode 100644 zss/ZSS/Store.pm create mode 100644 zss/zss.psgi create mode 100644 zss/zss.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6cf6f37 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,107 @@ +FROM debian:wheezy-backports +MAINTAINER Gabriele Facciolo +# Following http://git.27o.de/dataserver/about/Installation-Instructions-for-Debian-Wheezy.md + +# debian packages +RUN apt-get update && apt-get install -y \ + apache2 libapache2-mod-php5 mysql-server memcached zendframework php5-cli php5-memcached php5-mysql php5-curl \ + apache2 uwsgi uwsgi-plugin-psgi libplack-perl libdigest-hmac-perl libjson-xs-perl libfile-util-perl libapache2-mod-uwsgi libswitch-perl \ + git gnutls-bin runit wget curl net-tools vim build-essential + +# Zotero +RUN mkdir -p /srv/zotero/log/upload && \ + mkdir -p /srv/zotero/log/download && \ + mkdir -p /srv/zotero/log/error && \ + mkdir -p /srv/zotero/log/api-errors && \ + mkdir -p /srv/zotero/log/sync-errors && \ + mkdir -p /srv/zotero/dataserver && \ + mkdir -p /srv/zotero/zss && \ + mkdir -p /var/log/httpd/sync-errors && \ + mkdir -p /var/log/httpd/api-errors && \ + chown www-data: /var/log/httpd/sync-errors && \ + chown www-data: /var/log/httpd/api-errors + +# Dataserver +RUN git clone --depth=1 git://git.27o.de/dataserver /srv/zotero/dataserver && \ + chown www-data:www-data /srv/zotero/dataserver/tmp +#RUN cd /srv/zotero/dataserver/include && rm -r Zend && ln -s /usr/share/php/libzend-framework-php/Zend +RUN cd /srv/zotero/dataserver/include && rm -r Zend && ln -s /usr/share/php/Zend + +#Apache2 +#certtool -p --sec-param high --outfile /etc/apache2/zotero.key +#certtool -s --load-privkey /etc/apache2/zotero.key --outfile /etc/apache2/zotero.cert +ADD apache/zotero.key /etc/apache2/ +ADD apache/zotero.cert /etc/apache2/ +ADD apache/sites-zotero.conf /etc/apache2/sites-available/zotero +ADD apache/dot.htaccess /srv/zotero/dataserver/htdocs/\.htaccess +RUN a2enmod ssl && \ + a2enmod rewrite && \ + a2ensite zotero + +#Mysql +ADD mysql/zotero.cnf /etc/mysql/conf.d/zotero.cnf +ADD mysql/setup_db /srv/zotero/dataserver/misc/setup_db +RUN /etc/init.d/mysql start && \ + mysqladmin -u root password password && \ + cd /srv/zotero/dataserver/misc/ && \ + ./setup_db + + +# Zotero Configuration +ADD dataserver/dbconnect.inc.php dataserver/config.inc.php /srv/zotero/dataserver/include/config/ +ADD dataserver/sv/zotero-download /etc/sv/zotero-download +ADD dataserver/sv/zotero-upload /etc/sv/zotero-upload +ADD dataserver/sv/zotero-error /etc/sv/zotero-error +RUN cd /etc/service && \ + ln -s ../sv/zotero-download /etc/service/ && \ + ln -s ../sv/zotero-upload /etc/service/ && \ + ln -s ../sv/zotero-error /etc/service/ + + + +# ZSS +RUN git clone --depth=1 git://git.27o.de/zss /srv/zotero/zss && \ + mkdir /srv/zotero/storage && \ + chown www-data:www-data /srv/zotero/storage + +ADD zss/zss.yaml /etc/uwsgi/apps-available/ +ADD zss/ZSS.pm /srv/zotero/zss/ +ADD zss/zss.psgi /srv/zotero/zss/ +RUN ln -s /etc/uwsgi/apps-available/zss.yaml /etc/uwsgi/apps-enabled +# fix uwsgi init scipt (always fails) +ADD patches/uwsgi /etc/init.d/uwsgi + + +## failed attempt to install Zotero Web-Library locally +## not working +#RUN cd /srv/ && \ +# git clone --depth=1 --recursive https://github.com/zotero/web-library.git && \ +# curl -sL https://deb.nodesource.com/setup_4.x | bash - && apt-get install -y nodejs && \ +# cd /srv/web-library && \ +# npm install && \ +# npm install prompt + + + + +# replace custom /srv/zotero/dataserver/admin/add_user that allows to write the password +ADD patches/add_user /srv/zotero/dataserver/admin/add_user + +# TEST ADD USER: test PASSWORD: test +RUN service mysql start && service memcached start && \ + cd /srv/zotero/dataserver/admin && \ + ./add_user 101 test test && \ + ./add_user 102 test2 test2 && \ + ./add_group -o test -f members -r members -e members testgroup && \ + ./add_groupuser testgroup test2 member + + +# docker server startup +EXPOSE 80 443 + +CMD service mysql start && \ + service uwsgi start && \ + service apache2 start && \ + service memcached start && \ + bash -c "/usr/sbin/runsvdir-start&" && \ + /bin/bash diff --git a/README.md b/README.md index c48c1da..bc08c50 100644 --- a/README.md +++ b/README.md @@ -1 +1,46 @@ -# Docker image for Zotero Data Server +# Docker image for a Zotero Data Server + +This image was build following the instructions for installing a Zotero dataserver at (http://git.27o.de/dataserver/about/), which is an updated procedure of [this document](https://github.com/Panzerkampfwagen/dataserver/blob/master/misc/Zotero_Data_Server_Installation_Debian.pdf). + + +## Build the image + + docker build -t zotero . + +The resulting image is configured run a dataserver on https://localhost/. + +To customize the installation the following files must be edited: +* SSL certificate: apache/zotero.{cert,key}. The current certificate is self-signed for localhost. +* Apache site config: apache/sites-zotero.conf. +* Dataserver: dataserver/config.inc.php. To match the site config. +* MySQL credentials/passwords: mysql/setup\_db and dataserver/dbconnect.inc.php accordingly. + +The build procedure also creates a couple of test users: test:test and test2:test2. + + +## Start the dataserver + + docker run -p 80:80 -p 443:443 -t -i zotero + +This will start the dataserver on https://localhost/ + + https://localhost/sync/login?version=9&username=test&password=test + +because of the self-signed certificate some browsers may refuse to connect the server. + + +## Patch the standalone client to use the new dataserver + +We follow the procedure of (http://git.27o.de/dataserver/about/Zotero-Client.md). +Download the Zotero client, and change these two lines in resource/config.js inside the zotero.jar archive (zip) + + SYNC_URL: 'https://localhost/sync/', + API_URL: 'https://localhost/', + +If the server uses a self-signed certificate an exception should be added to the client. A cert\_override.txt file must added to the local profile generated by zotero client: + + ~/Library/Application\ Support/Zotero/Profiles/[something].default/ MAC + ~/.zotero/Profiles/[something].default/ Linux + c:Users//AppData/Roaming/Zotero/Zotero/ Win + +The cert\_override.txt file can be generated with Firefox following (https://groups.google.com/d/msg/zotero-dev/MEwLaptJIzI/PVDAFJiqEgAJ) diff --git a/apache/dot.htaccess b/apache/dot.htaccess new file mode 100644 index 0000000..fe0ba8f --- /dev/null +++ b/apache/dot.htaccess @@ -0,0 +1,30 @@ +# If on a testing site, deny by default unless IP is allowed +SetEnvIf Host "apidev" ACCESS_CONTROL +SetEnvIf Host "syncdev" ACCESS_CONTROL +####### Local +SetEnvIf X-Forwarded-For "192.168.1.|" !ACCESS_CONTROL +order deny,allow +deny from env=ACCESS_CONTROL + +#php_flag zlib.output_compression On +#php_value zlib.output_compression_level 5 +php_value short_open_tag 1 + +php_value include_path "../include" +php_value auto_prepend_file "header.inc.php" +php_value auto_append_file "footer.inc.php" + +php_value memory_limit 500M + +#php_value xdebug.show_local_vars 1 +#php_value xdebug.profiler_enable 1 +#php_value xdebug.profiler_enable_trigger 1 +#php_value xdebug.profiler_output_dir /tmp/xdebug + +RewriteEngine On + +# If file or directory doesn't exist, pass to director for MVC redirections +RewriteCond %{SCRIPT_FILENAME} !-f +RewriteCond %{SCRIPT_FILENAME} !-d +RewriteCond %{REQUEST_URI} !^/zotero +RewriteRule .* index.php [L] diff --git a/apache/sites-zotero.conf b/apache/sites-zotero.conf new file mode 100644 index 0000000..9a27088 --- /dev/null +++ b/apache/sites-zotero.conf @@ -0,0 +1,46 @@ + + DocumentRoot /srv/web-library + + Options FollowSymLinks + AllowOverride None + + + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + + + + + + DocumentRoot /srv/zotero/dataserver/htdocs + SSLEngine on + SSLCertificateFile /etc/apache2/zotero.cert + SSLCertificateKeyFile /etc/apache2/zotero.key + + + SetHandler uwsgi-handler + uWSGISocket /var/run/uwsgi/app/zss/socket + uWSGImodifier1 5 + + + + Options FollowSymLinks MultiViews + AllowOverride All + +#2.2 + Order allow,deny + Allow from all +# 2.4 + # Require all granted + # + # If you are using a more recent version of apache + # and are getting 403 errors, replace the Order and + # Allow lines with: + # Require all granted + + + ErrorLog /srv/zotero/log/error.log + CustomLog /srv/zotero/log/access.log common + diff --git a/apache/zotero.cert b/apache/zotero.cert new file mode 100644 index 0000000..365bd97 --- /dev/null +++ b/apache/zotero.cert @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEFTCCAmegAwIBAgIEV7XanjANBgkqhkiG9w0BAQsFADAAMB4XDTE2MDgxODE1 +NTYxNloXDTQ0MDEwNDE1NTYxOFowADCCAbgwDQYJKoZIhvcNAQEBBQADggGlADCC +AaACggGXANJ8/OlJsFwD0I2Xgjas22dg5ESYcj+xf5IBNd1FVQxKUfpLA/vpEV9n +bIyDHZgXdiKKQYdfTUbdJVX7ilSmmvDJ9wPjfa/72L7fCJl2oCY98pNVNBelXe/u +zABW4PZVGNoaLE/H5/Bar14E9l0YJ6DbaaEQ8xNeieTZkGZ0SUwVdC0p6V11dEHG +MPhEw3aXH0kAy9KAAPmkXVnmSKsRnCaVft1ob8IMvYPmHwlYa4uYbW3JhOu8NkMJ +kVWJ3JEdWfWPX/M7VT7DFyC42JP17NHJgViFS8tuYGiWtwFDeY1grExB3ZDFySGh +s3NYSMRV710xAY5M7oqKnojv53tGzeQOQ5Mhv1A9i62f4h3xzwnQ7rroPprU5h4Y +LPvIQ4RnVOH1o7+Ug70SG8IOWPP8wX8egBWalWqtrP5XyADEUwZM3f0EWUUMnCg1 +kZp1woiE4eRI6YdbWBrJlelcF6DSo+GONHHnyp438PS9eiLE0Xmv4ZyRXx+TjIMC +A1RqydW6qsHmvaBuAwCKggoFvQ5vSpAolgBFIOcCAwEAAaNrMGkwDAYDVR0TAQH/ +BAIwADAlBgNVHREEHjAcgglsb2NhbGhvc3SCCWxvY2FsaG9zdIcEfwAAATATBgNV +HSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUTnN76df4/7g8WmMd7J2UTIUO/wcw +DQYJKoZIhvcNAQELBQADggGXAM1Gdi5xk74RPJTIwdRn/J7YLQMwwUB0Nhsk/FvO +9pjQJrXBNq7dPHaLPwpFCK6YjDiVm3rV7f96mdSpo7A9GtH0d2Cx6I1a+3h/zHUu +XSWLoLPQpt84Qk2qhNRNNQhBc6NbpSS/754wQH4o+QiVbfrti2f0SaGAlso1tJbB +/gckYA9SjCDlBPL4nUjms2w32ooiXxUkYcgAp0a9TBpQ6YgNL5bD3m0I2kPi/VZD +UIaMoj6lw5xUKaT8EhowrwO5796bCBB3sCGN0YciUrMrl04ZWYqbNuZYtFCb2kWt +K9wkiIoZzn+CMEfDFmFODG4qf0YvwO+qZz26ph4gJJ6XIi8vcZJam9HBWBvb/TOZ +UZVZKwHvTPkVFLyCd8q8S3LPlSrvWy4m63jtp6FGbOt0lQyVEt/L/xsig72UXBxl +3QfdPEHbr5pL8L8HNHElT2SB8q2OKFpHSdQtwA45jCA7W3OHtCIYvIhf34fAIHmX +2rglG/EHHGCZSPaC6lzbAyXvcuGyQ5ENUQiEL84nNm1vo51p6zlzR/E= +-----END CERTIFICATE----- diff --git a/apache/zotero.key b/apache/zotero.key new file mode 100644 index 0000000..2e3f61a --- /dev/null +++ b/apache/zotero.key @@ -0,0 +1,41 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/cert_override.txt b/cert_override.txt new file mode 100644 index 0000000..056ce91 --- /dev/null +++ b/cert_override.txt @@ -0,0 +1,3 @@ +# PSM Certificate Override Settings file +# This is a generated file! Do not edit. +localhost:443 OID.2.16.840.1.101.3.4.2.1 A1:B0:AF:69:BC:F0:59:39:3A:BF:2C:8C:80:05:6B:9F:5F:80:69:BB:12:8C:92:07:C7:B4:E9:2B:90:82:AF:E9 U AAAAAAAAAAAAAAAEAAAAAle12p4wAA== diff --git a/dataserver/config.inc.php b/dataserver/config.inc.php new file mode 100644 index 0000000..ff264c1 --- /dev/null +++ b/dataserver/config.inc.php @@ -0,0 +1,85 @@ + 'sync' + ); + + public static $MEMCACHED_ENABLED = true; + public static $MEMCACHED_SERVERS = array( + 'localhost:11211' + ); + + public static $TRANSLATION_SERVERS = array( + "translation1.localdomain:1969" + ); + + public static $CITATION_SERVERS = array( + "citeserver1.localdomain:8080", "citeserver2.localdomain:8080" + ); + + public static $ATTACHMENT_SERVER_HOSTS = array("files1.localdomain", "files2.localdomain"); + public static $ATTACHMENT_SERVER_DYNAMIC_PORT = 80; + public static $ATTACHMENT_SERVER_STATIC_PORT = 81; + public static $ATTACHMENT_SERVER_URL = "https://files.example.net"; + public static $ATTACHMENT_SERVER_DOCROOT = "/var/www/attachments/"; + + public static $STATSD_ENABLED = false; + public static $STATSD_PREFIX = ""; + public static $STATSD_HOST = "monitor.localdomain"; + public static $STATSD_PORT = 8125; + + public static $LOG_TO_SCRIBE = false; + public static $LOG_ADDRESS = ''; + public static $LOG_PORT = 1463; + public static $LOG_TIMEZONE = 'US/Eastern'; + public static $LOG_TARGET_DEFAULT = 'errors'; + + public static $PROCESSOR_PORT_DOWNLOAD = 3455; + public static $PROCESSOR_PORT_UPLOAD = 3456; + public static $PROCESSOR_PORT_ERROR = 3457; + + public static $PROCESSOR_LOG_TARGET_DOWNLOAD = 'sync-processor-download'; + public static $PROCESSOR_LOG_TARGET_UPLOAD = 'sync-processor-upload'; + public static $PROCESSOR_LOG_TARGET_ERROR = 'sync-processor-error'; + + public static $SYNC_DOWNLOAD_SMALLEST_FIRST = false; + public static $SYNC_UPLOAD_SMALLEST_FIRST = false; + + // Set some things manually for running via command line + public static $CLI_PHP_PATH = '/usr/bin/php'; + public static $CLI_DOCUMENT_ROOT = "/srv/zotero/dataserver/"; + + public static $SYNC_ERROR_PATH = '/srv/zotero/log/sync-errors/'; + public static $API_ERROR_PATH = '/srv/zotero/log/api-errors/'; + + public static $CACHE_VERSION_ATOM_ENTRY = 1; + public static $CACHE_VERSION_BIB = 1; + public static $CACHE_VERSION_ITEM_DATA = 1; +} +?> diff --git a/dataserver/dbconnect.inc.php b/dataserver/dbconnect.inc.php new file mode 100644 index 0000000..dcfc08e --- /dev/null +++ b/dataserver/dbconnect.inc.php @@ -0,0 +1,45 @@ +$host, + 'port'=>$port, + 'db'=>$db, + 'user'=>$user, + 'pass'=>$pass, + 'charset'=>$charset + ); +} +?> diff --git a/dataserver/sv/zotero-download/log/run b/dataserver/sv/zotero-download/log/run new file mode 100755 index 0000000..9723b40 --- /dev/null +++ b/dataserver/sv/zotero-download/log/run @@ -0,0 +1,3 @@ +#!/bin/sh + +exec svlogd /srv/zotero/log/download diff --git a/dataserver/sv/zotero-download/run b/dataserver/sv/zotero-download/run new file mode 100755 index 0000000..c777c43 --- /dev/null +++ b/dataserver/sv/zotero-download/run @@ -0,0 +1,5 @@ +#!/bin/sh + +cd /srv/zotero/dataserver/processor/download +exec 2>&1 +exec chpst -u www-data:www-data php5 daemon.php diff --git a/dataserver/sv/zotero-error/log/run b/dataserver/sv/zotero-error/log/run new file mode 100755 index 0000000..fcbf9c7 --- /dev/null +++ b/dataserver/sv/zotero-error/log/run @@ -0,0 +1,3 @@ +#!/bin/sh + +exec svlogd /srv/zotero/log/error diff --git a/dataserver/sv/zotero-error/run b/dataserver/sv/zotero-error/run new file mode 100755 index 0000000..ce22ec6 --- /dev/null +++ b/dataserver/sv/zotero-error/run @@ -0,0 +1,5 @@ +#!/bin/sh + +cd /srv/zotero/dataserver/processor/error +exec 2>&1 +exec chpst -u www-data:www-data php5 daemon.php diff --git a/dataserver/sv/zotero-upload/log/run b/dataserver/sv/zotero-upload/log/run new file mode 100755 index 0000000..6a2b107 --- /dev/null +++ b/dataserver/sv/zotero-upload/log/run @@ -0,0 +1,3 @@ +#!/bin/sh + +exec svlogd /srv/zotero/log/upload diff --git a/dataserver/sv/zotero-upload/run b/dataserver/sv/zotero-upload/run new file mode 100755 index 0000000..fb25945 --- /dev/null +++ b/dataserver/sv/zotero-upload/run @@ -0,0 +1,5 @@ +#!/bin/sh + +cd /srv/zotero/dataserver/processor/upload +exec 2>&1 +exec chpst -u www-data:www-data php5 daemon.php diff --git a/mysql/setup_db b/mysql/setup_db new file mode 100755 index 0000000..777d97f --- /dev/null +++ b/mysql/setup_db @@ -0,0 +1,33 @@ +#!/bin/sh +DB="mysql -h 127.0.0.1 -P 3306 -u root -ppassword" + +echo "DROP DATABASE IF EXISTS zotero_master" | $DB +echo "DROP DATABASE IF EXISTS zotero_shards" | $DB +echo "DROP DATABASE IF EXISTS zotero_ids" | $DB + +echo "CREATE DATABASE zotero_master" | $DB +echo "CREATE DATABASE zotero_shards" | $DB +echo "CREATE DATABASE zotero_ids" | $DB + +echo "DROP USER zotero@localhost;" | $DB + +echo "CREATE USER zotero@localhost IDENTIFIED BY 'foobar';" | $DB + +echo "GRANT SELECT, INSERT, UPDATE, DELETE ON zotero_master.* TO zotero@localhost;" | $DB +echo "GRANT SELECT, INSERT, UPDATE, DELETE ON zotero_shards.* TO zotero@localhost;" | $DB +echo "GRANT SELECT,INSERT,DELETE ON zotero_ids.* TO zotero@localhost;" | $DB + +# Load in master schema +$DB zotero_master < master.sql +$DB zotero_master < coredata.sql + +# Set up shard info +echo "INSERT INTO shardHosts VALUES (1, '127.0.0.1', 3306, 'up');" | $DB zotero_master +echo "INSERT INTO shards VALUES (1, 1, 'zotero_shards', 'up', 0);" | $DB zotero_master + +# Load in shard schema +cat shard.sql | $DB zotero_shards +cat triggers.sql | $DB zotero_shards + +# Load in schema on id server +cat ids.sql | $DB zotero_ids diff --git a/mysql/zotero.cnf b/mysql/zotero.cnf new file mode 100644 index 0000000..6e1152f --- /dev/null +++ b/mysql/zotero.cnf @@ -0,0 +1,6 @@ +[mysqld] +character-set-server = utf8 +collation-server = utf8_general_ci +event-scheduler = ON +sql-mode = STRICT_ALL_TABLES +default-time-zone = '+0:00' diff --git a/patches/add_user b/patches/add_user new file mode 100755 index 0000000..c302bc4 --- /dev/null +++ b/patches/add_user @@ -0,0 +1,22 @@ +#!/usr/bin/php + diff --git a/patches/uwsgi b/patches/uwsgi new file mode 100755 index 0000000..5cd1ef9 --- /dev/null +++ b/patches/uwsgi @@ -0,0 +1,143 @@ +#!/bin/bash +### BEGIN INIT INFO +# Provides: uwsgi +# Required-Start: $local_fs $remote_fs $network +# Required-Stop: $local_fs $remote_fs $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start/stop uWSGI server instance(s) +# Description: This script manages uWSGI server instance(s). +# You could control specific instance(s) by issuing: +# +# service uwsgi ... +# +# You can issue to init.d script following commands: +# * start | starts daemon +# * stop | stops daemon +# * reload | sends to daemon SIGHUP signal +# * force-reload | sends to daemon SIGTERM signal +# * restart | issues 'stop', then 'start' commands +# * status | shows status of daemon instance +# +# 'status' command must be issued with exactly one +# argument: ''. +# +# In init.d script output: +# * . -- command was executed without problems or instance +# is already in needed state +# * ! -- command failed (or executed with some problems) +# * ? -- configuration file for this instance isn't found +# and this instance is ignored +# +# For more details see /usr/share/doc/uwsgi/README.Debian. +### END INIT INFO + +# Author: Leonid Borisenko + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="app server(s)" +NAME="uwsgi" +DAEMON="/usr/bin/uwsgi" +SCRIPTNAME="/etc/init.d/${NAME}" + +UWSGI_CONFDIR="/etc/uwsgi" +UWSGI_APPS_CONFDIR_SUFFIX="s-enabled" +UWSGI_APPS_CONFDIR_GLOB="${UWSGI_CONFDIR}/app${UWSGI_APPS_CONFDIR_SUFFIX}" + +UWSGI_RUNDIR="/run/uwsgi" + +# Configuration namespace is used as name of runtime and log subdirectory. +# uWSGI instances sharing the same app configuration directory also shares +# the same runtime and log subdirectory. +# +# When init.d script cannot detect namespace for configuration file, default +# namespace will be used. +UWSGI_DEFAULT_CONFNAMESPACE=app + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Read configuration variable file if it is present +[ -r "/etc/default/${NAME}" ] && . "/etc/default/${NAME}" + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# Define supplementary functions +. /usr/share/uwsgi/init/snippets +. /usr/share/uwsgi/init/do_command + +WHAT=$1 +shift +case "$WHAT" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_command "$WHAT" "$@" + RETVAL="$?" + RETVAL=0 + [ "$VERBOSE" != no ] && log_end_msg "$RETVAL" + ;; + + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_command "$WHAT" "$@" + RETVAL="$?" + [ "$VERBOSE" != no ] && log_end_msg "$RETVAL" + ;; + + status) + if [ -z "$1" ]; then + [ "$VERBOSE" != no ] && log_failure_msg "which one?" + else + PIDFILE="$( + find_specific_pidfile "$(relative_path_to_conffile_with_spec "$1")" + )" + status_of_proc -p "$PIDFILE" "$DAEMON" "$NAME" \ + && exit 0 \ + || exit $? + fi + ;; + + reload) + [ "$VERBOSE" != no ] && log_daemon_msg "Reloading $DESC" "$NAME" + do_command "$WHAT" "$@" + RETVAL="$?" + [ "$VERBOSE" != no ] && log_end_msg "$RETVAL" + ;; + + force-reload) + [ "$VERBOSE" != no ] && log_daemon_msg "Forced reloading $DESC" "$NAME" + do_command "$WHAT" "$@" + RETVAL="$?" + [ "$VERBOSE" != no ] && log_end_msg "$RETVAL" + ;; + + restart) + [ "$VERBOSE" != no ] && log_daemon_msg "Restarting $DESC" "$NAME" + CURRENT_VERBOSE=$VERBOSE + VERBOSE=no + do_command stop "$@" + VERBOSE=$CURRENT_VERBOSE + case "$?" in + 0) + do_command start "$@" + RETVAL="$?" + [ "$VERBOSE" != no ] && log_end_msg "$RETVAL" + ;; + *) + # Failed to stop + [ "$VERBOSE" != no ] && log_end_msg 1 + ;; + esac + ;; + + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload|force-reload}" >&2 + exit 3 + ;; +esac diff --git a/zss/ZSS.pm b/zss/ZSS.pm new file mode 100644 index 0000000..ec8ad91 --- /dev/null +++ b/zss/ZSS.pm @@ -0,0 +1,512 @@ +package ZSS; + +use strict; +use warnings; + +use Plack::Request; +use Digest::HMAC_SHA1 qw(hmac_sha1); +use Digest::MD5 qw (md5_base64); +use MIME::Base64 qw(decode_base64 encode_base64); +use JSON::XS; +use Date::Parse; +use URI; +use URI::QueryParam; +use URI::Escape; +use Switch; +use Encode; +use Try::Tiny; + +use ZSS::Store; + +use Data::Dumper qw(Dumper); +$Data::Dumper::Sortkeys = 1; + +sub new { + my ($class) = @_; + + # TODO: read from config + my $self = {}; + + $self->{buckets}->{zotero}->{secretkey} = "yoursecretkey"; + $self->{buckets}->{zotero}->{store} = ZSS::Store->new("/srv/zotero/storage/"); + + bless $self, $class; +} + +sub respond { + my $code = shift; + my $msg = shift; + return [ $code, [ 'Content-Type' => 'text/plain', 'Content-Length' => length($msg)], [$msg] ]; +} + +sub xml2string { + my $xml = shift; + + my $msg = ''; + + while (my $token = shift @{$xml}) { + my $data = shift @{$xml}; + $msg .= '<'.$token.'>'; + if (ref $data eq 'ARRAY') { + $msg .= xml2string($data); + } else { + $msg .= $data; + } + $msg .= ''; + } + return $msg; +} + +sub respondXML { + my $code = shift; + my $xml = shift; + + return [ $code, [ 'Content-Type' => 'application/xml'], ["\n".xml2string($xml)] ]; +} + +sub check_policy { + my ($self, $cmp, $key, $val) = @_; + + switch ($cmp) { + case 'eq' { + if ($key eq 'bucket') { + $key = $self->{request}->{bucket}; + } else { + # TODO: Replace Plack::Request + $key = $self->{req}->parameters->get($key) + } + return 1 if $key eq $val; + }; + case 'content-length-range' { + my $len = $self->{request}->{env}->{CONTENT_LENGTH}; + # $self->log("Length: ".$len.", Limits: ".$key.", ".$val); + return 1 if (($len > $key) && ($len < $val)); + } + } + return 0; +} + +sub log { + my ($self, $msg) = @_; + + $self->{request}->{env}->{'psgix.logger'}->({ level => 'debug', message => $msg }); +} + +sub get_signature { + my $self = shift; + + my $request = $self->{request}; + my $env = $request->{env}; + + my $secret = $self->{buckets}->{$request->{bucket}}->{secretkey}; + + my $query = {}; + my $use_query = undef; + + if ($env->{QUERY_STRING}) { + $query = $request->{uri}->query_form_hash(); + } + + if ($query->{Signature}) { + $use_query = 1; + } + + # X-AMZ headers or query parameters + my $amzstring = ''; + if ($use_query) { + for my $key (sort(grep(/^x-amz/, keys %$query))) { + my $value = $query->{$key}; + $amzstring .= lc($key).":".$value."\n"; + } + } else { + for my $key (sort(grep(/^HTTP_X_AMZ/, keys %$env))) { + next if ($key eq 'HTTP_X_AMZ_DATE'); + my $value = $env->{$key}; + $key =~ s/_/-/g; + $amzstring .= lc(substr($key,5)).":".$value."\n"; + } + } + + # Date Header or Expires query parameter + my $date; + if ($use_query) { + $date = $query->{Expires}; + } else { + if ($env->{HTTP_X_AMZ_DATE}) { + $date = $env->{HTTP_X_AMZ_DATE}; + } else { + $date = $env->{HTTP_DATE}; + } + } + + # changing response headers parameters + my $additional_params = ''; + if ($query) { + my $sep = '?'; + my @params = qw(response-cache-control response-content-disposition response-content-encoding response-content-language response-content-type response-expires); + for my $key (@params) { + if ($query->{$key}) { + $additional_params .= $sep.$key."=".$query->{$key}; + $sep = '&' if ($sep eq '?'); + } + } + } + + my $stringtosign = $env->{REQUEST_METHOD}."\n". + ($env->{HTTP_CONTENT_MD5} || '')."\n". + ($env->{CONTENT_TYPE} || '')."\n". + ($date || '')."\n". + $amzstring. + "/".$request->{bucket}."/".$request->{key_escaped}.$additional_params; + + # $self->log("Stringtosign:".$stringtosign."End"); + + return encode_base64(hmac_sha1($stringtosign, $secret), ''); +} + +sub check_signature { + my $self = shift; + + my $request = $self->{request}; + my $env = $request->{env}; + + my $received_signature; + + if ($env->{QUERY_STRING} eq '') { + ($received_signature) = ($env->{HTTP_AUTHORIZATION} || '') =~ m/^AWS .*:(.*)$/; + } else { + $received_signature = $request->{uri}->query_param('Signature') || ''; + } + + unless ($received_signature) { + return 0; + } + + my $signature = $self->get_signature(); + + # $self->log("Check Signature: $received_signature == $signature"); + + return ($signature eq $received_signature); +} + +sub handle_POST { + my ($self) = @_; + + my $request = $self->{request}; + my $env = $request->{env}; + + my $req = $self->{req}; + + my $policy = $req->parameters->get('policy'); + my $signature = $req->parameters->get('signature'); + + unless ($signature && $policy) { + return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']]); + } + + unless ($signature eq encode_base64(hmac_sha1($policy, $self->{buckets}->{$request->{bucket}}->{secretkey}), '')) { + return respondXML(403, ['Error' => ['Code' => 'SignatureDoesNotMatch']]); + } + + my $json; + try { + $json = JSON::XS->new->relaxed->decode(decode_base64($policy)); + } catch { + return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']]); + }; + + return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument', 'Message' => 'No expiration time specified in policy document']]) unless (defined $json->{expiration}); + + my $expiration = Date::Parse::str2time($json->{expiration}); + + if ($self->{request}->{starttime} > $expiration) { + return respondXML(400, ['Error' => ['Code' => 'ExpiredToken']]); + } + # $self->log("Expires:".$expiration."; Starttime:".$self->{request}->{starttime}); + + foreach my $ref (@{$json->{conditions}}) { + if (ref $ref eq 'HASH') { + foreach my $key (keys %{$ref}) { + my $val = encode("utf8", $$ref{$key}); #TODO: better to decode parameter? Is unicode normalization required? + my $result = $self->check_policy('eq', $key, $val); + # $self->log($key."=".$val."(".$result.")"); + unless ($result) {return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']])}; + } + } + if (ref $ref eq 'ARRAY') { + my $key = $$ref[1]; + $key =~ s/^\$//; + my $val = encode("utf8", $$ref[2]); #TODO: better to decode parameter? Is unicode normalization required? + my $result = $self->check_policy($$ref[0], $key, $val); + # $self->log($key." ".$$ref[0]." ".$val." (".$result.")"); + unless ($result) {return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']])}; + } + } + + my $data = $req->parameters->get('file'); + unless ($data) { + return respondXML(400, ['Error' => ['Code' => 'IncorrectNumberOfFilesInPostRequest']]); + } + + my $md5 = md5_base64($data); + unless ($req->parameters->get('Content-MD5') eq $md5.'==') { + return respondXML(400, ['Error' => ['Code' => 'BadDigest']]) + } + + my $key = $req->parameters->get('key'); + my $store = $self->{buckets}->{$request->{bucket}}->{store}; + + my $meta = { + 'md5' => unpack('H*', decode_base64($md5)), + 'acl' => $self->{req}->parameters->get('acl') || 'private' + }; + + $store->store_file($key, $req->parameters->get('file'), JSON::XS->new->utf8->encode($meta)); + + my $status = $req->parameters->get('success_action_status'); + $status = '403' unless (($status eq '200') || ($status eq '201')); + + # TODO: access_action_redirect + + return respond($status, ''); +} + +sub handle_HEAD { + my ($self) = @_; + + my $request = $self->{request}; + my $env = $request->{env}; + my $key = $request->{key}; + + my $store = $self->{buckets}->{$request->{bucket}}->{store}; + + unless ($store->check_exists($key)) { + return respondXML(404, ['Error' => ['Code' => 'NoSuchKey']]); + } + + my $meta; + try { + $meta = JSON::XS->new->utf8->decode($store->retrieve_filemeta($key)); + }; + unless (ref($meta) eq 'HASH') { + $meta = {}; + } + + my $headers = ['Content-Length' => $store->get_size($key)]; + if ($meta->{type}) { + push @$headers, 'Content-Type'; + push @$headers, $meta->{type}; + } + if ($meta->{md5}) { + push @$headers, 'ETag'; + push @$headers, "\"".$meta->{md5}."\""; + } + + return [200, $headers, []]; +} + +sub handle_GET { + my ($self) = @_; + + my $request = $self->{request}; + my $env = $request->{env}; + + my $key = $request->{key}; + + my $store = $self->{buckets}->{$request->{bucket}}->{store}; + + unless($store->check_exists($key)){ + return respondXML(404, ['Error' => ['Code' => 'NoSuchKey']]); + } + + my $meta; + try { + $meta = JSON::XS->new->utf8->decode($store->retrieve_filemeta($key)); + }; + unless (ref($meta) eq 'HASH') { + $meta = {}; + } + + my $headers = ['Content-Length' => $store->get_size($key)]; + my $ct = $request->{uri}->query_param('response-content-type'); + $ct = $meta->{type} unless ($ct); + if ($ct) { + push @$headers, 'Content-Type'; + push @$headers, $ct; + } + if ($meta->{md5}) { + push @$headers, 'ETag'; + push @$headers, "\"".$meta->{md5}."\""; + } + + return [200, $headers, $store->retrieve_file($key)]; +} + + +sub handle_PUT { + my $self = shift; + + my $request = $self->{request}; + my $env = $request->{env}; + my $store = $self->{buckets}->{$request->{bucket}}->{store}; + + my $key = $request->{key}; + + my $cl = $env->{CONTENT_LENGTH}; + my $source = $env->{HTTP_X_AMZ_COPY_SOURCE}; + + if (($cl == 0) && ($source)) { + # Copy File + $source = uri_unescape($source); + (my $sourceBucket, my $sourceKey) = $source =~ m/^\/([^\?\/]*)\/?([^\?]*)/; + + # $self->log("Source: ".$sourceBucket."/bla/".$sourceKey."\nDestinationKey: ".$key."\n"); + + my $res = $store->link_files($sourceKey, $key); + + if ($res) { + + my $meta; + try { + $meta = JSON::XS->new->utf8->decode($store->retrieve_filemeta($key)); + }; + unless (ref($meta) eq 'HASH') { + $meta = {}; + } + + return respondXML(200, ['CopyObjectResult' => [ 'LastModified' => '2012', 'ETag' => $meta->{md5}]]); + } else { + return respondXML(500, ['Error' => ['Code' => 'InternalError']]); + } + } else { + # Normal PUT + my $input = $env->{'psgi.input'}; + my $cl = $env->{CONTENT_LENGTH}; + my $data; + + if (($input->read($data, $cl)) != $cl) { + return respondXML(400, ['Error' => ['Code' => 'IncompleteBody']]); + } + + my $md5 = md5_base64($data); + + my $meta = {}; + $meta->{type} = $env->{CONTENT_TYPE} if ($env->{CONTENT_TYPE}); + $meta->{acl} = $env->{HTTP_X_AMZ_ACL} || 'private'; + $meta->{md5} = unpack('H*', decode_base64($md5)); + + if ($env->{HTTP_CONTENT_MD5}) { + return respondXML(400, ['Error' => ['Code' => 'BadDigest']]) unless ($env->{HTTP_CONTENT_MD5} eq $md5.'=='); + } + + $store->store_file($key, $data, JSON::XS->new->utf8->encode($meta)); + + return respond(200, ''); + } +} + +sub handle_DELETE { + my $self = shift; + + my $request = $self->{request}; + my $env = $request->{env}; + my $store = $self->{buckets}->{$request->{bucket}}->{store}; + + + my $key = $request->{key}; + + unless ($store->check_exists($key)) { + return respondXML(404, ['Error' => ['Code' => 'NoSuchKey', 'Message' => 'The resource you requested does not exist', 'Resource' => $key]]); + } + + if ($store->delete_file($key)) { + return [204, [], []]; + } else { + return respondXML(500, ['Error' => ['Code' => 'InternalError']]); + } + +} + +sub request_uri { + my $env = shift; + + my $uri = ($env->{'psgi.url_scheme'} || "http") . + "://" . + ($env->{HTTP_HOST} || (($env->{SERVER_NAME} || "") . ":" . ($env->{SERVER_PORT} || 80))) . + ($env->{SCRIPT_NAME} || ""); + + return URI->new($uri . $env->{REQUEST_URI})->canonical(); +} + +sub handle { + my ($self, $env) = @_; + + my $request = {}; + + $request->{env} = $env; + $request->{starttime} = time(); + + $request->{uri} = request_uri($env); + + # split in bucket and key (currently only path style buckets no host style) + ($request->{bucket}, $request->{key_escaped}) = $env->{REQUEST_URI} =~ m/^\/([^\?\/]*)\/?([^\?]*)/; + $request->{key} = uri_unescape($request->{key_escaped}) || ''; + + return respond(200, "Nothing to see here") if ($request->{bucket} eq ''); + + if (not defined $self->{buckets}->{$request->{bucket}}) { + return respondXML(404, + ['Error' => + ['Code' => 'NoSuchBucket', + 'Message' => 'The specified bucket does not exist', + 'BucketName' => $request->{bucket}] + ]); + } + + $self->{request} = $request; + + # TODO: body parsing for POST. Parameter "file" should be saved as file instead of in memory + my $req = Plack::Request->new($env); + $self->{req} = $req; + + my @methods = qw(POST GET HEAD PUT DELETE); + + unless ($env->{REQUEST_METHOD} ~~ @methods) { + undef($self->{request}); + + return respondXML(405, + ['Error' => + ['Code' => 'MethodNotAllowed', + 'Message' => 'The specified method is not allowed'] + ]); + } + + my $result; + if ($env->{REQUEST_METHOD} eq 'POST') { + $result = $self->handle_POST(); + } else { + + unless ($self->check_signature()) { + undef($self->{request}); + return respondXML(403, ['Error' => ['Code' => 'SignatureDoesNotMatch']]); + } + + my $method = 'handle_'.$env->{REQUEST_METHOD}; + $result = $self->$method; + } + + undef($self->{request}); + + return $result; +}; + + +sub psgi_callback { + my $self = shift; + + sub { + $self->handle( shift ); + }; +} + +1; diff --git a/zss/ZSS/Store.pm b/zss/ZSS/Store.pm new file mode 100644 index 0000000..52cfd7f --- /dev/null +++ b/zss/ZSS/Store.pm @@ -0,0 +1,164 @@ +package ZSS::Store; + +use strict; +use warnings; + +use Digest::MD5 qw (md5_hex); +use File::Util qw(escape_filename); +use File::Path qw(make_path); + +sub new { + my $class = shift; + + # TODO: read from config + my $self = {storagepath => shift}; + + bless $self, $class; +} + +sub get_path { + my $self = shift; + my $key = shift; + + my $dirname = md5_hex($key); + + my $dir = $self->{storagepath} . substr($dirname, 0, 1) . "/" . $dirname ."/"; + + return $dir; +} + +sub get_filename { + my $self = shift; + my $key = shift; + + return escape_filename($key, '_'); +} + +sub get_filepath { + my $self = shift; + my $key = shift; + + return $self->get_path($key) . $self->get_filename($key); +} + +sub store_file { + my $self = shift; + my $key = shift; + my $data = shift; + my $meta = shift; + + my $dir = $self->get_path($key); + my $file = $self->get_filename($key); + + make_path($dir); + + # Write data to temp file and rename to the desired name + # This only changes this file and not other hardlinks + open(my $fh, '>:raw', $dir.$file.".temp"); + print $fh ($data); + close($fh); + rename($dir.$file.".temp", $dir.$file); + + if ($meta) { + open($fh, '>:raw', $dir.$file.".meta.temp"); + print $fh ($meta); + close($fh); + rename($dir.$file.".meta.temp", $dir.$file.".meta"); + } +} + +sub check_exists{ + my $self = shift; + my $key = shift; + + my $path = $self->get_filepath($key); + unless (-e $path){ + return 0; + } + return 1; +} + +sub retrieve_file { + my $self = shift; + my $key = shift; + + unless($self->check_exists($key)){ + return undef; + } + my $path = $self->get_filepath($key); + open(my $fh, '<:raw', $path); + return $fh; +} + +sub retrieve_filemeta { + my $self = shift; + my $key = shift; + + unless($self->check_exists($key)){ + return undef; + } + my $metafile = $self->get_filepath($key) . ".meta"; + + # check if metadata is present + unless (-e $metafile) { + return undef; + } + + # limt size of metadata to 8kB + my $size = -s $metafile; + unless ($size <= 8192) { + return undef; + } + + my $meta; + open(my $fh, '<:raw', $metafile); + read ($fh, $meta, $size); + return $meta; +} + +sub get_size{ + my $self = shift; + my $key = shift; + + my $path = $self->get_filepath($key); + + unless (-e $path) { + return 0; + } + my $size = -s $path; + return $size; +} + +sub link_files{ + my $self = shift; + my $source_key = shift; + my $destination_key = shift; + + my $source_path = $self->get_filepath($source_key); + my $destination_dir = $self->get_path($destination_key); + my $destination_path = $self->get_filepath($destination_key); + + make_path($destination_dir); + + link($source_path.".meta", $destination_path.".meta"); + + return link($source_path, $destination_path); +} + +sub delete_file{ + my $self = shift; + my $key = shift; + + my $dir = $self->get_path($key); + my $file = $self->get_filename($key); + + # Remove metadata + unlink($dir.$file.".meta"); + + unless (unlink($dir.$file)) { + return 1; + } + return rmdir($dir); +} + +1; diff --git a/zss/zss.psgi b/zss/zss.psgi new file mode 100644 index 0000000..1d15cbf --- /dev/null +++ b/zss/zss.psgi @@ -0,0 +1,11 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use lib ('/srv/zotero/zss/'); + +use ZSS; + +my $app = ZSS->new(); + +$app->psgi_callback(); diff --git a/zss/zss.yaml b/zss/zss.yaml new file mode 100644 index 0000000..f8dbc42 --- /dev/null +++ b/zss/zss.yaml @@ -0,0 +1,3 @@ +uwsgi: + plugin: psgi + psgi: /srv/zotero/zss/zss.psgi