initial commit

This commit is contained in:
Gabriele Facciolo
2016-08-24 01:34:02 +02:00
parent bb492657c2
commit 9f2d51be33
23 changed files with 1345 additions and 1 deletions

107
Dockerfile Normal file
View File

@@ -0,0 +1,107 @@
FROM debian:wheezy-backports
MAINTAINER Gabriele Facciolo <gfacciol@gmail.com>
# 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

View File

@@ -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/<username>/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)

30
apache/dot.htaccess Normal file
View File

@@ -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]

46
apache/sites-zotero.conf Normal file
View File

@@ -0,0 +1,46 @@
<VirtualHost *:80>
DocumentRoot /srv/web-library
<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>
<Directory /srv/web-library/>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
allow from all
</Directory>
</VirtualHost>
<VirtualHost *:443>
DocumentRoot /srv/zotero/dataserver/htdocs
SSLEngine on
SSLCertificateFile /etc/apache2/zotero.cert
SSLCertificateKeyFile /etc/apache2/zotero.key
<Location /zotero/>
SetHandler uwsgi-handler
uWSGISocket /var/run/uwsgi/app/zss/socket
uWSGImodifier1 5
</Location>
<Directory "/srv/zotero/dataserver/htdocs/">
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
</Directory>
ErrorLog /srv/zotero/log/error.log
CustomLog /srv/zotero/log/access.log common
</VirtualHost>

24
apache/zotero.cert Normal file
View File

@@ -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-----

41
apache/zotero.key Normal file
View File

@@ -0,0 +1,41 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

3
cert_override.txt Normal file
View File

@@ -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==

85
dataserver/config.inc.php Normal file
View File

@@ -0,0 +1,85 @@
<?
class Z_CONFIG {
public static $API_ENABLED = true;
public static $SYNC_ENABLED = true;
public static $PROCESSORS_ENABLED = true;
public static $MAINTENANCE_MESSAGE = 'Server updates in progress. Please try again in a few minutes.';
public static $TESTING_SITE = false;
public static $DEV_SITE = false;
public static $DEBUG_LOG = false;
public static $BASE_URI = 'http://zotero.org/';
public static $API_BASE_URI = 'https://localhost/';
public static $WWW_BASE_URI = '';
public static $SYNC_DOMAIN = 'sync';
public static $AUTH_SALT = 'sometext';
public static $API_SUPER_USERNAME = 'someusername';
public static $API_SUPER_PASSWORD = 'somepassword';
public static $AWS_ACCESS_KEY = '';
public static $AWS_SECRET_KEY = 'yoursecretkey';
public static $S3_BUCKET = 'zotero';
public static $S3_ENDPOINT = 'localhost';
public static $S3_USE_SSL = true;
public static $S3_VALIDATE_SSL = false;
public static $URI_PREFIX_DOMAIN_MAP = array(
'/sync/' => '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;
}
?>

View File

@@ -0,0 +1,45 @@
<?
function Zotero_dbConnectAuth($db) {
$charset = '';
if ($db == 'master') {
$host = 'localhost';
$port = 3306;
$db = 'zotero_master';
$user = 'zotero';
$pass = 'foobar';
}
else if ($db == 'shard') {
$host = false;
$port = false;
$db = false;
$user = 'zotero';
$pass = 'foobar';
}
else if ($db == 'id1') {
$host = 'localhost';
$port = 3306;
$db = 'zotero_ids';
$user = 'zotero';
$pass = 'foobar';
}
else if ($db == 'id2') {
$host = 'localhost';
$port = 3306;
$db = 'zotero_ids';
$user = 'zotero';
$pass = 'foobar';
}
else {
throw new Exception("Invalid db '$db'");
}
return array(
'host'=>$host,
'port'=>$port,
'db'=>$db,
'user'=>$user,
'pass'=>$pass,
'charset'=>$charset
);
}
?>

View File

@@ -0,0 +1,3 @@
#!/bin/sh
exec svlogd /srv/zotero/log/download

View File

@@ -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

View File

@@ -0,0 +1,3 @@
#!/bin/sh
exec svlogd /srv/zotero/log/error

5
dataserver/sv/zotero-error/run Executable file
View File

@@ -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

View File

@@ -0,0 +1,3 @@
#!/bin/sh
exec svlogd /srv/zotero/log/upload

View File

@@ -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

33
mysql/setup_db Executable file
View File

@@ -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

6
mysql/zotero.cnf Normal file
View File

@@ -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'

22
patches/add_user Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/php
<?
set_include_path("../include");
require("header.inc.php");
if (empty($argv[1]) || empty($argv[2]) || empty($argv[3])) {
die("Usage: $argv[0] " . '$userID $username $password' . "\n");
}
$userID = $argv[1];
$username = $argv[2];
$password = $argv[3];
$salt = Z_CONFIG::$AUTH_SALT;
echo "Adding new user $username with ID $userID\n";
Zotero_Users::add($userID, $username);
$hash = SHA1($salt . $password);
echo "$salt . $password $hash\n";
$sql = "update users set password=? where userid=?";
Zotero_DB::query($sql, array($hash, $userID));
?>

143
patches/uwsgi Executable file
View File

@@ -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 <command> <confname> <confname> ...
#
# 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: '<confname>'.
#
# 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 <leo.borisenko@gmail.com>
# 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

512
zss/ZSS.pm Normal file
View File

@@ -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 .= '</'.$token.'>';
}
return $msg;
}
sub respondXML {
my $code = shift;
my $xml = shift;
return [ $code, [ 'Content-Type' => 'application/xml'], ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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;

164
zss/ZSS/Store.pm Normal file
View File

@@ -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;

11
zss/zss.psgi Normal file
View File

@@ -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();

3
zss/zss.yaml Normal file
View File

@@ -0,0 +1,3 @@
uwsgi:
plugin: psgi
psgi: /srv/zotero/zss/zss.psgi