tmpfhJ2Ca/0000700000175000017500000000000013026234541011646 5ustar lunarlunartmpfhJ2Ca/TODO0000644000175000017500000000307012574355361012363 0ustar lunarlunarFuture plans ============ Here are a list of welcome changes to [Coquelicot]: * Implement optional client-side encryption Using the new HTML 5FileAPI, encryption and decryption of the files could be performed client side instead of server side. See the [up-crypt] proof of concept from hellais on how this could be done. * More flexible expiration It might be interesting to also offer a calendar for specifying an exact date after which the file will be unavailable. * Hide file size (padding) There is currently a real close mapping from original file size to stored file size. Original file size will also be recorded in server logs. Padding could be used to improve this situation. * Investigate more secure encryption algorithm Coquelicot currently uses AES-256-CBC. [AES is getting weaker] and [CBC mode is subject to Padding Oracle attacks]. * Make a usable Gem Most Ruby stuff is installed using Gem, so Coquelicot should also be installable that way. What is mostly missing is an easy way to create a default configuration and directories to hold uploaded files and temp. data. * Better support consecutive uploads Previous settings are lost when uploading several files in a row. This is clearly suboptimal user experience. [up-crypt]: https://github.com/hellais/up-crypt [Coquelicot]: https://coquelicot.potager.org/ [AES is getting weaker]: https://www.schneier.com/blog/archives/2009/07/another_new_aes.html [CBC mode is subject to Padding Oracle attacks]: http://www.limited-entropy.com/padding-oracle-attacks tmpfhJ2Ca/coquelicot.gemspec0000644000175000017500000000611013026223065015371 0ustar lunarlunar# Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . $:.unshift File.expand_path("../lib", __FILE__) require "coquelicot/version" Gem::Specification.new do |s| s.name = 'coquelicot' s.version = Coquelicot::VERSION s.license = 'AGPL-3.0' s.authors = ['potager.org', 'mh / immerda.ch'] s.email = ['jardiniers@potager.org'] s.homepage = 'https://coquelicot.potager.org/' s.summary = %q{"one-click" file sharing web application focusing on privacy} s.description = <<-DESCRIPTION.gsub(/^ */, '') Coquelicot is a "one-click" file sharing web application with a focus on protecting users' privacy. Basic principle: users can upload a file to the server, in return they get a unique URL which can be shared with others in order to download the file. Coquelicot aims to protect, to some extent, users and system administrators from disclosure of the files exchanged from passive and not so active attackers. DESCRIPTION s.files = `git ls-files`.split("\n"). select { |p| !['.gitignore', '.placeholder', 'coquelicot.git'].include?(File.basename(p)) } s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ['lib'] s.add_development_dependency 'rake' s.add_development_dependency 'rspec', '~>3' s.add_development_dependency 'timecop' s.add_development_dependency 'rack-test' s.add_development_dependency 'capybara' s.add_development_dependency 'cucumber' s.add_development_dependency 'activesupport' s.add_development_dependency 'tzinfo' s.add_development_dependency 'net-ldap' s.add_development_dependency 'gettext', '~>3' s.add_development_dependency 'bcrypt' s.add_runtime_dependency 'sinatra', '~>1.4' s.add_runtime_dependency 'sinatra-contrib', '~>1.4' s.add_runtime_dependency 'rack', '>=1.1', '<2' s.add_runtime_dependency 'haml', '~>4' s.add_runtime_dependency 'haml-magic-translations' s.add_runtime_dependency 'sass' s.add_runtime_dependency 'maruku' s.add_runtime_dependency 'fast_gettext' s.add_runtime_dependency 'lockfile', '~>2' s.add_runtime_dependency 'json' s.add_runtime_dependency 'rainbows' s.add_runtime_dependency 'multipart-parser' s.add_runtime_dependency 'upr' s.add_runtime_dependency 'moneta', '>=0.7', '<2' end tmpfhJ2Ca/po/0000755000175000017500000000000013026234541012276 5ustar lunarlunartmpfhJ2Ca/po/fr/0000755000175000017500000000000013026234541012705 5ustar lunarlunartmpfhJ2Ca/po/fr/coquelicot.po0000644000175000017500000002052213026223065015414 0ustar lunarlunar# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: coquelicot 0.9.6\n" "PO-Revision-Date: 2016-12-20 12:58+0100\n" "Last-Translator: potager.org \n" "Language-Team: potager.org \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/tebibyte #: lib/coquelicot/num.rb:26 msgid "TiB" msgstr "Tio" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/gibibyte #: lib/coquelicot/num.rb:28 msgid "GiB" msgstr "Gio" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/mebibyte #: lib/coquelicot/num.rb:30 msgid "MiB" msgstr "Mio" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/kibibyte #: lib/coquelicot/num.rb:32 views/layout.haml:41 msgid "KiB" msgstr "Kio" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/byte #: lib/coquelicot/num.rb:34 msgid "B" msgstr "o" #: lib/coquelicot/rack/upload.rb:191 msgid "" "File is bigger than maximum allowed size: %s would exceed the maximum " "allowed %s." msgstr "" "Le fichier est plus gros que la taille maximale autorisée : %s dépasse le " "maximum autorisé (%s)." #: lib/coquelicot/rack/upload.rb:194 msgid "File is bigger than maximum allowed size %s." msgstr "Le fichier est plus gros que la taille maximale autorisée (%s)." #: lib/coquelicot/rack/upload.rb:201 msgid "File has no content" msgstr "Le fichier est vide" #: views/about_your_data.haml:18 views/layout.haml:53 msgid "About your data…" msgstr "À propos de vos données…" #: views/about_your_data.haml:20 msgid "" "Welcome to *Coquelicot*. A simple way to share files with people you know,\n" "with a little bit of privacy." msgstr "" "Bienvenue sur *Coquelicot*. Un moyen simple de partager des fichiers avec\n" "des personnes que vous connaissez, cela avec un peu d'intimité." #: views/about_your_data.haml:23 msgid "What should I expect from “a little bit of privacy”?" msgstr "Que puis-je attendre d'« un peu d'intimité » ?" #: views/about_your_data.haml:25 msgid "Exchanges between your computer and %s are encrypted." msgstr "Les échanges entre votre ordinateur et %s sont chiffrés." #: views/about_your_data.haml:26 msgid "" "An attacker in-between will be able to see how much data is exchanged,\n" "but not its nature." msgstr "" "Un attaquant entre les deux pourra voir quelle quantité de données est\n" "échangée, mais pas leur nature." #: views/about_your_data.haml:29 msgid "" "Files are stored encrypted. In case someone gets access to the server\n" "storage, they will know the size, arrival and expiration dates of the\n" "files; but they will not be able to get their content without the\n" "password.\n" "\n" "In case no *download password* has been specified, the password might\n" "be kept in the server request logs. This means that the server might\n" "store enough information to retrieve the actual file content.\n" "\n" "When a *download password* has been specified, the password will not be\n" "stored anywhere on the server. This will prevent retrieval of the\n" "file content, except if the server has been actively compromised\n" "beforehand." msgstr "" "Les fichiers sont enregistrés sous forme chiffrée. Au cas où une personne\n" "obtiendrait l'accès aux fichiers du serveur, elle saura la taille, la date\n" "d'arrivée et d'expiration des fichiers ; mais elle ne pourra pas recupérer\n" "leur contenu sans mot de passe.\n" "\n" "Au cas où aucun *mot de passe de téléchargement* n'a été indiqué, le mot\n" "de passe peut être noté dans les journaux du serveur. Cela veut dire que\n" "le serveur peut garder suffisamment d'informations pour permettre de\n" "récupérer le contenu du fichier.\n" "\n" "Si un *mot de passe de téléchargement* a été indiqué, le mot de passe\n" "ne sera enregistré nul part sur le serveur. Cela devrait empêcher de\n" "récupérer le contenu du fichier, sauf si le serveur a été activement\n" "compromis au préalable." #: views/about_your_data.haml:43 msgid "What if I don't trust the server admins?" msgstr "Et si je ne fais pas confiance aux admin. du serveur ?" #: views/about_your_data.haml:44 msgid "" "You are [free](http://www.gnu.org/licenses/agpl.txt) to install Coquelicot\n" "on your own system. Please refer to the [README](README) if you wish to\n" "know how." msgstr "" "Vous êtes [libre](http://www.gnu.org/licenses/agpl.txt) d'installer\n" "Coquelicot sur votre propre système. Veuillez vous référer au\n" "[fichier README](README) pour savoir comment faire." #: views/auth/imap.haml:20 msgid "E-mail User:" msgstr "Compte email :" #: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22 msgid "Password:" msgstr "Mot de passe :" #: views/auth/ldap.haml:20 msgid "LDAP User:" msgstr "Compte LDAP :" #: views/auth/simplepass.haml:19 views/auth/userpass.haml:23 msgid "Upload password:" msgstr "Mot de passe pour envoyer :" #: views/auth/userpass.haml:20 msgid "Upload user:" msgstr "Compte pour envoyer :" #: views/download_in_progress.haml:1 msgid "Download in progress" msgstr "Téléchargement en cours" #: views/download_in_progress.haml:2 msgid "The requested file is currently being downloaded by another client." msgstr "" "Le fichier demandé est actuellement en train d'être téléchargé par un autre " "client." #: views/enter_file_key.haml:18 msgid "Enter download password…" msgstr "Entrer le passe de téléchargement…" #: views/error.haml:1 msgid "Error" msgstr "Erreur :" #: views/error.haml:2 msgid "Something bad happened: %s" msgstr "Quelque chose de grave est arrivé : %s" #: views/expired.haml:18 msgid "Too late…" msgstr "Trop tard…" #: views/expired.haml:20 msgid "Sorry, file has expired." msgstr "Désolé, le fichier a expiré." #: views/forbidden.haml:1 msgid "Forbidden" msgstr "Interdit" #: views/forbidden.haml:2 msgid "This password does not allow access to this resource." msgstr "Ce mot de passe ne permet pas d'accéder à cette ressource." #: views/index.haml:22 msgid "Share a file!" msgstr "Partager un fichier !" #: views/index.haml:32 msgid "Available for:" msgstr "Disponible pendant :" #: views/index.haml:34 msgid "1 day" msgstr "1 jour" #: views/index.haml:34 msgid "1 hour" msgstr "1 heure" #: views/index.haml:34 msgid "1 month" msgstr "1 mois" #: views/index.haml:34 msgid "1 week" msgstr "1 semaine" #: views/index.haml:43 msgid "Unlimited downloads until expiration" msgstr "Téléchargements illimités avant expiration" #: views/index.haml:46 msgid "Remove after one download" msgstr "Effacer après un téléchargement" #: views/index.haml:48 msgid "Download password (optional):" msgstr "Mot de passe pour le téléchargement (optionnel) :" #: views/index.haml:51 msgid "File (max. size: %s):" msgstr "Fichier (taille max. : %s) :" #: views/index.haml:55 msgid "Share!" msgstr "Partager !" #: views/layout.haml:23 msgid "Coquelicot" msgstr "Coquelicot" #: views/layout.haml:34 msgid "Generate random" msgstr "Générer aléatoirement" #: views/layout.haml:35 msgid "Generating…" msgstr "Génération…" #: views/layout.haml:36 msgid "Don't forget to write it down!" msgstr "N'oubliez pas de le noter !" #: views/layout.haml:37 msgid "Please try again!" msgstr "Allez, essaye encore !" #: views/layout.haml:38 msgid "Error:" msgstr "Erreur :" #: views/layout.haml:39 msgid "Upload starting..." msgstr "Démarrage de l’envoi..." #: views/layout.haml:40 msgid "Uploading: " msgstr "Envoi :" #: views/not_found.haml:1 msgid "Not found" msgstr "Introuvable" #: views/not_found.haml:2 msgid "The requested URL %s was not found on this server." msgstr "L'URL %s demandé est introuvable sur ce serveur." #: views/ready.haml:18 msgid "Share this!" msgstr "À transmettre !" #: views/ready.haml:23 msgid "A password is required to download this file." msgstr "Un mot de passe est nécessaire pour télécharger ce fichier." #: views/ready.haml:24 msgid "The file will be available until %s." msgstr "Ce fichier sera accessible jusqu'au %s." #: views/ready.haml:26 msgid "Share another file…" msgstr "Partager un autre fichier…" tmpfhJ2Ca/po/el/0000755000175000017500000000000013026234541012676 5ustar lunarlunartmpfhJ2Ca/po/el/coquelicot.po0000644000175000017500000002460113026223065015407 0ustar lunarlunar# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: coquelicot 0.9.6\n" "PO-Revision-Date: 2016-12-20 13:02+0100\n" "Last-Translator: Rowan Thorpe \n" "Language-Team: potager.org \n" "Language: el\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/tebibyte #: lib/coquelicot/num.rb:26 msgid "TiB" msgstr "TiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/gibibyte #: lib/coquelicot/num.rb:28 msgid "GiB" msgstr "GiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/mebibyte #: lib/coquelicot/num.rb:30 msgid "MiB" msgstr "MiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/kibibyte #: lib/coquelicot/num.rb:32 views/layout.haml:41 msgid "KiB" msgstr "KiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/byte #: lib/coquelicot/num.rb:34 msgid "B" msgstr "B" #: lib/coquelicot/rack/upload.rb:191 msgid "" "File is bigger than maximum allowed size: %s would exceed the maximum " "allowed %s." msgstr "" "Το αρχείο είναι μεγαλύτερο από το μέγιστο επιτρεπόμενο μέγεθος: %s " "υπερβαίνει το μέγιστο επιτρεπόμενο %s." #: lib/coquelicot/rack/upload.rb:194 msgid "File is bigger than maximum allowed size %s." msgstr "Το αρχείο είναι μεγαλύτερο από το μέγιστο επιτρεπόμενο μέγεθος %s." #: lib/coquelicot/rack/upload.rb:201 msgid "File has no content" msgstr "Το αρχείο δεν έχει περιεχόμενο" #: views/about_your_data.haml:18 views/layout.haml:53 msgid "About your data…" msgstr "Σχετικά με τα δεδομένα σας…" #: views/about_your_data.haml:20 msgid "" "Welcome to *Coquelicot*. A simple way to share files with people you know,\n" "with a little bit of privacy." msgstr "" "Καλώς ήρθατε στο *Coquelicot*. Ένας απλός τρόπος για να μοιραστείτε τα\n" "αρχεία με τους ανθρώπους που γνωρίζετε, διατηρώντας λίγη από την " "ιδιωτικότητά σας.\n" #: views/about_your_data.haml:23 msgid "What should I expect from “a little bit of privacy”?" msgstr "Τι να περιμένω από το «διατηρώντας λίγη από την ιδιωτικότητά σας»;" #: views/about_your_data.haml:25 msgid "Exchanges between your computer and %s are encrypted." msgstr "" "Οι ανταλλαγές μεταξύ του υπολογιστή σας και του %s είναι κρυπτογραφημένες." #: views/about_your_data.haml:26 msgid "" "An attacker in-between will be able to see how much data is exchanged,\n" "but not its nature." msgstr "" "Ένας εισβολέας στο ενδιάμεσο θα είναι σε θέση να δεί πόσα δεδομένα " "ανταλλάσσονται,\n" "αλλά όχι τη φύση τους." #: views/about_your_data.haml:29 msgid "" "Files are stored encrypted. In case someone gets access to the server\n" "storage, they will know the size, arrival and expiration dates of the\n" "files; but they will not be able to get their content without the\n" "password.\n" "\n" "In case no *download password* has been specified, the password might\n" "be kept in the server request logs. This means that the server might\n" "store enough information to retrieve the actual file content.\n" "\n" "When a *download password* has been specified, the password will not be\n" "stored anywhere on the server. This will prevent retrieval of the\n" "file content, except if the server has been actively compromised\n" "beforehand." msgstr "" "Τα αρχεία αποθηκεύονται κρυπτογραφημένα. Σε περίπτωση που κάποιος\n" "αποκτήσει πρόσβαση στην αποθήκη του διακομιστή, θα ξέρει\n" "το μέγεθος, την ημερομηνία άφιξης και λήξης των αρχείων - αλλά δεν θα είναι " "σε θέση να\n" "πάρει το περιεχόμενό τους χωρίς τον κωδικό πρόσβασης.\n" "\n" "Σε περίπτωση που δεν έχει καθοριστεί κανένας *κωδικός λήψης*, ο κωδικός\n" "πρόσβασης μπορεί να διατηρηθεί στα αρχεία αιτημάτων καταγραφής του " "διακομιστή.\n" "Αυτό σημαίνει ότι ο διακομιστής μπορεί να αποθηκεύσει αρκετές πληροφορίες\n" "για να ανακτήθεί το πραγματικό περιεχόμενο του αρχείου.\n" "\n" "Όταν έχει οριστεί ένας *κωδικός λήψης*, ο κωδικός πρόσβασης δεν θα\n" "αποθηκευτεί πουθενά στο διακομιστή. Αυτό θα αποτρέψει την ανάκτηση του\n" "περιεχομένου του αρχείου, εκτός εάν ο διακομιστής έχει προηγουμένως τεθεί σε " "κίνδυνο.\n" #: views/about_your_data.haml:43 msgid "What if I don't trust the server admins?" msgstr "" "Τι μπορώ να κάνω εάν δεν εμπιστεύομαι τους διαχειριστές του διακομιστή;" #: views/about_your_data.haml:44 msgid "" "You are [free](http://www.gnu.org/licenses/agpl.txt) to install Coquelicot\n" "on your own system. Please refer to the [README](README) if you wish to\n" "know how." msgstr "" "Είστε [ελεύθεροι](http://www.gnu.org/licenses/agpl.txt) να εγκαταστήσετε\n" "Coquelicot στο δικό σας σύστημα. Παρακαλείσθε να συμβουλευθείτε το\n" "[README](README) αν θέλετε να μάθετε το πώς." #: views/auth/imap.haml:20 msgid "E-mail User:" msgstr "Χρήστης email:" #: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22 msgid "Password:" msgstr "Κωδικός πρόσβασης:" #: views/auth/ldap.haml:20 msgid "LDAP User:" msgstr "Χρήστης LDAP:" #: views/auth/simplepass.haml:19 views/auth/userpass.haml:23 msgid "Upload password:" msgstr "Κωδικός πρόσβασης για μεταφόρτωση:" #: views/auth/userpass.haml:20 msgid "Upload user:" msgstr "Χρήστης για μεταφόρτωση:" #: views/download_in_progress.haml:1 msgid "Download in progress" msgstr "Λήψη σε εξέλιξη" #: views/download_in_progress.haml:2 msgid "The requested file is currently being downloaded by another client." msgstr "Το αρχείο που ζητήθηκε αυτή τη στιγμή λαμβάνεται από άλλο πελάτη." #: views/enter_file_key.haml:18 msgid "Enter download password…" msgstr "Εισάγετε τον κωδικό λήψης…" #: views/error.haml:1 msgid "Error" msgstr "Σφάλμα:" #: views/error.haml:2 msgid "Something bad happened: %s" msgstr "Κάτι κακό συνέβη: %s" #: views/expired.haml:18 msgid "Too late…" msgstr "Πολύ αργά…" #: views/expired.haml:20 msgid "Sorry, file has expired." msgstr "Συγγνώμη, το αρχείο έχει λήξει." #: views/forbidden.haml:1 msgid "Forbidden" msgstr "Απαγορευμένο" #: views/forbidden.haml:2 msgid "This password does not allow access to this resource." msgstr "" "Αυτός ο κωδικός πρόσβασης δεν επιτρέπει την πρόσβαση σε αυτόν τον πόρο." #: views/index.haml:22 msgid "Share a file!" msgstr "Μοιραστείτε ένα αρχείο!" #: views/index.haml:32 msgid "Available for:" msgstr "Διαθέσιμο για:" #: views/index.haml:34 msgid "1 day" msgstr "1 ημέρα" #: views/index.haml:34 msgid "1 hour" msgstr "1 ώρα" #: views/index.haml:34 msgid "1 month" msgstr "1 μήνα" #: views/index.haml:34 msgid "1 week" msgstr "1 εβδομάδα" #: views/index.haml:43 msgid "Unlimited downloads until expiration" msgstr "Απεριόριστες λήψεις μέχρι τη λήξη" #: views/index.haml:46 msgid "Remove after one download" msgstr "Αφαιρέστε μετά από μια λήψη" #: views/index.haml:48 msgid "Download password (optional):" msgstr "Κωδικός πρόσβασης για λήψη (προαιρετικό):" #: views/index.haml:51 msgid "File (max. size: %s):" msgstr "Αρχείο (Μέγ. μέγεθος: %s):" #: views/index.haml:55 msgid "Share!" msgstr "Μοιραστείτε!" #: views/layout.haml:23 msgid "Coquelicot" msgstr "Coquelicot" #: views/layout.haml:34 msgid "Generate random" msgstr "Δημιουργήστε τυχαίο" #: views/layout.haml:35 msgid "Generating…" msgstr "Δημιουργία…" #: views/layout.haml:36 msgid "Don't forget to write it down!" msgstr "Μην ξεχάσετε να το σημειώσετε!" #: views/layout.haml:37 msgid "Please try again!" msgstr "Παπακαλώ προσπαθήστε ξανα!" #: views/layout.haml:38 msgid "Error:" msgstr "Σφάλμα:" #: views/layout.haml:39 msgid "Upload starting..." msgstr "Η μεταφόρτωση έχει αρχίσει..." #: views/layout.haml:40 msgid "Uploading: " msgstr "Μεταφόρτωση: " #: views/not_found.haml:1 msgid "Not found" msgstr "Δε βρέθηκε" #: views/not_found.haml:2 msgid "The requested URL %s was not found on this server." msgstr "Η ζητούμενη διεύθυνση URL %s δε βρέθηκε σε αυτόν το διακομιστή." #: views/ready.haml:18 msgid "Share this!" msgstr "Μοιραστείτε αυτό!" #: views/ready.haml:23 msgid "A password is required to download this file." msgstr "Απαιτείται κωδικός πρόσβασης για να κατεβάσετε το αρχείο αυτό." #: views/ready.haml:24 msgid "The file will be available until %s." msgstr "Το αρχείο θα είναι διαθέσιμο μέχρι %s." #: views/ready.haml:26 msgid "Share another file…" msgstr "Μοιραστείτε ένα άλλο αρχείο…" tmpfhJ2Ca/po/de/0000755000175000017500000000000013026234541012666 5ustar lunarlunartmpfhJ2Ca/po/de/coquelicot.po0000644000175000017500000002011213026223065015370 0ustar lunarlunar# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: coquelicot 0.9.6\n" "PO-Revision-Date: 2016-12-20 12:58+0100\n" "Last-Translator: potager.org \n" "Language-Team: potager.org \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/tebibyte #: lib/coquelicot/num.rb:26 msgid "TiB" msgstr "TiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/gibibyte #: lib/coquelicot/num.rb:28 msgid "GiB" msgstr "GiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/mebibyte #: lib/coquelicot/num.rb:30 msgid "MiB" msgstr "MiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/kibibyte #: lib/coquelicot/num.rb:32 views/layout.haml:41 msgid "KiB" msgstr "KiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/byte #: lib/coquelicot/num.rb:34 msgid "B" msgstr "B" #: lib/coquelicot/rack/upload.rb:191 msgid "" "File is bigger than maximum allowed size: %s would exceed the maximum " "allowed %s." msgstr "" "Die Datei ist größer als die maximal erlaubte Größe: %s würde die\n" "maximal erlaubten %s überschreiten." #: lib/coquelicot/rack/upload.rb:194 msgid "File is bigger than maximum allowed size %s." msgstr "Die Datei ist größer als die maximal erlaubte Größe %s" #: lib/coquelicot/rack/upload.rb:201 msgid "File has no content" msgstr "Die Datei hat keinen Inhalt" #: views/about_your_data.haml:18 views/layout.haml:53 msgid "About your data…" msgstr "Über deine Daten..." #: views/about_your_data.haml:20 msgid "" "Welcome to *Coquelicot*. A simple way to share files with people you know,\n" "with a little bit of privacy." msgstr "" "Willkommen bei *Coquelicot*. Ein einfacher Weg um Dateien mit Freunden zu " "teilen,\n" "mit ein bisschen Privatsphäre." #: views/about_your_data.haml:23 msgid "What should I expect from “a little bit of privacy”?" msgstr "Was sollte ich von “ein bisschen Privatsphäre” erwarten?" #: views/about_your_data.haml:25 msgid "Exchanges between your computer and %s are encrypted." msgstr "Der Austausch zwischen deinem Computer und %s sind verschlüsselt." #: views/about_your_data.haml:26 msgid "" "An attacker in-between will be able to see how much data is exchanged,\n" "but not its nature." msgstr "" "Ein zwischengeschalteter Angreifer wird sehen können, wieviel Daten " "übermittelt werden, aber nicht welcher Art." #: views/about_your_data.haml:29 msgid "" "Files are stored encrypted. In case someone gets access to the server\n" "storage, they will know the size, arrival and expiration dates of the\n" "files; but they will not be able to get their content without the\n" "password.\n" "\n" "In case no *download password* has been specified, the password might\n" "be kept in the server request logs. This means that the server might\n" "store enough information to retrieve the actual file content.\n" "\n" "When a *download password* has been specified, the password will not be\n" "stored anywhere on the server. This will prevent retrieval of the\n" "file content, except if the server has been actively compromised\n" "beforehand." msgstr "" "Dateien werden verschlüsselt gespeichert. Falls jemand Zugriff auf den " "Server\n" "erlangt, werden sie die Größe, Ankunfts- und Ablaufdaten der Files kennen,\n" " aber sie werden den Inhalt ohne das Passwort nicht sehen können.\n" "\n" "Falls kein *Download-Passwort* angegeben wurde, könnte das Passwort in\n" "den Server-Logs gehalten werden. Das bedeutet, der Server könnte genug\n" "Informationen vorhalten um an den Datei-Inhalt zu gelangen.\n" "\n" "Falls ein *Download-Passwort* angegeben wurde, wird es nirgendwo\n" "gespeichert werden. Das wird die Abfrage des Datei-Inhalts verhindern,\n" "es sei denn der Server wurde vorher aktiv kompromitiert." #: views/about_your_data.haml:43 msgid "What if I don't trust the server admins?" msgstr "Was is, wenn ich den Server-Admins nicht vertraue?" #: views/about_your_data.haml:44 msgid "" "You are [free](http://www.gnu.org/licenses/agpl.txt) to install Coquelicot\n" "on your own system. Please refer to the [README](README) if you wish to\n" "know how." msgstr "" "Du bist [frei](http://www.gnu.org/licenses/agpl.txt) Coquelicot auf deinem\n" "Computer zu installieren. Bitte verwende das [README](README), wenn du " "wissen\n" "möchtest wie." #: views/auth/imap.haml:20 msgid "E-mail User:" msgstr "Email User:" #: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22 msgid "Password:" msgstr "Passwort:" #: views/auth/ldap.haml:20 msgid "LDAP User:" msgstr "LDAP User:" #: views/auth/simplepass.haml:19 views/auth/userpass.haml:23 msgid "Upload password:" msgstr "Upload Passwort:" #: views/auth/userpass.haml:20 msgid "Upload user:" msgstr "Upload user:" #: views/download_in_progress.haml:1 msgid "Download in progress" msgstr "Download läuft" #: views/download_in_progress.haml:2 msgid "The requested file is currently being downloaded by another client." msgstr "" "Die angeforderte Datei wird gerade von einem anderen Client heruntergeladen." #: views/enter_file_key.haml:18 msgid "Enter download password…" msgstr "Gib das Download-Passwort ein…" #: views/error.haml:1 msgid "Error" msgstr "Fehler:" #: views/error.haml:2 msgid "Something bad happened: %s" msgstr "Etwas schlechtes ist passiert: %s" #: views/expired.haml:18 msgid "Too late…" msgstr "Zu spät…" #: views/expired.haml:20 msgid "Sorry, file has expired." msgstr "Entschuldigung, die Datei ist nicht mehr verfügbar." #: views/forbidden.haml:1 msgid "Forbidden" msgstr "Verboten" #: views/forbidden.haml:2 msgid "This password does not allow access to this resource." msgstr "Dieses Passwort erlaubt keinen Zugang zu dieser Ressource." #: views/index.haml:22 msgid "Share a file!" msgstr "Verteile eine Datei!" #: views/index.haml:32 msgid "Available for:" msgstr "Verfügbar für:" #: views/index.haml:34 msgid "1 day" msgstr "1 Tag" #: views/index.haml:34 msgid "1 hour" msgstr "1 Stunde" #: views/index.haml:34 msgid "1 month" msgstr "1 Monat" #: views/index.haml:34 msgid "1 week" msgstr "1 Woche" #: views/index.haml:43 msgid "Unlimited downloads until expiration" msgstr "Unbeschränkte Downloads bis zum Ablaufdatum" #: views/index.haml:46 msgid "Remove after one download" msgstr "Nach einem Download löschen" #: views/index.haml:48 msgid "Download password (optional):" msgstr "Download-Passwort (optional):" #: views/index.haml:51 msgid "File (max. size: %s):" msgstr "Datei (max. Größe: %s):" #: views/index.haml:55 msgid "Share!" msgstr "Verteile!" #: views/layout.haml:23 msgid "Coquelicot" msgstr "Coquelicot" #: views/layout.haml:34 msgid "Generate random" msgstr "Generiere ein zufälliges" #: views/layout.haml:35 msgid "Generating…" msgstr "Generiere…" #: views/layout.haml:36 msgid "Don't forget to write it down!" msgstr "Vergiss nicht es aufzuschreiben!" #: views/layout.haml:37 msgid "Please try again!" msgstr "Bitte versuch's noch einmal!" #: views/layout.haml:38 msgid "Error:" msgstr "Fehler:" #: views/layout.haml:39 msgid "Upload starting..." msgstr "Upload startet..." #: views/layout.haml:40 msgid "Uploading: " msgstr "Lade hoch: " #: views/not_found.haml:1 msgid "Not found" msgstr "Nit gefunden" #: views/not_found.haml:2 msgid "The requested URL %s was not found on this server." msgstr "Die angeforderte URL %s wurde nicht auf dem Server gefunden." #: views/ready.haml:18 msgid "Share this!" msgstr "Verteile dies!" #: views/ready.haml:23 msgid "A password is required to download this file." msgstr "Ein Passwort ist notwendig, um diese Datei herunter zu laden." #: views/ready.haml:24 msgid "The file will be available until %s." msgstr "Diese Datei wird verfügbar sein bis %s" #: views/ready.haml:26 msgid "Share another file…" msgstr "Verteile eine weitere Datei…" tmpfhJ2Ca/po/coquelicot.pot0000644000175000017500000001350013026223065015167 0ustar lunarlunar# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the coquelicot package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: coquelicot 0.9.6\n" "Report-Msgid-Bugs-To: Coquelicot developers \n" "POT-Creation-Date: 2016-12-20 12:58+0100\n" "PO-Revision-Date: 2016-12-20 12:58+0100\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/tebibyte #: lib/coquelicot/num.rb:26 msgid "TiB" msgstr "" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/gibibyte #: lib/coquelicot/num.rb:28 msgid "GiB" msgstr "" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/mebibyte #: lib/coquelicot/num.rb:30 msgid "MiB" msgstr "" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/kibibyte #: lib/coquelicot/num.rb:32 views/layout.haml:41 msgid "KiB" msgstr "" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/byte #: lib/coquelicot/num.rb:34 msgid "B" msgstr "" #: lib/coquelicot/rack/upload.rb:191 msgid "" "File is bigger than maximum allowed size: %s would exceed the maximum " "allowed %s." msgstr "" #: lib/coquelicot/rack/upload.rb:194 msgid "File is bigger than maximum allowed size %s." msgstr "" #: lib/coquelicot/rack/upload.rb:201 msgid "File has no content" msgstr "" #: views/about_your_data.haml:18 views/layout.haml:53 msgid "About your data…" msgstr "" #: views/about_your_data.haml:20 msgid "" "Welcome to *Coquelicot*. A simple way to share files with people you know,\n" "with a little bit of privacy." msgstr "" #: views/about_your_data.haml:23 msgid "What should I expect from “a little bit of privacy”?" msgstr "" #: views/about_your_data.haml:25 msgid "Exchanges between your computer and %s are encrypted." msgstr "" #: views/about_your_data.haml:26 msgid "" "An attacker in-between will be able to see how much data is exchanged,\n" "but not its nature." msgstr "" #: views/about_your_data.haml:29 msgid "" "Files are stored encrypted. In case someone gets access to the server\n" "storage, they will know the size, arrival and expiration dates of the\n" "files; but they will not be able to get their content without the\n" "password.\n" "\n" "In case no *download password* has been specified, the password might\n" "be kept in the server request logs. This means that the server might\n" "store enough information to retrieve the actual file content.\n" "\n" "When a *download password* has been specified, the password will not be\n" "stored anywhere on the server. This will prevent retrieval of the\n" "file content, except if the server has been actively compromised\n" "beforehand." msgstr "" #: views/about_your_data.haml:43 msgid "What if I don't trust the server admins?" msgstr "" #: views/about_your_data.haml:44 msgid "" "You are [free](http://www.gnu.org/licenses/agpl.txt) to install Coquelicot\n" "on your own system. Please refer to the [README](README) if you wish to\n" "know how." msgstr "" #: views/auth/imap.haml:20 msgid "E-mail User:" msgstr "" #: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22 msgid "Password:" msgstr "" #: views/auth/ldap.haml:20 msgid "LDAP User:" msgstr "" #: views/auth/simplepass.haml:19 views/auth/userpass.haml:23 msgid "Upload password:" msgstr "" #: views/auth/userpass.haml:20 msgid "Upload user:" msgstr "" #: views/download_in_progress.haml:1 msgid "Download in progress" msgstr "" #: views/download_in_progress.haml:2 msgid "The requested file is currently being downloaded by another client." msgstr "" #: views/enter_file_key.haml:18 msgid "Enter download password…" msgstr "" #: views/error.haml:1 msgid "Error" msgstr "" #: views/error.haml:2 msgid "Something bad happened: %s" msgstr "" #: views/expired.haml:18 msgid "Too late…" msgstr "" #: views/expired.haml:20 msgid "Sorry, file has expired." msgstr "" #: views/forbidden.haml:1 msgid "Forbidden" msgstr "" #: views/forbidden.haml:2 msgid "This password does not allow access to this resource." msgstr "" #: views/index.haml:22 msgid "Share a file!" msgstr "" #: views/index.haml:32 msgid "Available for:" msgstr "" #: views/index.haml:34 msgid "1 day" msgstr "" #: views/index.haml:34 msgid "1 hour" msgstr "" #: views/index.haml:34 msgid "1 month" msgstr "" #: views/index.haml:34 msgid "1 week" msgstr "" #: views/index.haml:43 msgid "Unlimited downloads until expiration" msgstr "" #: views/index.haml:46 msgid "Remove after one download" msgstr "" #: views/index.haml:48 msgid "Download password (optional):" msgstr "" #: views/index.haml:51 msgid "File (max. size: %s):" msgstr "" #: views/index.haml:55 msgid "Share!" msgstr "" #: views/layout.haml:23 msgid "Coquelicot" msgstr "" #: views/layout.haml:34 msgid "Generate random" msgstr "" #: views/layout.haml:35 msgid "Generating…" msgstr "" #: views/layout.haml:36 msgid "Don't forget to write it down!" msgstr "" #: views/layout.haml:37 msgid "Please try again!" msgstr "" #: views/layout.haml:38 msgid "Error:" msgstr "" #: views/layout.haml:39 msgid "Upload starting..." msgstr "" #: views/layout.haml:40 msgid "Uploading: " msgstr "" #: views/not_found.haml:1 msgid "Not found" msgstr "" #: views/not_found.haml:2 msgid "The requested URL %s was not found on this server." msgstr "" #: views/ready.haml:18 msgid "Share this!" msgstr "" #: views/ready.haml:23 msgid "A password is required to download this file." msgstr "" #: views/ready.haml:24 msgid "The file will be available until %s." msgstr "" #: views/ready.haml:26 msgid "Share another file…" msgstr "" tmpfhJ2Ca/po/es/0000755000175000017500000000000013026234541012705 5ustar lunarlunartmpfhJ2Ca/po/es/coquelicot.po0000644000175000017500000002006713026223065015420 0ustar lunarlunar# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: coquelicot 0.9.6\n" "PO-Revision-Date: 2016-12-20 12:58+0100\n" "Last-Translator: potager.org \n" "Language-Team: potager.org \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/tebibyte #: lib/coquelicot/num.rb:26 msgid "TiB" msgstr "TiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/gibibyte #: lib/coquelicot/num.rb:28 msgid "GiB" msgstr "GiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/mebibyte #: lib/coquelicot/num.rb:30 msgid "MiB" msgstr "MiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/kibibyte #: lib/coquelicot/num.rb:32 views/layout.haml:41 msgid "KiB" msgstr "KiB" #. Abbreviated unit of storage. See https://en.wiktionary.org/wiki/byte #: lib/coquelicot/num.rb:34 msgid "B" msgstr "B" #: lib/coquelicot/rack/upload.rb:191 msgid "" "File is bigger than maximum allowed size: %s would exceed the maximum " "allowed %s." msgstr "" "El archivo es más grande que el máximo autorisado:\n" "%s supera el máximo autorisado (%s)." #: lib/coquelicot/rack/upload.rb:194 msgid "File is bigger than maximum allowed size %s." msgstr "El archivo es más grande que el máximo autorisado (%s)" #: lib/coquelicot/rack/upload.rb:201 msgid "File has no content" msgstr "El archivo está vacío" #: views/about_your_data.haml:18 views/layout.haml:53 msgid "About your data…" msgstr "A propósito de sus datos…" #: views/about_your_data.haml:20 msgid "" "Welcome to *Coquelicot*. A simple way to share files with people you know,\n" "with a little bit of privacy." msgstr "" "Bienvenido en *Coquelicot*. Una manera simple de compartir archivos con\n" "personas que conoce, con un poco de intimidad." #: views/about_your_data.haml:23 msgid "What should I expect from “a little bit of privacy”?" msgstr "¿Que puedo esperar de “un poco de intimidad”?" #: views/about_your_data.haml:25 msgid "Exchanges between your computer and %s are encrypted." msgstr "Los intercambios entre su computadora y %s están cifrados." #: views/about_your_data.haml:26 msgid "" "An attacker in-between will be able to see how much data is exchanged,\n" "but not its nature." msgstr "" "Un atacante intermedio podrá ver que cantidad de datos se\n" "intercambia, pero no su tipo." #: views/about_your_data.haml:29 msgid "" "Files are stored encrypted. In case someone gets access to the server\n" "storage, they will know the size, arrival and expiration dates of the\n" "files; but they will not be able to get their content without the\n" "password.\n" "\n" "In case no *download password* has been specified, the password might\n" "be kept in the server request logs. This means that the server might\n" "store enough information to retrieve the actual file content.\n" "\n" "When a *download password* has been specified, the password will not be\n" "stored anywhere on the server. This will prevent retrieval of the\n" "file content, except if the server has been actively compromised\n" "beforehand." msgstr "" "Los archivos están guardados cifrados. Si una persona\n" "obtuviera acceso a los archivos en el servidor, podría saber el tamaño,\n" "la fecha de llegada y de expiración de los archivos, pero no podría acceder\n" "a su contenido sin la contraseña.\n" "\n" "Si ninguna *contraseña de descarga* ha sido especificada, la contraseña\n" "podría aparecer en el registro del servidor. Eso significa que\n" "el servidor puede conservar la información necesaria para recuperar\n" "el contenido del archivo.\n" "\n" "Si una *contraseña de descarga* ha sido especificada, esta contraseña\n" "no estará guardada en ningún lugar del servidor. Eso debería impedir\n" "recuperar el contenido del archivo, salvo si el servidor ha sido\n" "activamente comprometido previamente." #: views/about_your_data.haml:43 msgid "What if I don't trust the server admins?" msgstr "¿Que pasa si no tengo confianza en el administrador del servidor?" #: views/about_your_data.haml:44 msgid "" "You are [free](http://www.gnu.org/licenses/agpl.txt) to install Coquelicot\n" "on your own system. Please refer to the [README](README) if you wish to\n" "know how." msgstr "" "Está [libre](http://www.gnu.org/licenses/agpl.txt) de instalar\n" "Coquelicot en su propio sistema. Consulte el [archivo README](README)\n" "para saber cómo." #: views/auth/imap.haml:20 msgid "E-mail User:" msgstr "Correo electrónico:" #: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22 msgid "Password:" msgstr "Contraseña:" #: views/auth/ldap.haml:20 msgid "LDAP User:" msgstr "" #: views/auth/simplepass.haml:19 views/auth/userpass.haml:23 msgid "Upload password:" msgstr "Contraseña para el envío:" #: views/auth/userpass.haml:20 msgid "Upload user:" msgstr "" #: views/download_in_progress.haml:1 msgid "Download in progress" msgstr "Descargando" #: views/download_in_progress.haml:2 msgid "The requested file is currently being downloaded by another client." msgstr "El archivo solicitado está siendo descargado por otro usuario." #: views/enter_file_key.haml:18 msgid "Enter download password…" msgstr "Ingrese la contraseña de descarga…" #: views/error.haml:1 msgid "Error" msgstr "Error" #: views/error.haml:2 msgid "Something bad happened: %s" msgstr "Algo grave pasó:%s" #: views/expired.haml:18 msgid "Too late…" msgstr "Demasiado tarde…" #: views/expired.haml:20 msgid "Sorry, file has expired." msgstr "Lo siento, el archivo ha caducado." #: views/forbidden.haml:1 msgid "Forbidden" msgstr "Prohibido" #: views/forbidden.haml:2 msgid "This password does not allow access to this resource." msgstr "Esta contraseña no permite acceder a este recurso." #: views/index.haml:22 msgid "Share a file!" msgstr "¡Compartir este archivo!" #: views/index.haml:32 msgid "Available for:" msgstr "Disponible durante:" #: views/index.haml:34 msgid "1 day" msgstr "1 día" #: views/index.haml:34 msgid "1 hour" msgstr "1 hora" #: views/index.haml:34 msgid "1 month" msgstr "1 mes" #: views/index.haml:34 msgid "1 week" msgstr "1 semana" #: views/index.haml:43 msgid "Unlimited downloads until expiration" msgstr "Descargas ilimitadas hasta expiración" #: views/index.haml:46 msgid "Remove after one download" msgstr "Borrar después de una descarga" #: views/index.haml:48 msgid "Download password (optional):" msgstr "Contraseña de descarga (opcional):" #: views/index.haml:51 msgid "File (max. size: %s):" msgstr "Archivo (tamaño max.:%s):" #: views/index.haml:55 msgid "Share!" msgstr "¡Compartir!" #: views/layout.haml:23 msgid "Coquelicot" msgstr "Coquelicot" #: views/layout.haml:34 msgid "Generate random" msgstr "Generar aleatoriamente" #: views/layout.haml:35 msgid "Generating…" msgstr "Generando" #: views/layout.haml:36 msgid "Don't forget to write it down!" msgstr "¡No se olvide de anotarlo!" #: views/layout.haml:37 msgid "Please try again!" msgstr "¡Intente nuevamente!" #: views/layout.haml:38 msgid "Error:" msgstr "Error:" #: views/layout.haml:39 msgid "Upload starting..." msgstr "Empezando el envío…" #: views/layout.haml:40 msgid "Uploading: " msgstr "Enviando:" #: views/not_found.haml:1 msgid "Not found" msgstr "No se encuentra" #: views/not_found.haml:2 msgid "The requested URL %s was not found on this server." msgstr "El URL %s no se encuentra en este servidor." #: views/ready.haml:18 msgid "Share this!" msgstr "¡Compartir!" #: views/ready.haml:23 msgid "A password is required to download this file." msgstr "Se necesita una contrasña para descargar este archivo." #: views/ready.haml:24 msgid "The file will be available until %s." msgstr "Este archivo estará disponible hasta el %s." #: views/ready.haml:26 msgid "Share another file…" msgstr "Compartir otro archivo…" tmpfhJ2Ca/views/0000755000175000017500000000000013026234541013015 5ustar lunarlunartmpfhJ2Ca/views/not_found.haml0000644000175000017500000000011112574355361015657 0ustar lunarlunar%h1 Not found %p The requested URL #{@uri} was not found on this server. tmpfhJ2Ca/views/auth/0000755000175000017500000000000013026234541013756 5ustar lunarlunartmpfhJ2Ca/views/auth/imap.haml0000644000175000017500000000221412574355361015561 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2012-2013 potager.org -# © 2011 mh / immerda.ch -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . .field %label{ :for => 'imap_user' } E-mail User: %input.input{ :type => 'text', :id => 'imap_user', :name => 'imap_user' } .field %label{ :for => 'imap_password' } Password: %input.input{ :type => 'password', :id => 'imap_password', :name => 'imap_password' } tmpfhJ2Ca/views/auth/simplepass.haml0000644000175000017500000000200012574355361017004 0ustar lunarlunar-# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2010-2013 potager.org -# © 2011 mh / immerda.ch -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . .field %label{ :for => 'upload_password' } Upload password: %input.input{ :type => 'password', :id => 'upload_password', :name => 'upload_password' } tmpfhJ2Ca/views/auth/ldap.haml0000644000175000017500000000221112574355361015550 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2012-2014 potager.org -# © 2014 Rowan Thorpe -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . .field %label{ :for => 'ldap_user' } LDAP User: %input.input{ :type => 'text', :id => 'ldap_user', :name => 'ldap_user' } .field %label{ :for => 'ldap_password' } Password: %input.input{ :type => 'password', :id => 'ldap_password', :name => 'ldap_password' } tmpfhJ2Ca/views/auth/userpass.haml0000644000175000017500000000223613026223065016470 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2012-2016 potager.org -# © 2016 Rowan Thorpe -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . .field %label{ :for => 'upload_user' } Upload user: %input.input{ :type => 'text', :id => 'upload_user', :name => 'upload_user' } .field %label{ :for => 'upload_password' } Upload password: %input.input{ :type => 'password', :id => 'upload_password', :name => 'upload_password' } tmpfhJ2Ca/views/enter_file_key.haml0000644000175000017500000000215012574355361016655 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2010 potager.org -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . %h1 Enter download password… #content %form{ :action => @link, :method => 'post' } .field %label{ :for => 'file_key' } Password: %input{ :type => 'password', :id => 'file_key', :name => 'file_key' } .field .submit %input{ :type => 'submit', :value => 'Get file' } tmpfhJ2Ca/views/expired.haml0000644000175000017500000000156112574355361015336 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2010 potager.org -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . %h1 Too late… #content %p Sorry, file has expired. tmpfhJ2Ca/views/ready.haml0000644000175000017500000000212312574355361014775 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2010 potager.org -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . %h1 Share this! #content .url %textarea.ready{ 'readonly' => 'readonly' }= uri(@name) - unless @unprotected %p A password is required to download this file. %p The file will be available until #{@expire_at}. .again %a{ :href => uri('/') } Share another file… tmpfhJ2Ca/views/error.haml0000644000175000017500000000005712574355361015026 0ustar lunarlunar%h1 Error %p Something bad happened: #{@error} tmpfhJ2Ca/views/layout.haml0000644000175000017500000000534613026223065015204 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2010-2013 potager.org -# © 2011 mh / immerda.ch -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . !!! XML !!! Strict %html(xmlns='http://www.w3.org/1999/xhtml') %head %title Coquelicot %meta{ :"http-equiv" => "Content-Type", :content => "text/html; charset=UTF-8" } %meta{ :name => 'generator', :content => "Coquelicot #{Coquelicot::VERSION}" } %base{ :href => uri('/') } %link{ :rel => 'stylesheet', :href => "style.css", :type => 'text/css', :media => "screen, projection" } - unless settings.additional_css.empty? %link{ :rel => 'stylesheet', :href => "#{settings.additional_css}", :type => 'text/css', :media => "screen, projection" } %script{ :type => 'text/javascript', :src => 'javascripts/jquery.min.js' } %script{ :type => 'text/javascript', :src => 'javascripts/jquery.lightBoxFu.js' } %script{ :type => 'text/javascript', :src => 'javascripts/jquery.uploadProgress.js' } :javascript var i18n = { generateRandomPassword: _('Generate random'), generatingRandomPassword: _('Generating…'), writeItDown: _('Don\'t forget to write it down!'), pleaseTryAgain: _('Please try again!'), error: _('Error:'), uploadStarting: _('Upload starting...'), uploading: _('Uploading: '), kib: _('KiB'), }; %script{ :type => 'text/javascript', :src => 'javascripts/coquelicot.js' } %body #header - unless uri.end_with? '/README' - Coquelicot::AVAILABLE_LOCALES.each do |locale| %a{ :href => uri + "?lang=#{locale}" }= locale #container = yield #footer %div %a{ :href => 'about-your-data' }= _('About your data…') = '—' %a{ :href => 'README' }= 'Coquelicot' = '© 2010-2013 potager.org —' %a{ :href => 'http://www.gnu.org/licenses/agpl.txt' }= 'AGPLv3' %div %code= "#{clone_command}" tmpfhJ2Ca/views/about_your_data.haml0000644000175000017500000000420212574355361017052 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2010 potager.org -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . %h1 About your data… #content :markdown Welcome to *Coquelicot*. A simple way to share files with people you know, with a little bit of privacy. %h2 What should I expect from “a little bit of privacy”? - if request.secure? %p Exchanges between your computer and #{request.host} are encrypted. :markdown An attacker in-between will be able to see how much data is exchanged, but not its nature. :markdown Files are stored encrypted. In case someone gets access to the server storage, they will know the size, arrival and expiration dates of the files; but they will not be able to get their content without the password. In case no *download password* has been specified, the password might be kept in the server request logs. This means that the server might store enough information to retrieve the actual file content. When a *download password* has been specified, the password will not be stored anywhere on the server. This will prevent retrieval of the file content, except if the server has been actively compromised beforehand. %h2 What if I don't trust the server admins? :markdown You are [free](http://www.gnu.org/licenses/agpl.txt) to install Coquelicot on your own system. Please refer to the [README](README) if you wish to know how. tmpfhJ2Ca/views/index.haml0000644000175000017500000000500213026223065014763 0ustar lunarlunar-# -*- coding: UTF-8 -*- -# Coquelicot: "one-click" file sharing with a focus on users' privacy. -# Copyright © 2010-2013 potager.org -# © 2011 mh / immerda.ch -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . :javascript $(document).ready(addLinkToPasswordGenerator); $(document).ready(authenticate); %h1 Share a file! - unless @error.nil? .error= @error %form#upload{ :enctype => 'multipart/form-data', :action => 'upload', :method => 'post' } #upload-authentication - unless about_text.empty? %p.about= about_text %script{ :type => 'text/javascript', :src => "javascripts/coquelicot.auth.#{auth_method}.js" } = render :haml, :"auth/#{auth_method}", :layout => false .field %label{ :for => 'expire' } Available for: %select.input{ :id => 'expire',:name => 'expire' } - { _('1 hour') => 60, _('1 day') => 60 * 24, _('1 week') => 60 * 24 * 7, _('1 month') => 60 * 24 * 30 }.each_pair do |v, k| %option{:value => k, :selected => k == Coquelicot.settings.default_expire}= v .field %fieldset .radio %input{ :type => 'radio', :id => 'any_number', :name => 'one_time', :value => '', :checked => 'checked' } %label{ :for => 'any_number' } Unlimited downloads until expiration .radio %input{ :type => 'radio', :id => 'one_time', :name => 'one_time', :value => 'true' } %label{ :for => 'one_time' } Remove after one download .field %label{ :for => 'file_key' } Download password (optional): %input.input{ :type => 'password', :id => 'file_key', :name => 'file_key' } .field %label{ :for => 'file' } File (max. size: #{Coquelicot.settings.max_file_size.as_size}): %input.input{ :type => 'file', :id => 'file', :name => 'file' } .field .submit %input#submit{ :type => 'submit', :value => _('Share!') } tmpfhJ2Ca/views/download_in_progress.haml0000644000175000017500000000014012574355361020107 0ustar lunarlunar%h1 Download in progress %p The requested file is currently being downloaded by another client. tmpfhJ2Ca/views/style.sass0000644000175000017500000000576712574355361015102 0ustar lunarlunar@charset "utf-8" /* * Coquelicot: "one-click" file sharing with a focus on users' privacy. * Copyright © 2010-2013 potager.org * © 2011 mh / immerda.ch * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ body background: no-repeat fixed 50% 100% #70794e url('images/background.jpg') font-family: Georgia color: darkgreen a, a:visited text-decoration: underline color: blue .error background-color: red color: white border: black solid 1px h1 margin-top: 0.1ex border-bottom: solid 1px #b60a00 text-align: center h2 border-bottom: solid 1px #b60a00 #header width: 550px margin-top: 1.5em margin-bottom: 0.1em margin-left: auto margin-right: auto padding-right: 25px text-align: right font-size: small #header a text-decoration: none color: #ccc #container width: 550px margin-bottom: 5em margin-left: auto margin-right: auto border-radius: 25px -moz-border-radius: 25px -webkit-border-radius: 25px background: white border: solid 1px black padding: 5px 25px .url text-align: center .url .ready font-size: 120% width: 80% text-align: center .again margin-top: 1ex text-align: right .field label display: block float: left width: 70% fieldset border: none clear: left padding: 0 margin-bottom: 8px .radio margin-left: 0px .radio label display: inline text-align: left float: none .input, .random-pass clear: left margin: 0 5px 2px 0 width: 100% .random-pass color: black .random-pass code font-size: large .random-pass em font-size: 80% color: #ccc #gen_pass font-size: small text-align: right display: block float: left width: 29% .field clear: left margin-bottom: 8px .submit text-align: center margin-top: 8px #progress margin: 8px width: 220px height: 19px #progressbar background: url('images/ajax-loader.gif') no-repeat width: 0px height: 19px #auth-message clear: both color: red text-align: center #footer position: relative bottom: 0 clear: both width: 100% padding-top: 0.5em padding-bottom: 0.2em border-top: dashed 1px black text-align: center color: #ccc font-size: small #footer a, #footer a:visited color: #ccc text-decoration: underline #upload-authentication .field label margin: 0 5px 1px 0 #upload-authentication .submit float: right margin: 1px 22px tmpfhJ2Ca/views/forbidden.haml0000644000175000017500000000010712574355361015625 0ustar lunarlunar%h1 Forbidden %p This password does not allow access to this resource. tmpfhJ2Ca/Gemfile0000644000175000017500000000004712574355361013167 0ustar lunarlunarsource "https://rubygems.org" gemspec tmpfhJ2Ca/INSTALL0000644000175000017500000001430413026223065012712 0ustar lunarlunarHow to setup Coquelicot? ======================== Coquelicot is written in Ruby using the Sinatra web framework and Rainbows! web server. Coquelicot is intended to be run on a fully encrypted system and accessible only through HTTPS. In order to support HTTPS, Coquelicot needs the help of a non-buffering HTTPS reverse proxy. Install dependencies -------------------- Coquelicot uses Bundler to manage its dependency. To install Bundler on Debian, please issue: # apt-get install rubygems libxml2-dev libxslt-dev $ gem install bundler Once Bundler is available, simply run: $ bundle install --deployment AGPL compliance --------------- If you have downloaded Coquelicot from Git, AGPL compliance can be made by serving the local Git clone. This can be achieved with the following commands: git update-server-info echo '#!/bin/sh' > .git/hooks/post-update echo 'exec git update-server-info' >> .git/hooks/post-update chmod +x .git/hooks/post-update Start Coquelicot! ----------------- To start Coquelicot use: $ bundle exec coquelicot start `start` can be replaced by `stop` to shut down the server. HTTPS reverse proxy ------------------- Coquelicot itself is able to serve HTTPS directly, so a non-buffering HTTPS reverse proxy needs to be setup to protect users' privacy. ### Apache To configure [Apache] as a reverse proxy, the `proxy`, `proxy_http` and `ssl` modules must be enabled. A minimal configuration would then look like: ServerName dl.example.org SSLEngine on [… insert other SSL related directives here …] ProxyPass / http://127.0.0.1:51161/ SetEnv proxy-sendchunks 1 RequestHeader set X-Forwarded-SSL "on" If you wish to have Coquelicot served from a “sub-directory”, `path` needs to be set in `settings.yml` to the proper value. For the following example, we use `/coquelicot`: ServerName dl.example.org SSLEngine on […] ProxyPass http://127.0.0.1:51161/coquelicot SetEnv proxy-sendchunks 1 RequestHeader set X-Forwarded-SSL "on" [Apache]: http://httpd.apache.org/ ### Nginx Here is a sample configuration fox Nginx: server { listen 443; server_name dl.example.org ssl on; [… insert other SSL related directives here …] location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-SSL on; proxy_pass http://127.0.0.1:51161; proxy_buffering off; } } [Nginx]: http://nginx.net ### Pound Here is a sample configuration excerpt for [Pound]: ListenHTTPS Address 0.0.0.0 Port 443 Cert "/etc/ssl/cert.pem" AddHeader "X-Forwarded-SSL: on" Service BackEnd Address 127.0.0.1 Port 51161 End End End [Pound]: http://www.apsis.ch/pound/ ### Using other Rack compatible webservers Coquelicot has been written to use [Rainbows!] as its webserver. It can probably be also run with other [Rack] compatible webservers like mod_passenger, Mongrel, Thin. Please note that such configurations have not been tested and that they are likely to **ruin privacy expectations** because of *buffered inputs*. See [HACKING](/HACKING) for details on the later. [Rainbows!]: http://rainbows.rubyforge.org/ [Rack]: http://rack.rubyforge.org Configuration ------------- By default Coquelicot is configured to authenticate with the "simplepass" mechanism and some other reasonable defaults. It is possible to overwrite these settings from a configuration file named `settings.yml` that will be used if it is present in the `conf` directory of the application. All available settings with their default values are documented in `conf/settings-default.yml`. Further settings example: * `conf/settings-simplepass.yml`: shows how to change the default password for the "simplepass" mechanism. * `conf/settings-imap.yml`: necessary configuration for the "userpass" authentication mechanism. * `conf/settings-imap.yml`: necessary configuration for the "imap" authentication mechanism. * `conf/settings-ldap.yml`: necessary configuration for the "ldap" authentication mechanism. You can copy one of these examples to `conf/settings.yml` and adjust them according to your environment. Using the "userpass" authentication method requires the `bcrypt` gem to be installed manually. Using the LDAP authentication method requires the `net-ldap` gem to be installed manually. A different location for the configuration file can be specified using the `-c` option when running `bin/coquelicot`. Garbage collection ------------------ To cleanup files automatically when they expired, coquelicot comes with a cleanup script, that does the garbage collection for you. The easiest way is to set up a cron job that will run every 5 minutes (or so): bundle exec coquelicot gc Migrate from Jyraphe -------------------- [Jyraphe] is another free software web file sharing application. Coquelicot provides a migration script to import Jyraphe 0.5 repositories. It can be run using `bundle exec coquelicot migrate-jyraphe`: Usage: coquelicot [options] migrate-jyraphe \ [command options] JYRAPHE_VAR > REWRITE_RULES Options: -c, --config FILE read settings from FILE Command options: -p, --rewrite-prefix PREFIX prefix URL in rewrite rules The last argument must be a path to the `var` directory of the Jyraphe installation. After migrating the files to Coquelicot, directives for Apache mod_rewrite will be printed on stdout which ought to be redirected to a file. Using the `-p` option will prefix URL with the given path in the rewrite rules. [Jyraphe]: http://home.gna.org/jyraphe/ tmpfhJ2Ca/conf/0000755000175000017500000000000013026234541012605 5ustar lunarlunartmpfhJ2Ca/conf/settings-imap.yml0000644000175000017500000000075512574355361016136 0ustar lunarlunar# Settings for the IMAP authentication method # ------------------------------------------- # # When using the IMAP authentication method users will be # asked for a login and a password. Those credentials will # be tested against the given IMAP server. # # Connections to the IMAP server are made using SSL/TLS. authentication_method: name: imap # Hostname of the authenticating IMAP server imap_server: "imap.example.com" # Port of the authenticating IMAP server imap_port: 993 tmpfhJ2Ca/conf/settings-simplepass.yml0000644000175000017500000000067712574355361017373 0ustar lunarlunar# Settings for the 'simplepass' authentication method # --------------------------------------------------- # # When using the 'simplepass' authentication method, users will # be asked for a pre-shared password. authentication_method: name: simplepass # SHA1 of the pre-shared password # # One way to compute the hash could be: # # $ echo -n 'test' | sha1sum # upload_password: "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" tmpfhJ2Ca/conf/settings-userpass.yml0000644000175000017500000000124313026223065017032 0ustar lunarlunar# Settings for the 'userpass' authentication method # ------------------------------------------- # # When using the 'userpass' authentication method, users will # be asked for their username and a pre-shared password. authentication_method: name: "userpass" # Passwords are stored using BCrypt # # You can compute them using: # # $ echo 'secret' | ruby -rbcrypt -p -e 'puts BCrypt::Password.create($_).to_s' # credentials: fred: "$2a$10$Xe8F9F.4vNBmuA6V/5hvK.Glw0ab4pJgcVPQlUN8dP/uhf4QCWAFW" jenny: "$2a$10$mBAOGjcsIL9wyaAIGDdxf.rOyJEfCYeG2pAaXCrwjhXbis7k/Vcs." abdul: "$2a$10$8NXS8SQdhkUlC7b2vog.FOm9Nob4t38v146rQHlAVoyClNJPiCnVa" tmpfhJ2Ca/conf/settings-default.yml0000644000175000017500000000736612575011374016633 0ustar lunarlunar# Default settings for Coquelicot # =============================== # # Coquelicot is a "one-click" file sharing web application with a focus # on protecting users' privacy. # # This file contains the default settings and their meaning. # # These settings are only here for illustration purpose. Site specific # configuration only needs to specify the ones that need to be changed. # Maximum size allowed for uploaded files # (in bytes) # # Default: 5242880 = 5 * 1024 * 1024 # max_file_size: 5242880 # Default expiration time (if unspecified by users) # (in minutes) # # Default: 1440 = 60 * 24 ≈ 1 day # default_expire: 1440 # Maximum expiration time that can be set by users # (in minutes) # # Default: 43200 = 60 * 24 * 30 ≈ 1 month # maximum_expire: 43200 # Time before complete cleanup of an expired file # (in minutes) # # Once a file is expired either because of time or because # it was set for only one download, Coquelicot will scrape # the file content, but keep an empty file around to display # a “Too late” message instead of the default “Not found”. # # This setting will influence how long will users see the # first message instead of the second in case they try to # access an expired link. # # Default: 10080 = 60 * 24 * 7 ≈ 1 week # gone_period: 10080 # Number of characters in generated filenames # # URL to download files looks like: # https://example.org/dhut7f73u2hiwwifwyrs-gs5wj3ixjheg6dg7 # (when no password has been specified) # or: # https://example.org/dhut7f73u2hiwwifwyrs # # This setting controls the first set of characters. # filename_length: 20 # Number of characters in generated passwords # # When no password is specified URL looks like: # https://example.org/dhut7f73u2hiwwifwyrs-gs5wj3ixjheg6dg7 # # This setting controls the second set of characters. The same # code is also used when using the 'Generate password…' link. # random_pass_length: 16 # Directory in which Coquelicot will write the stored files # depot_path: "./files" # Directory in which Coquelicot will write cache files. The content is only # required to speed up operations and does not have to be stored permanently. # cache_path: "./tmp/cache" # Text to display on top of the upload form # # This is indexed by locale code to support multiple languages. # It will fallback to English for unspecified languages. about_text: en: "" # Path to an additional stylesheet additional_css: "" # Path to the PID file of the web server pid: "./tmp/coquelicot.pid" # Path to Coquelicot log file # # Set to an empty string to disable logging. # log: "./tmp/coquelicot.log" # Listening addresses of the web server # # Each entries may be a port number for a TCP port, an “IP_ADDRESS:PORT” for # TCP listeners or a pathname for UNIX domain sockets. # # Examples: # - "51161" # listen to port 51161 on all TCP interfaces # - "127.0.0.1:51161" # listen to port 51161 on the loopback interface # - "/tmp/.coquelicot.sock" # listen on the given Unix domain socket # - "[::1]:51161" # listen to port 51161 on the IPv6 loopback interface # listen: - "127.0.0.1:51161" # Path used URL to access the application. # # As an example, if you want to make Coquelicot accessible from # https://example.org/dl/ set `path` to `/dl`. # path: "/" # Display debugging data in the browser when an exception is raised # # This should only be turned on when doing development. show_exceptions: false # Authentication method # # Please have a look at `conf/settings-simplepass.yml`, # `conf/settings-imap.yml` and `conf/settings-ldap.yml` for more details. # # The default password is 'test'. authentication_method: name: "simplepass" upload_password: "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" tmpfhJ2Ca/conf/settings-ldap.yml0000644000175000017500000000110112574355361016112 0ustar lunarlunar# Settings for the LDAP authentication method # ------------------------------------------- # # When using the LDAP authentication method users will be # asked for a login and a password. Those credentials will # be tested against the given LDAP server. # # Connections to the LDAP server are made using SSL/TLS. authentication_method: name: ldap # Hostname of the authenticating LDAP server ldap_server: "ldap.example.com" # Port of the authenticating LDAP server ldap_port: 636 # Search base of the authenticating LDAP server ldap_base: "dc=example,dc=com" tmpfhJ2Ca/README0000644000175000017500000001203113026225500012530 0ustar lunarlunarAbout “Coquelicot” ================== [Coquelicot] — /kɔ.kli.ko/ —  is a "one-click" file sharing web application with a focus on protecting users' privacy. Basic principle: users can upload a file to the server, in return they get a unique URL which can be shared with others in order to download the file. Coquelicot aims to protect, to some extent, users and system administrators from disclosure of the files exchanged from passive and not so active attackers. [Coquelicot]: https://coquelicot.potager.org/ Features -------- * Support for different authentication methods In order to prevent random Internet users to eat bandwidth and disk space, Coquelicot limits upload to authenticated users. It currently ships with three authentication mechanisms: - "simplepass": uploading users need to provide a global, pre-shared, password; - "imap": users will need to provide a login and a password, that are used to authenticate against an existing IMAP server. - "ldap": users will need to provide a uid and a password, that are used to authenticate against an existing LDAP server. It is possible to integrate more authentication mechanisms by implementing a single method, some JavaScript, and a partial template to render the common fields. For more information have a look at the notes below. * Mandatory expiration When uploading, a time limit has to be specified. The file will be unavailable once this much time has passed. During a configurable period of time, trying to download the file will return a page saying "too late" instead of "not found". * Support for one-time download A user might want to allow exactly _one_ download of a file, to more closely replace an email attachment. The file will be removed after the first complete download and concurrent downloads are prevented. * Upload progress bar Users having JavaScript enabled will see a nice progress bar during the file upload. * Downgrade nicely The application works fine without JavaScript or CSS. * Download URL can be written on paper URLs generated to download files uses the Base32 character set. This set is specifically designed to overcome misread of 'l', '1', '0' and 'O' characters. Coquelicot will automatically convert case and ambiguous characters to facilitate URL exchanges using pieces of paper. * Files are stored encrypted on the server While being uploaded, files are written to the disk using symmetric encryption. The encryption key is _not_ stored directly by Coquelicot. It is either generated randomly and given as part of the download URL, or specified by the uploader. * Download can be protected by a password When uploading, a password can be specified which will then be used to encrypt the file. For subsequent downloads, the password must be entered through in a POST'ed form. This prevents the password from appearing in most server logs. * Files are stored with a random name To prevent disclosure of the shared file name, it is stored encrypted together with the file content. On the server, this encrypted file is stored with a random name. * Download URLs do not reflect stored file names The random names given in download URLs do not map directly to file names on the server. This prevent server logs from giving a direct mapping to the shared files. This creates another difficulty to link users to files through forensic techniques. * File content is zero'ed before removal When a file has expired, it is removed from the server. In order to make it harder to retrieve its content through filesystem analysis, it is filled with zeros first. Reporting bugs -------------- Please report bugs or suggest new features on the users and developers [mailing list]. [mailing list]: https://listes.potager.org/listinfo/coquelicot Authors ------- Coquelicot © 2010-2016 potager.org © 2014-2016 Rowan Thorpe © 2010-2012 Jake Santee © 2012 Silvio Rhatto © 2011 mh / immerda.ch Coquelicot is distributed under the [GNU Affero General Public License] version 3 or (at your option) any later version. Background image (`public/images/background.jpg`) derived from: [“coquelicot” picture] © 2008 Jean-Louis Zimmermann Licensed under [Creative Commons Attributions 2.0 Generic] *jQuery* is © 2011 John Resig. Licensed under the [MIT license]. *jquery.uploadProgress* is © 2008 Piotr Sarnacki. Licensed under the [MIT license]. *lightboxFu* is © 2008 Piotr Sarnacki. Licensed under the [MIT license]. [“coquelicot” picture]: https://secure.flickr.com/photos/jeanlouis_zimmermann/2478019744/ [GNU Affero General Public License]: http://www.gnu.org/licenses/agpl.txt [Creative Commons Attributions 2.0 Generic]: https://creativecommons.org/licenses/by/2.0/deed [MIT license]: http://www.opensource.org/licenses/mit-license.php tmpfhJ2Ca/public/0000755000175000017500000000000013026234541013136 5ustar lunarlunartmpfhJ2Ca/public/javascripts/0000755000175000017500000000000013026234541015467 5ustar lunarlunartmpfhJ2Ca/public/javascripts/coquelicot.auth.ldap.js0000644000175000017500000000275612574355361022100 0ustar lunarlunar/* * Coquelicot: "one-click" file sharing with a focus on users' privacy. * Copyright © 2012-2014 potager.org * © 2014 Rowan Thorpe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ var authentication = { getData: function() { return { ldap_user: $('#ldap_user').val(), ldap_password: $('#ldap_password').val() }; }, focus: function() { $('#ldap_user').focus(); }, handleReject: function() { $('#ldap_user').val(''); $('#ldap_password').val(''); }, }; $(document).ready(function() { $('#ldap-auth-submit').remove(); var submit = $(''); submit.attr('value', 'Login'); submit.attr('id', 'ldap-auth-submit'); $('#upload-authentication').append( $('
').append( $('
').append( submit))); }); tmpfhJ2Ca/public/javascripts/coquelicot.auth.userpass.js0000644000175000017500000000300013026223065022770 0ustar lunarlunar/* * Coquelicot: "one-click" file sharing with a focus on users' privacy. * Copyright © 2012-2016 potager.org * © 2016 Rowan Thorpe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ var authentication = { getData: function() { return { upload_user: $('#upload_user').val(), upload_password: $('#upload_password').val() }; }, focus: function() { $('#upload_user').focus(); }, handleReject: function() { $('#upload_user').val(''); $('#upload_password').val(''); }, }; $(document).ready(function() { $('#upload-auth-submit').remove(); var submit = $(''); submit.attr('value', 'Login'); submit.attr('id', 'upload-auth-submit'); $('#upload-authentication').append( $('
').append( $('
').append( submit))); }); tmpfhJ2Ca/public/javascripts/jquery.lightBoxFu.js0000644000175000017500000000356312575077351021441 0ustar lunarlunar/* * lightboxFu * * Copyright (c) 2008 Piotr Sarnacki (drogomir.com) * * Licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * */ (function($) { $.extend($, {lightBoxFu: {}}); $.extend($.lightBoxFu, { initialize: function (o) { if($('#lightboxfu').length == 0) { options = {stylesheetsPath: '/stylesheets/', imagesPath: '/images/'}; jQuery.extend(options, o); html = ''; $('body').append(html); $('#lOverlay').css('background', 'url('+options.imagesPath+'overlay.png) fixed'); $.lightBoxFu.appendStyle(); } }, open: function(options) { options = options || {}; $('#lInner').html(options.html); $('#lightboxfu').show(); var width = options.width || '250'; $('#lInner').css({'width': width}); if(options.closeOnClick != false) { $('#lOverlay').one('click', $.lightBoxFu.close); } }, close: function() { $('#lightboxfu').hide(); }, appendStyle: function() { $('#lOverlay').css({display: 'table'}); $('#lOverlay #lWindow').css({display: 'table-cell'}); $('#lOverlay').css({position: 'fixed', top: 0, left: 0, width: "100%", height: "100%"}); $('#lOverlay #lWindow').css({'vertical-align': 'middle'}); $('#lOverlay #lInner').css({width: '300px', 'background-color': '#fff', '-webkit-border-radius': '10px', 'border-radius': '10px', '-moz-border-radius': '10px', 'max-height': '350px', margin: '0 auto', padding: '15px', overflow: 'auto'}); } }); $.extend($.fn, { lightBoxFu: function(options){ return this.each(function() { $(this).click(function() { $.lightBoxFu.open(options); return false; }); }); }}); })(jQuery); tmpfhJ2Ca/public/javascripts/coquelicot.js0000644000175000017500000001132013026223065020170 0ustar lunarlunar/* * Coquelicot: "one-click" file sharing with a focus on users' privacy. * Copyright © 2010-2013 potager.org * © 2011 mh / immerda.ch * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ $(function($) { $.lightBoxFu.initialize({ imagesPath: 'images/', stylesheetsPath: 'stylesheets/' }); $('form#upload').uploadProgress({ start:function() { // after starting upload open lightBoxFu with our bar as html $.lightBoxFu.open({ html: '

 
', width: "250px", closeOnClick: false }); jQuery('#received').html(i18n.uploadStarting); jQuery('#percent').html("0%"); }, uploading: function(upload) { // update upload info on each /progress response jQuery('#received').html(i18n.uploading + parseInt(upload.received / 1024) + "/"); jQuery('#size').html(parseInt(upload.size / 1024) + ' ' + i18n.kib); jQuery('#percent').html(upload.percents + "%"); }, success: function(upload) { $('#received').html(''); $('#size').html(''); $('#percent').html("100%"); }, interval: 2000, /* if we are using images it's good to preload them, safari has problems with downloading anything after hitting submit button. these are images for lightBoxFu and progress bar */ preloadImages: ["images/overlay.png", "images/ajax-loader.gif"], jqueryPath: "javascripts/jquery.min.js", uploadProgressPath: "javascripts/jquery.uploadProgress.js", progressUrl: "progress" }); }); function addLinkToPasswordGenerator() { var link = $(''); link.text(i18n.generateRandomPassword); var file_key = $('#file_key'); file_key.before(link); link.click(function(e) { e.preventDefault(); link.text(i18n.generatingRandomPassword); $.get('random_pass', function(pass) { file_key.val(pass); file_key.hide(); var show = $('
'); show.append($('
').append($('').text(pass))). append($('
').append($('').text(i18n.writeItDown))); link.before(show); link.remove(); }); }); } function authenticate() { var authForm = $('
') var authDiv = $('#upload-authentication').remove(); var lb = $.lightBoxFu; authForm.bind('submit', function() { jQuery.ajax({ type: 'POST', url: 'authenticate', dataType: 'text', data: authentication.getData(), success: function(data, textStatus, jqXHR) { if (data != 'OK') { /* Mh. Something strange happened. */ return; } var hiddenFields = $('
') $.each(authentication.getData(), function(key, value) { var hiddenField = $(''); hiddenField.attr('name', key); hiddenField.val(value); hiddenFields.append(hiddenField) }); $('#upload').prepend(hiddenFields); lb.close(); if (authentication.handleAccept) { authentication.handleAccept(); } }, error: function(jqXHR, textStatus, errorThrown) { switch (jqXHR.status) { case 403: $('#auth-message').text(i18n.pleaseTryAgain); if (authentication.handleReject) { authentication.handleReject(); } return; default: $('#auth-message'). empty(). append($('
').text(i18n.error)). append($('
').append($('').text(errorThrown))). append($('
').text(jqXHR.responseText)); if (authentication.handleFailure) { authentication.handleFailure(textStatus); } } }, }); return false; }); lb.open({ html: authForm.append(authDiv).append('
'), width: "350px", closeOnClick: false }); authentication.focus(); } tmpfhJ2Ca/public/javascripts/coquelicot.auth.simplepass.js0000644000175000017500000000215612574355361023332 0ustar lunarlunar/* * Coquelicot: "one-click" file sharing with a focus on users' privacy. * Copyright © 2012-2013 potager.org * © 2011 mh / immerda.ch * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ var authentication = { getData: function() { return { upload_password: $('#upload_password').val() }; }, focus: function() { $('#upload_password').focus(); }, handleReject: function() { $('#upload_password').val(''); }, }; tmpfhJ2Ca/public/javascripts/coquelicot.auth.imap.js0000644000175000017500000000275712574355361022107 0ustar lunarlunar/* * Coquelicot: "one-click" file sharing with a focus on users' privacy. * Copyright © 2012-2013 potager.org * © 2011 mh / immerda.ch * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ var authentication = { getData: function() { return { imap_user: $('#imap_user').val(), imap_password: $('#imap_password').val() }; }, focus: function() { $('#imap_user').focus(); }, handleReject: function() { $('#imap_user').val(''); $('#imap_password').val(''); }, }; $(document).ready(function() { $('#imap-auth-submit').remove(); var submit = $(''); submit.attr('value', 'Login'); submit.attr('id', 'imap-auth-submit'); $('#upload-authentication').append( $('
').append( $('
').append( submit))); }); tmpfhJ2Ca/public/javascripts/jquery.uploadProgress.js0000644000175000017500000000504312575077541022373 0ustar lunarlunar/* * jquery.uploadProgress * * Copyright (c) 2008 Piotr Sarnacki (drogomir.com) * * Licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * */ (function($) { $.fn.uploadProgress = function(options) { options = $.extend({ dataType: "json", interval: 2000, progressBar: "#progressbar", progressUrl: "/progress", start: function() {}, uploading: function() {}, complete: function() {}, success: function() {}, error: function() {}, preloadImages: [], uploadProgressPath: '/javascripts/jquery.uploadProgress.js', jqueryPath: '/javascripts/jquery.js', timer: "" }, options); $(function() { //preload images for(var i = 0; i").attr("src", options.preloadImages[i]); } }); return this.each(function(){ $(this).bind('submit', function() { var uuid = ""; for (i = 0; i < 32; i++) { uuid += Math.floor(Math.random() * 16).toString(16); } /* update uuid */ options.uuid = uuid; /* start callback */ options.start(); /* patch the form-action tag to include the progress-id if X-Progress-ID has been already added just replace it */ if(old_id = /X-Progress-ID=([^&]+)/.exec($(this).attr("action"))) { var action = $(this).attr("action").replace(old_id[1], uuid); $(this).attr("action", action); } else { $(this).attr("action", jQuery(this).attr("action") + "?X-Progress-ID=" + uuid); } var uploadProgress = jQuery.uploadProgress; options.timer = window.setInterval(function() { uploadProgress(this, options) }, options.interval); }); }); }; jQuery.uploadProgress = function(e, options) { jQuery.ajax({ type: "GET", url: options.progressUrl + "?X-Progress-ID=" + options.uuid, dataType: options.dataType, success: function(upload) { var bar = $(options.progressBar); if (upload.state == 'uploading') { upload.percents = Math.floor((upload.received / upload.size)*1000)/10; bar.css({width: upload.percents+'%'}); options.uploading(upload); } if (upload.state == 'done' || upload.state == 'error') { window.clearTimeout(options.timer); options.complete(upload); } if (upload.state == 'done') { bar.css({width: '100%'}); options.success(upload); } if (upload.state == 'error') { options.error(upload); } } }); }; })(jQuery); tmpfhJ2Ca/public/stylesheets/0000755000175000017500000000000013026234541015512 5ustar lunarlunartmpfhJ2Ca/public/stylesheets/lightbox-fu-ie7.css0000644000175000017500000000114112574355361021146 0ustar lunarlunar#lightboxfu { display: none; } #lOverlay { background: none; -ieh: expression( this.parsed ? 0 : ( ov = document.createElement('div'), ov.id = 'ov', this.parentNode.insertBefore(ov, this), this.parsed = 1 ) ) } #ov { position: fixed; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; filter: progid:DXImageTransform.Microsoft.Alpha(opacity=60); } #lOverlay #lWindow { position: absolute; top: 50%; } #lOverlay #lInner { position: relative; top: -50%; height: expression(this.scrollHeight > 350 ? '350px' : 'auto' ); } tmpfhJ2Ca/public/stylesheets/lightbox-fu-ie6.css0000644000175000017500000000164612574355361021157 0ustar lunarlunar#lightboxfu { display: none; } #lOverlay { background: none; -ieh: expression( this.parsed ? 0 : ( img = document.createElement('div'), img.id = 'ov', this.parentNode.insertBefore(img, this), this.parsed = 1 ) ) } #lOverlay, #ov { position: absolute; top: 0; left: 0; width: expression(document.documentElement.clientWidth + 'px'); height: expression(document.documentElement.clientHeight + 'px'); top: expression( offset = 0 + parseInt(document.body.currentStyle.paddingTop) + parseInt(document.body.currentStyle.marginTop), document.documentElement.scrollTop + offset + 'px' ); } #ov { background-color: #000; filter: progid:DXImageTransform.Microsoft.Alpha(opacity=60); } #lOverlay #lWindow { position: absolute; top: 50%; } #lOverlay #lInner { position: relative; top: -50%; height: expression(this.scrollHeight > 350 ? '350px' : 'auto' ); } tmpfhJ2Ca/public/images/0000755000175000017500000000000013026234541014403 5ustar lunarlunartmpfhJ2Ca/public/images/overlay.png0000644000175000017500000000020612574355361016603 0ustar lunarlunarPNG  IHDRo&gAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxbd``ɀ  >U7&IENDB`tmpfhJ2Ca/public/images/blank.gif0000644000175000017500000000005312574355361016172 0ustar lunarlunarGIF89a!,D;tmpfhJ2Ca/public/images/ajax-loader.gif0000644000175000017500000002510312574355361017275 0ustar lunarlunarGIF89aߢژ֏ޣݡۛגՉ! NETSCAPE2.0!Created with ajaxload.info! , dihlp,tmx|pHrl:ШtJZجv:(zݸHxhh|{~zxkwuyst`f^+]d dpn p½õǾźʱӰ,  ׏ ;x/!A{ОÂNF}\H\:YEr0V8wС<:ϥG*T͠U>źӪѨZrjW+XA-ZpSYK‚vpgɭ;n^św/_ E\8`q&Y]ʴӅYb̀-EB-,,Jk;u]Ǿ,ݻe |7y\䰏Ås_Wݛt Ө-mא`͟~~z݋/v_]xxpƊf1 voNZbHI!YX?8Yh Ao5USta)cs;h\8xTB*GciձƣLʋ& P ^ԗaW&~`JIbnզXo^Wh `4 F}>'Ngjg h+.٠H:أ*M*ꨤj*]ꪬ꫰*무j'! , dihlp,tmx|pHrl:ШtJZجv:( 6ap݃z݆|Jy㙗w=}Ko\?g<,! |_v0XV X[d]b^'ͅjZɵA!v !lsb+ZW/ 6Z^7\"rx v`X$5$#ʸ#*v!{.*a(8ar GfdNa}%B"&hܛ|b]p_m-tݢ2vz)y()fr G+)bBfvvP*{9Zxvu,Q!z^"+kik9gJ`k.Da>#|H卦99>2I8gH@ `.YLf4|҈Fs ,O% l' map2@,Y(fIi+*{r c2httM?4Ԁn7zEF1I،4g/ `,Gpjh-{5TbӭxyZۋHl砇.SEꬷ.~B! , dihlp,tmx|pHrl:ШtJZجv:ix6hpA!M_ x<@lx}ipo}{jon|nwli{km`^+]k  gt Аg hhet}ڈiV4 7P8(``A A.(p@ɓ| )AI `T$P@M2c\fN<\d1քpΞE=)է֠d ThX!A0+䄻wxfz 2ΛC7Fvy%868rLǔV3eH5m軪'oԬYxӮ!gw< 0OAss,HOu K>aA]k0|Λξ{зWw}<]uwY|] 2ũp\P`l]gAymLxȄ5l2 N@c'RL4g`"8/ʸc=V,B5NV$,fN7b)h8&X{)htezʼ' ȷfY烲!z墆ieIgR-_yv*hpy $Ph@ ݂݊;F1Y t2 d<#Zuׄvkoia&1?b+n&BChkna-[E{Er *+Y@$n).2 )&ty$x-yL2^Rd K%4Va_3VI8Fo5nL(7{0mX2l05$h`[y->sЧ} XKG9'*y[3P_ZXYʩ&-LQKvGhOX*wɫnfx2-E7K1 yxޥIo7ݎ8V8|/o<]7G/Wo'! , dihlp,tmx|pHrl:ШtJZجv:g0tqpچ1O yy}u~m{|~jxqov}jwn~poxzzpeg,]wo  h ih~ؒ juuNХG\A?T@0S0$yA L e'QBX`M2g>в2i\ %\,З&ss$Ԙ(d3̣<.Aȕ1hEH0n , `]x%o߿uƣY38V,ٚ*+1g~C fӊUho!]A !ூ $XFxǑU| 'P'jt{~SaL2#^r VWT̊^@>cCx׉Hbw<"w)VK!wvc is)ʓfyщ̜l0|2 ܉Y pb(gz'ꢡ%t)] 4i$ TO,hmg_' 0]lפ77ÜKXYCeq%(6maPD" kagw]Y }ܮ劽v-k%$o{?/q [;?o^ӻ-T=rtB $Pi &z-&_H=l@]gV|ebfe֘ovih3D8tp x኏xMy#:g"vmAbnuPf=f ^0 ~ Ȣ[xXNhԉt#Q%i22֗43&@]&֠eZt(%ߊȚQV7h1 &ommۗXג)(ڱb `v}A!^sܧSZ*Uu~ͅyyIz'AYٙio6'l^N) Տz4f [AuF6Z]8ڋ}뛆2[z oGȚ@lІG05&H, ɯ&g1P`pŌL|-KV؀Mis=h9s Y&\:ۇhrJ-jW]sud3l^HAʉv&ph-/&ܼ8K8\*,yx-<CXn=*|7 6[TtQo'7П! , dihlp,tmx|pHrl:ШtJZجv:);Oano#~x< 7 yye||g~vx o{fzhwy qrwpnGea^+yɂ  ѵۀlvɊmnA$߷ps` X0eB 0$Ǝ$ir#J `\˘%c:g#M-} JRP/ UN<dP ʧ7 Й,bXʝpNP[qu k  Ob72$38[rFYh3GN|z&sB fWAbtZ0܂v^q;[9l .tv<7V^^!j nYuvYd@ Te]Afuq=[f&Z&R&"{n.quAb̂ HY8̊!8׆!1ɣ`P` ^0Ф\ahYRh؍ȧ%`]F @'\rk2@'3>&z9܌o䵝fxʼn͡Ee'LVݕ@sJ&7N0~'ZN\ـ'c^#lڪ"쉭+z|ĩj]Ǟg' v]c/ڐ^&fE!H^ ݆6k9ʫ0Jp &c*kp@x[辙1]Eez&Zsv%~gRۀ\'8k@|z+i[W6^0cvcs3Is}zb3GUp/GW_\?Kp|wu~Wv'p*tzr_= 1cbD.n;]/o'! , dihlp,tmx|pHrl:ШtJZجv:ph4i7f9=x$ِ6=vx|jwzciu p~xfc~y{~yv{o ea^+ `Ô ºy”mٕm“nгCLۛz 0a@Qƒ2j\@/\ԸAE %H'+l1%Ŕ3IL gN |<3ˡ!BSҠ|Zfŕ-K@ 1EچҪpǚj`Ѯjε sӂT@Ykk \ u%e` WÁ&P0as뎶a1굪1T vI~kmC  $(.wʙWH+!za޲oWλq/x`엝b 'ͻ9rl)m* B)ßeA˜Fle}r e!ah!gtۃɇH3*px8`բXcjI` S%kY@o j j][ed%Y0fl)bg g'E&ϭYfqfpxB  G9|g,u\٘l-yU] Qzn)Z-* 暤[*쫦'†JZuW5͹(\}W'4)!ofnj#zغY5cKVl[,YNpYek\ 0]K"ž H ,UZWia:ہͥɌfJ䍎N2jL,'JlB]eZShj^YArcdrWffC+RʲP킧]دR dP̘Ʊs)'pjp lpo#yˍH`;⒫exٕf=7ޞ;]!o'7'! , dihlp,tmx|pHrl:ШtJZجv:P h)l#1O;~x{}~cvw ~|zx| gkk|kyqzd^+ oypo zB90a GE @qA\@#Ǎ rCɓ?-^=Hl/SE'7G/WB! , dihlp,tmx|pHrl:ШtJZجv:(8!6̮{~6x y}~ul lcp tkizzoye^+ {`фzͿ ьyq[ =k$0  (p@ŋ4\̶#ǏDxAI @b%ʕ f,i2ȟ [s$͡' ZNJFw3FScδ8hhh(@`@@Aʝ R5ΝA$jZl6̭`{뮭p_ niH yH +,ʋ1EzeEw{Y,0ٻ ,{8Z 7 '>aAlktqǵ޶q7UY:k=6m* @c%s}eV3GۂX!3!_ b kZff%B`{}^2&[bȕ|}HXF` "h1qS֠Vf{ӥiYMz7AYc&$tԵpnةxxꉀ2v)Emx.e5)W< i T@$|05"HΑZjt~ިx kE+t}'k[[IJV(+!+Apbی'&XmVq2'>;m"^8kay6tIb2&|- 0r$'d"E]\_Λr Xe-Y G]BޟHh.iYIGF4L\%6dɅVo `c +m;\ Hjt#wYH~0q*vk| .ANdBP\+!/`#ꬷNEEn/ !;tmpfhJ2Ca/LICENSE0000644000175000017500000010333012574355361012700 0ustar lunarlunar GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . tmpfhJ2Ca/spec/0000755000175000017500000000000013026234541012612 5ustar lunarlunartmpfhJ2Ca/spec/coquelicot/0000755000175000017500000000000013026234541014761 5ustar lunarlunartmpfhJ2Ca/spec/coquelicot/rack/0000755000175000017500000000000013026234541015701 5ustar lunarlunartmpfhJ2Ca/spec/coquelicot/rack/multipart_parser_spec.rb0000644000175000017500000003475212574355361022663 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' module Coquelicot::Rack describe MultipartParser do let(:env) { { 'SERVER_NAME' => 'example.org', 'SERVER_PORT' => 80, 'REQUEST_METHOD' => 'POST', 'PATH_INFO' => '/upload', 'CONTENT_TYPE' => "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}", 'CONTENT_LENGTH' => "#{defined?(input) ? input.size : 0}", 'rack.input' => StringIO.new(defined?(input) ? input : '') } } describe '.parse' do context 'when given a block taking one argument' do it 'should run the block with a new parser as argument' do MultipartParser.parse(env) do |p| expect(p).to be_a(MultipartParser) end end end end describe '#start' do context 'when given no block' do it 'should raise an error' do MultipartParser.parse(env) do |p| expect { p.start }.to raise_exception(ArgumentError) end end end context 'when used once' do it 'should call the block on start' do mock = double expect(mock).to receive(:act) MultipartParser.parse(env) do |p| p.start { mock.act } end end end context 'when used twice in a row' do it 'should call both blocks on start' do mock = double expect(mock).to receive(:run).ordered expect(mock).to receive(:walk).ordered MultipartParser.parse(env) do |p| p.start { mock.run } p.start { mock.walk } end end end context 'when used twice with steps inbetween' do it 'should call both blocks on start' do mock = double expect(mock).to receive(:run).ordered expect(mock).to receive(:walk).ordered MultipartParser.parse(env) do |p| p.start { mock.run } p.many_fields p.start { mock.walk } end end end end describe '#many_fields' do let(:input) do <<-MULTIPART_DATA.gsub(/^ */, '').gsub(/\n/, "\r\n") --AaB03x Content-Disposition: form-data; name="one" 1 --AaB03x Content-Disposition: form-data; name="two" 2 --AaB03x Content-Disposition: form-data; name="three" 3 --AaB03x-- MULTIPART_DATA end context 'when used alone' do it 'should call the given block only once' do mock = double expect(mock).to receive(:act).once MultipartParser.parse(env) do |p| p.many_fields do |params| mock.act end end end it 'should call the given block for all fields' do MultipartParser.parse(env) do |p| p.many_fields do |params| expect(params).to be == { 'one' => '1', 'two' => '2', 'three' => '3' } end end end end context 'positioned after "field"' do it 'should call the given block only once' do mock = double expect(mock).to receive(:act).once MultipartParser.parse(env) do |p| p.field :one p.many_fields do |params| mock.act end end end it 'should call the given block for the remaning fields' do MultipartParser.parse(env) do |p| p.field :one p.many_fields do |params| expect(params).to be == { 'two' => '2', 'three' => '3' } end end end end context 'positioned before "field"' do it 'should call the given block only once' do mock = double expect(mock).to receive(:act).once MultipartParser.parse(env) do |p| p.many_fields do |params| mock.act end p.field :three end end it 'should call the given block for the first two fields' do MultipartParser.parse(env) do |p| p.many_fields do |params| expect(params).to be == { 'one' => '1', 'two' => '2' } end p.field :three end end end context 'before and after "field"' do it 'should call each given block only once' do mock = double expect(mock).to receive(:run).ordered expect(mock).to receive(:walk).ordered MultipartParser.parse(env) do |p| p.many_fields do |params| mock.run end p.field :two p.many_fields do |params| mock.walk end end end it 'should call each given block for the first and last fields, respectively' do MultipartParser.parse(env) do |p| p.many_fields do |params| expect(params).to be == { 'one' => '1' } end p.field :two p.many_fields do |params| expect(params).to be == { 'three' => '3' } end end end end end describe '#field' do let(:input) do <<-MULTIPART_DATA.gsub(/^ */, '').gsub(/\n/, "\r\n") --AaB03x Content-Disposition: form-data; name="one" 1 --AaB03x Content-Disposition: form-data; name="two" 2 --AaB03x Content-Disposition: form-data; name="three" 3 --AaB03x-- MULTIPART_DATA end context 'when positioned like the request' do it 'should call a block for each field' do mock = double expect(mock).to receive(:first).with('1').ordered expect(mock).to receive(:second).with('2').ordered expect(mock).to receive(:third).with('3').ordered MultipartParser.parse(env) do |p| p.field(:one) { |value| mock.first value } p.field(:two) { |value| mock.second value } p.field(:three) { |value| mock.third value } end end end context 'when request field does not match' do it 'should issue an error' do expect { MultipartParser.parse(env) do |p| p.field(:whatever) end }.to raise_exception(EOFError) end end context 'when request field does not match after many_fields' do it 'should not call the field block' do mock = double expect(mock).not_to receive(:foo) MultipartParser.parse(env) do |p| p.many_fields p.field(:whatever) { mock.foo } end end end context 'when request field match after many_fields' do it 'should call the field block' do mock = double expect(mock).to receive(:foo).with('3') MultipartParser.parse(env) do |p| p.many_fields p.field(:three) { |value| mock.foo(value) } end end end end describe '#file' do context 'when file is at the end of the request' do let(:file) { __FILE__ } let(:input) { Rack::Multipart::Generator.new( 'field1' => '1', 'field2' => '2', 'field3' => Rack::Multipart::UploadedFile.new(file) ).dump } context 'when positioned like the request' do it 'should call the given block in the right order' do mock = double expect(mock).to receive(:first).ordered expect(mock).to receive(:second).ordered expect(mock).to receive(:third).ordered MultipartParser.parse(env) do |p| p.field(:field1) { |value| mock.first } p.field(:field2) { |value| mock.second } p.file(:field3) do |filename, content_type, reader| mock.third while reader.call; end # flush file data end end end it 'should call the block passing the filename' do filename = File.basename(file) MultipartParser.parse(env) do |p| p.many_fields p.file(:field3) do |filename, content_type, reader| expect(filename).to be == filename while reader.call; end # flush file data end end end it 'should call the block passing the content type' do MultipartParser.parse(env) do |p| p.many_fields p.file(:field3) do |filename, content_type, reader| expect(content_type).to be == 'text/plain' while reader.call; end # flush file data end end end it 'should read the whole file with multiple reader.call' do data = '' MultipartParser.parse(env) do |p| p.many_fields p.file(:field3) do |filename, content_type, reader| buf = '' data << buf until (buf = reader.call).nil? end end expect(data).to be == slurp(file) end end end context 'when file is at the middle of the request' do let(:file) { __FILE__ } let(:input) { Rack::Multipart::Generator.new( 'field1' => '1', 'field2' => Rack::Multipart::UploadedFile.new(file), 'field3' => '3' ).dump } context 'when positioned like the request' do it 'should call the given block in the right order' do mock = double expect(mock).to receive(:first).ordered expect(mock).to receive(:second).ordered expect(mock).to receive(:third).ordered MultipartParser.parse(env) do |p| p.field(:field1) { |value| mock.first } p.file(:field2) do |filename, content_type, reader| mock.second while reader.call; end # flush file data end p.field(:field3) { |value| mock.third } end end it 'should read the whole file with multiple reader.call' do data = '' MultipartParser.parse(env) do |p| p.field(:field1) p.file(:field2) do |filename, content_type, reader| buf = '' data << buf until (buf = reader.call).nil? end p.field(:field3) end expect(data).to be == slurp(file) end end end context 'when there two files follow each others in the request' do let(:file1) { __FILE__ } let(:file2) { File.expand_path('../../../spec_helper.rb', __FILE__) } let(:input) { Rack::Multipart::Generator.new( 'field1' => Rack::Multipart::UploadedFile.new(file1), 'field2' => Rack::Multipart::UploadedFile.new(file2) ).dump } context 'when positioned like the request' do it 'should call the given block in the right order' do mock = double expect(mock).to receive(:first).ordered expect(mock).to receive(:second).ordered MultipartParser.parse(env) do |p| p.file(:field1) do |filename, content_type, reader| mock.first while reader.call; end # flush file data end p.file(:field2) do |filename, content_type, reader| mock.second buf = '' while reader.call; end # flush file data end end end it 'should read the files correctly' do filename1 = File.basename(file1) filename2 = File.basename(file2) data1 = '' data2 = '' MultipartParser.parse(env) do |p| p.file(:field1) do |filename, content_type, reader| expect(filename).to be == filename1 buf = '' data1 << buf until (buf = reader.call).nil? end p.file(:field2) do |filename, content_type, reader| expect(filename).to be == filename2 buf = '' data2 << buf until (buf = reader.call).nil? end end expect(data1).to be == slurp(file1) expect(data2).to be == slurp(file2) end end end end describe '#finish' do context 'when given no block' do it 'should raise an error' do MultipartParser.parse(env) do |p| expect { p.finish }.to raise_exception(ArgumentError) end end end context 'when used once' do it 'should call the block on finish' do mock = double expect(mock).to receive(:act) MultipartParser.parse(env) do |p| p.finish { mock.act } end end end context 'when used twice in a row' do it 'should call both blocks on finish (in reverse order)' do mock = double expect(mock).to receive(:run).ordered expect(mock).to receive(:walk).ordered MultipartParser.parse(env) do |p| p.finish { mock.walk } p.finish { mock.run } end end end context 'when used twice with steps inbetween' do it 'should call both blocks on finish (in reverse order)' do mock = double expect(mock).to receive(:run).ordered expect(mock).to receive(:walk).ordered MultipartParser.parse(env) do |p| p.finish { mock.walk } p.many_fields p.finish { mock.run } end end end end end end tmpfhJ2Ca/spec/coquelicot/rack/upload_spec.rb0000644000175000017500000003036212574517575020552 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'multipart_parser/reader' module Coquelicot::Rack # Helpers method to have more readable code to test Rack responses module RackResponse def status; self[0]; end def headers; self[1]; end def body; buf = ''; self[2].each { |l| buf << l }; buf; end end describe Upload do include_context 'with Coquelicot::Application' let(:lower_app) { lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['Lower']] } } let(:upload) { Upload.new(lower_app) } describe '#call' do subject { upload.call(env).extend(RackResponse) } context 'when receiving GET /' do let(:env) { { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/' } } it 'should pass the request to the lower app' do expect(subject.body).to be == 'Lower' end it 'should ensure the forwarded rack.input is rewindable' do spec_app = double expect(spec_app).to receive(:call) do |env| expect(env['rack.input']).to respond_to(:rewind) [200, {'Content-Type' => 'text/plain'}, ['mock']] end input = StringIO.new('foo=bar&quux=blabb') class << input; undef_method(:rewind); end env['rack.input'] = input Upload.new(spec_app).call(env) end end context 'when called for GET /upload' do let(:env) { { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/upload' } } it 'should pass the request to the lower app' do expect(subject.body).to be == 'Lower' end end context 'when called for POST /upload' do let(:env) { { 'SERVER_NAME' => 'example.org', 'SERVER_PORT' => 80, 'REQUEST_METHOD' => 'POST', 'PATH_INFO' => '/upload', 'CONTENT_TYPE' => "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}", 'CONTENT_LENGTH' => "#{input.size}", 'rack.input' => StringIO.new(input) } } context 'when rack.input is rewindable' do let(:input) { '' } it 'should log a warning during the first request' do logger = double('Logger') expect(logger).to receive(:warn).with(/rewindable/).once env['rack.logger'] = logger # set it to nil to stop Sinatra from messing up upload = Class.new(Upload) { set :logging, nil }.new(lower_app) upload.call(env.dup) # second request, to be sure the warning will show up only once upload.call(env) end end context 'when receiving a request which is not multipart' do let(:input) { 'foo=bar&quux=blabb' } it 'should raise an error' do env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' expect { subject }.to raise_exception(::MultipartParser::NotMultipartError) end end shared_context 'correct POST data' do let(:file) { File.expand_path('../../../spec_helper.rb', __FILE__) } let(:file_content) { File.read(file) } let(:file_key) { 'secret' } let(:input) do < filename)). and_yield.and_yield subject end it 'should increment the depot size' do expect { subject }.to change { Coquelicot.depot.size }.by(1) end end context 'when file is bigger than limit' do include_context 'correct POST data' before(:each) do allow(Coquelicot.settings.authenticator).to receive(:authenticate).and_return(true) allow(Coquelicot.settings).to receive(:max_file_size).and_return(100) end context 'when there is a request Content-Length header' do it 'should bail out with 413 (Request Entity Too Large)' do expect(subject.status).to be == 413 end it 'should display "File is bigger than maximum allowed size"' do expect(subject.body).to include('File is bigger than maximum allowed size') end it 'should display the maximum file size' do expect(subject.body).to include('100 B') end end context 'when there is no request Content-Length header' do before(:each) do env['CONTENT_LENGTH'] = nil end it 'should bail out with 413 (Request Entity Too Large)' do expect(subject.status).to be == 413 end it 'should display "File is bigger than maximum allowed size"' do expect(subject.body).to include('File is bigger than maximum allowed size') end it 'should display the maximum file size' do expect(subject.body).to include('100 B') end end context 'when the request Content-Length header is lying to us' do before(:each) do env['CONTENT_LENGTH'] = 99 end it 'should bail out with 413 (Request Entity Too Large)' do expect(subject.status).to be == 413 end it 'should display "File is bigger than maximum allowed size"' do expect(subject.body).to include('File is bigger than maximum allowed size') end it 'should display the maximum file size' do expect(subject.body).to include('100 B') end end end context 'when receiving a request with other fields after file' do before(:each) do allow(Coquelicot.settings.authenticator).to receive(:authenticate).and_return(true) end let(:file) { File.expand_path('../../../spec_helper.rb', __FILE__) } let(:file_content) { File.read(file) } let(:file_key) { 'secret' } let(:input) do < 'text/plain'}, ['forward mock']]) Upload.new(mock_app).call(env) end it 'should forward interesting params' do mock_app = double expect(mock_app).to receive(:call) do request = Sinatra::Request.new(env) expect(request.params['upload_password']).to be == 'whatever' expect(request.params['expire']).to be == '60' expect(request.params['one_time']).to be == 'true' [200, {'Content-Type' => 'text/plain'}, ['forward mock']] end Upload.new(mock_app).call(env) end it 'should not add a file' do expect { subject }.to_not change { Coquelicot.depot.size } end end context 'when the expiration time is bigger than allowed' do include_context 'correct POST data' before(:each) do allow(Coquelicot.settings.authenticator).to receive(:authenticate).and_return(true) allow(Coquelicot.settings).to receive(:maximum_expire).and_return(5) end it 'should bail out with 403 (Forbidden)' do subject.status == 403 end it 'should display "Forbidden: expiration time too big"' do expect(subject.body).to include('Forbidden: expiration time too big') end it 'should not add a file' do expect { subject }.to_not change { Coquelicot.depot.size } end end end end end end tmpfhJ2Ca/spec/coquelicot/stored_file_spec.rb0000644000175000017500000003706012574517673020646 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'tmpdir' require 'yaml' require 'timecop' require 'base64' module Coquelicot describe StoredFile do shared_context 'create new StoredFile' do around do |example| Dir.mktmpdir('coquelicot') do |tmpdir| @tmpdir = tmpdir example.run end end def create_stored_file(extra_meta = {}) @stored_file_path ||= File.expand_path('stored_file', @tmpdir) @pass = 'secret' @src = __FILE__ @src_length = File.stat(@src).size meta = { 'Expire-at' => 0 } meta.merge!(extra_meta) content = slurp(@src) StoredFile.create(@stored_file_path, @pass, meta) do buf, content = content, nil buf end end end def read_meta(path) File.open(path) do |f| meta = f.readline while buf = f.readline break if buf =~ /^---( |\n)/ meta += buf end YAML.load(meta) end end describe '.get_cipher' do context 'when given an unknown method' do it 'should raise an error' do expect { StoredFile.get_cipher('secret', 'salt', :whatever) }.to raise_error(NameError) end end [ :encrypt, :decrypt ].each do |method| let(:key_len) { 32 } # this is AES-256-CBC let(:iv_len) { 16 } # this is AES-256-CBC let(:hmac_len) { key_len + iv_len } let(:hmac) { (1..hmac_len).to_a.collect { |c| c.chr }.join } context "when given #{method} as method" do it 'should use PKCS5.pbkdf2_hmac_sha1' do expect(OpenSSL::PKCS5).to receive(:pbkdf2_hmac_sha1). with('secret', 'salt', 2000, hmac_len). and_return(hmac) StoredFile.get_cipher('secret', 'salt', method) end it 'should set the key to lower part of the HMAC' do allow(OpenSSL::PKCS5).to receive(:pbkdf2_hmac_sha1). and_return(hmac) cipher = OpenSSL::Cipher.new 'AES-256-CBC' expect(cipher).to receive(:key=).with(hmac[0..key_len-1]) allow(OpenSSL::Cipher).to receive(:new).and_return(cipher) StoredFile.get_cipher('secret', 'salt', method) end it 'should set the IV to the higher part of the HMAC' do allow(OpenSSL::PKCS5).to receive(:pbkdf2_hmac_sha1). and_return(hmac) cipher = OpenSSL::Cipher.new 'AES-256-CBC' expect(cipher).to receive(:iv=).with(hmac[key_len..-1]) allow(OpenSSL::Cipher).to receive(:new).and_return(cipher) StoredFile.get_cipher('secret', 'salt', method) end it 'should return an OpenSSL::Cipher' do cipher = StoredFile.get_cipher('secret', 'salt', method) expect(cipher).to be_a(OpenSSL::Cipher) end end end end describe '.gen_salt' do it 'should return a string of proper length' do StoredFile.gen_salt.length == StoredFile::SALT_LEN end it 'should call OpenSSL::Random every time' do expect(OpenSSL::Random).to receive(:random_bytes). and_return(1, 2) StoredFile.gen_salt == 1 StoredFile.gen_salt == 2 end end describe '.open' do context 'when the given file does not exist' do it 'should raise an error' do expect { StoredFile.open('/nonexistent') }.to raise_error(Errno::ENOENT) end end context 'when the file is not a StoredFile' do it 'should raise an error' do expect { StoredFile.open(__FILE__) }.to raise_error(ArgumentError) end end context 'when giving no pass' do for_all_file_versions do subject { StoredFile.open(stored_file_path) } it 'should read clear metadata' do subject.meta['Coquelicot'] == reference['Coquelicot'] end # XXX: maybe we want a way to know that we can't uncrypt the rest end end context 'when giving a wrong pass' do for_all_file_versions do it 'should raise an error' do expect { StoredFile.open(stored_file_path, 'whatever') }.to raise_error(BadKey) end end end context 'when giving the right pass' do for_all_file_versions do subject { StoredFile.open(stored_file_path, 'secret') } it 'should read the metadata' do subject.meta['Length'] == reference['Length'] end end end end describe '.create' do include_context 'create new StoredFile' context 'when the metadata file already exists' do it 'should raise an error' do @stored_file_path = File.expand_path('stored_file', @tmpdir) FileUtils.touch @stored_file_path expect { create_stored_file }.to raise_error(Errno::EEXIST) end end context 'when the content file already exists' do it 'should raise an error' do @stored_file_path = File.expand_path('stored_file', @tmpdir) FileUtils.touch "#{@stored_file_path}.content" expect { create_stored_file }.to raise_error(Errno::EEXIST) end end context 'in metadata file, clear part' do let(:test_salt) { "\0" * StoredFile::SALT_LEN } let(:expire_at) { Time.at(Time.now.to_i + 60) } # we need to round it at second-level before(:each) do allow(StoredFile).to receive(:gen_salt).and_return(test_salt) create_stored_file('Expire-at' => expire_at) end let(:clear_meta) { read_meta(@stored_file_path) } it 'should write Coquelicot file version' do expect(clear_meta['Coquelicot']).to be == '2.0' end it 'should generate a random Salt' do salt = Base64.decode64(clear_meta['Salt']) expect(salt).to be == test_salt end it 'should record expiration time' do expect(clear_meta['Expire-at']).to be == expire_at end end shared_context 'in encrypted part' do |path_regex| before(:each) do class NullCipher attr_reader :content def initialize; reset; end def reset; @buf, @content = '', nil; end def update(str); @buf << str ; str; end def final; @content = @buf; ''; end end cipher = NullCipher.new allow(StoredFile).to receive(:get_cipher).and_return(cipher) @content = StringIO.new open = File.method(:open) expect(File).to receive(:open).at_least(1).times do |path, *args, &block| if path =~ path_regex ret = block.call(@content) @cipher = cipher.dup ret else open.call(path, *args, &block) end end end end context 'in metadata file, encrypted part' do include_context 'in encrypted part', /stored_file$/ it 'should contain metadata as YAML block' do create_stored_file expect(@cipher.content.split(/^---(?: |\n)/, 3).length).to be == 2 expect(YAML.load(@cipher.content)).to be_a(Hash) end context 'in encrypted metadata' do before(:each) do create_stored_file @meta = YAML.load(@cipher.content) end it 'should contain Length' do expect(@meta['Length']).to be == @src_length end it 'should Created-at' do expect(@meta).to include('Created-at') end end end context 'in encrypted content' do include_context 'in encrypted part', /stored_file\.content$/ before(:each) do create_stored_file end it 'should contain the file content' do expect(@cipher.content).to be == slurp(@src) end it 'should have the whole file for encrypted content' do @content.string == slurp(@src) end end context 'when the given block raise an error' do it 'should not leave files' do expect { path = File.expand_path('stored_file', @tmpdir) begin StoredFile.create(path, 'secret', {}) do raise StandardError.new end rescue StandardError # that was expected! end }.to_not change { Dir.entries(@tmpdir) } end end end describe '#created_at' do context 'with a new file' do include_context 'create new StoredFile' it 'should return the creation time' do Timecop.freeze(Time.local(2012, 1, 1)) do create_stored_file stored_file = StoredFile.open(@stored_file_path, @pass) expect(stored_file.created_at).to be == Time.local(2012, 1, 1) end end end for_all_file_versions do it 'should return the creation time' do expect(stored_file.created_at).to be == Time.at(reference['Created-at']) end end end describe '#expire_at' do context 'with a new file' do include_context 'create new StoredFile' it 'should return the date of expiration' do create_stored_file('Expire-at' => Time.local(2012, 1, 1)) stored_file = StoredFile.open(@stored_file_path, @pass) expect(stored_file.expire_at).to be == Time.local(2012, 1, 1) end end for_all_file_versions do specify { expect(stored_file.expire_at).to be == Time.at(reference['Expire-at']) } end end describe '#expired?' do include_context 'create new StoredFile' context 'when expiration time is in the past' do it 'should return true' do Timecop.freeze do create_stored_file('Expire-at' => Time.now - 60) stored_file = StoredFile.open(@stored_file_path, @pass) expect(stored_file).to be_expired end end end context 'when expiration time is in the future' do it 'should return false' do Timecop.freeze do create_stored_file('Expire-at' => Time.now + 60) stored_file = StoredFile.open(@stored_file_path, @pass) expect(stored_file).not_to be_expired end end end end describe '#one_time_only?' do include_context 'create new StoredFile' context 'when file is labelled as "one time only"' do it 'should be true' do create_stored_file('One-time-only' => true) stored_file = StoredFile.open(@stored_file_path, @pass) expect(stored_file).to be_one_time_only end end context 'when file is not labelled as "one time only"' do it 'should be false' do create_stored_file stored_file = StoredFile.open(@stored_file_path, @pass) expect(stored_file).not_to be_one_time_only end end end describe '#empty!' do for_all_file_versions do include_context 'create new StoredFile' before(:each) do FileUtils.cp Dir.glob("#{stored_file_path}*"), @tmpdir @stored_file_path = File.expand_path('stored_file', @tmpdir) @stored_file = StoredFile.open(@stored_file_path, @pass) end it 'should overwrite file contents with \0' do Dir.glob("#{@stored_file_path}*").each do |path| expect(File).to receive(:open) do |*args, &block| length = File.stat(path).size file = StringIO.new(slurp(path)) block.call(file) expect(file.string).to be == "\0" * length end end @stored_file.empty! end it 'should truncate files' do @stored_file.empty! Dir.glob("#{@stored_file_path}*").each do |path| expect(File.stat(path).size).to be == 0 end end end end describe '#lockfile' do for_all_file_versions do let(:stored_file) { StoredFile.open(stored_file_path, 'secret') } it 'should return a Lockfile' do expect(stored_file.lockfile).to be_a(Lockfile) end it 'should create a Lockfile using the path followed by ".lock"' do expect(Lockfile).to receive(:new) do |path, options| expect(path).to be == "#{stored_file_path}.lock" end stored_file.lockfile end end end describe '#each' do context 'when the right pass has been given' do for_all_file_versions do it 'should output the whole content with several yields' do buf = '' stored_file.each do |data| buf << data end expect(buf).to be == reference['Content'] end end end context 'when no password has been given' do for_all_file_versions do let(:stored_file) { StoredFile.open(stored_file_path) } it 'should raise BadKey' do expect { stored_file.each }.to raise_error(BadKey) end end end end describe '#close' do for_all_file_versions do it 'should reset the cipher' do salt = Base64::decode64(read_meta(stored_file_path)['Salt']) cipher = StoredFile.get_cipher('secret', salt, :decrypt) allow(StoredFile).to receive(:get_cipher).and_return(cipher) stored_file = StoredFile.open(stored_file_path, 'secret') expect(cipher).to receive(:reset) stored_file.close end end context 'when file is "one-time only"' do include_context 'create new StoredFile' before(:each) do create_stored_file('One-time-only' => true) @stored_file = StoredFile.open(@stored_file_path, @pass) # XXX: that is not a nice assumption (at all) @stored_file.lockfile.lock end context 'when the file has not been fully sent' do it 'should leave the content untouched' do begin @stored_file.each { |data| raise StandardError } rescue # do nothing end @stored_file.close another = StoredFile.open(@stored_file_path, @pass) buf = '' another.each { |data| buf << data } expect(buf).to be == slurp(@src) end end context 'when the file has been fully sent' do before(:each) do # read entirely @stored_file.each { |data| nil } end it 'should empty the file' do expect(@stored_file).to receive(:empty!) @stored_file.close end end end end end end tmpfhJ2Ca/spec/coquelicot/auth/0000755000175000017500000000000013026234541015722 5ustar lunarlunartmpfhJ2Ca/spec/coquelicot/auth/ldap_spec.rb0000644000175000017500000000717612574640275020230 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # © 2014 Rowan Thorpe # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'coquelicot/auth/ldap' describe Coquelicot::Auth::LdapAuthenticator do include_context 'with Coquelicot::Application' before(:each) do app.set :authentication_method, :name => :ldap end def authenticate(params) Coquelicot.settings.authenticator.authenticate(params) end describe '.authenticate' do context 'when no LADP server is configured' do it 'should raise an error' do expect { authenticate(:ldap_user => 'user', :ldap_password => 'password') }.to raise_error(Coquelicot::Auth::Error) end end context 'when an LDAP server is configured' do before(:each) do allow(Coquelicot.settings).to receive_messages(:ldap_server => 'example.org', :ldap_port => 636, :ldap_base => 'dc=example,dc=com') @ldap = double('Net::LDAP').as_null_object end context 'when the server is working' do before(:each) do expect(Net::LDAP).to receive(:new).with( :host => 'example.org', :port => 636, :base => 'dc=example,dc=com', :encryption => :simple_tls, :auth => { :method => :anonymous }). and_return(@ldap) end it 'should attempt to login to the server' do expect(@ldap).to receive(:bind_as).with( :base => 'dc=example,dc=com', :filter => '(uid=user)', :password => 'password'). and_return(double('Net::LDAP::PDU')) authenticate(:ldap_user => 'user', :ldap_password => 'password') end it 'should return true when login has been accepted' do allow(@ldap).to receive(:bind_as).and_return(double('Net::LDAP::PDU')) expect(authenticate(:ldap_user => 'user', :ldap_password => 'password')). to be_truthy end it 'should return fales when login has been denied' do allow(@ldap).to receive(:bind_as).and_return(nil) expect(authenticate(:ldap_user => 'user', :ldap_password => 'password')). to be_falsy end it "should properly escape the given username" do expect(@ldap).to receive(:bind_as).with( :base => 'dc=example,dc=com', :filter => '(uid=us\\29er)', :password => 'password'). and_return(double('Net::LDAP::PDU')) authenticate(:ldap_user => 'us)er', :ldap_password => 'password') end end context 'when the server is unreachable' do before(:each) do expect(Net::LDAP).to receive(:new).and_raise(Errno::ECONNREFUSED) end it 'should raise an error' do expect { authenticate(:ldap_user => 'user', :ldap_password => 'password') }. to raise_error(Coquelicot::Auth::Error) end end end end end tmpfhJ2Ca/spec/coquelicot/auth/imap_spec.rb0000644000175000017500000000563512574640272020231 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'coquelicot/auth/imap' describe Coquelicot::Auth::ImapAuthenticator do include_context 'with Coquelicot::Application' before(:each) do app.set :authentication_method, :name => :imap end def authenticate(params) Coquelicot.settings.authenticator.authenticate(params) end describe '.authenticate' do context 'when no IMAP server is configured' do it 'should raise an error' do expect { authenticate(:imap_user => 'user', :imap_password => 'password') }.to raise_error(Coquelicot::Auth::Error) end end context 'when an IMAP server is configured' do before(:each) do allow(Coquelicot.settings).to receive_messages(:imap_server => 'example.org', :imap_port => 993) @imap = double('Net::IMAP').as_null_object end context 'when the server is working' do before(:each) do expect(Net::IMAP).to receive(:new).with('example.org', 993, true).and_return(@imap) end it 'should attempt to login to the server' do expect(@imap).to receive(:login).with('user', 'password') authenticate(:imap_user => 'user', :imap_password => 'password') end it 'should return true when login has been accepted' do allow(@imap).to receive(:login) expect(authenticate(:imap_user => 'user', :imap_password => 'password')). to be_truthy end it 'should return fales when login has been denied' do allow(@imap).to receive(:login).and_raise(Net::IMAP::NoResponseError.new(Net::IMAP::TaggedResponse.new(nil, nil, Net::IMAP::ResponseText.new(nil, :text => 'Login failed.')))) expect(authenticate(:imap_user => 'user', :imap_password => 'password')). to be_falsy end end context 'when the server is unreachable' do before(:each) do expect(Net::IMAP).to receive(:new).and_raise(Errno::ECONNREFUSED) end it 'should raise an error' do expect { authenticate(:imap_user => 'user', :imap_password => 'password') }. to raise_error(Coquelicot::Auth::Error) end end end end end tmpfhJ2Ca/spec/coquelicot/auth/simplepass_spec.rb0000644000175000017500000000367612574640301021457 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'coquelicot/auth/simplepass' describe Coquelicot::Auth::SimplepassAuthenticator do include_context 'with Coquelicot::Application' before(:each) do app.set :authentication_method, :name => :simplepass end def authenticate(params) Coquelicot.settings.authenticator.authenticate(params) end describe '.authenticate' do context 'when no upload password is configured' do before(:each) do allow(Coquelicot.settings).to receive(:upload_password).and_return(nil) end it 'should always return true' do expect(authenticate(:upload_password => nil)).to be_truthy end end context 'when an upload password is set' do before(:each) do allow(Coquelicot.settings).to receive(:upload_password).and_return(Digest::SHA1.hexdigest('uploadpassword')) end it 'should return true if the password is correct' do expect(authenticate(:upload_password => 'uploadpassword')).to be_truthy end it 'should return false if the password is wrong' do expect(authenticate(:upload_password => 'wrong')).to be_falsy end end end end tmpfhJ2Ca/spec/coquelicot/auth/userpass_spec.rb0000644000175000017500000000502113026223065021123 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2016 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'bcrypt' require 'coquelicot/auth/userpass' describe Coquelicot::Auth::UserpassAuthenticator do include_context 'with Coquelicot::Application' before(:each) do app.set :authentication_method, :name => :userpass end def authenticate(params) Coquelicot.settings.authenticator.authenticate(params) end describe '.authenticate' do context 'when no credentials are configured' do it 'should raise an error' do expect { authenticate(:upload_user => 'user', :upload_password => 'password') }.to raise_error(Coquelicot::Auth::Error) end end context 'when credentials are configured' do before(:each) do allow(Coquelicot.settings).to receive_messages( :credentials => { 'ada' => BCrypt::Password.create('lovelace'), 'emma' => BCrypt::Password.create('goldman') } ) end it 'should return false if the login is empty' do expect(authenticate(:upload_login => '', :upload_password => 'something')).to be_falsy end it 'should return false if the password is empty' do expect(authenticate(:upload_login => 'something', :upload_password => '')).to be_falsy end it 'should return false if the user is unknown' do expect(authenticate(:upload_login => 'random', :upload_password => 'password')).to be_falsy end it 'should return false if the password is wrong' do expect(authenticate(:upload_login => 'ada', :upload_password => 'goldman')).to be_falsy end it 'should return false if the user and password are right' do expect(authenticate(:upload_login => 'emma', :upload_password => 'goldman')).to be_falsy end end end end tmpfhJ2Ca/spec/coquelicot/app_spec.rb0000644000175000017500000005461012575752002017113 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'coquelicot/jyraphe_migrator' require 'capybara/dsl' require 'tempfile' require 'timecop' describe Coquelicot::Application do include Rack::Test::Methods include Capybara::DSL Capybara.app = Coquelicot::Application include_context 'with Coquelicot::Application' def upload_password 'secret' end before(:each) do app.set :authentication_method, :name => :simplepass, :upload_password => Digest::SHA1.hexdigest(upload_password) end shared_context 'browser prefers french' do around do |example| begin page.driver.header 'Accept-Language', 'fr-fr;q=1.0, en-gb;q=0.8, en;q=0.7' example.run ensure page.driver.header 'Accept-Language', nil reset_session! end end end describe 'get /' do context 'using the default language' do it 'should display the maximum file size' do visit '/' expect(find(:xpath, '//label[@for="file"]')). to have_content("max. size: #{Coquelicot.settings.max_file_size.as_size}") end context 'when the default expiration is one day' do before do allow(Coquelicot.settings).to receive(:default_expire).and_return(60 * 24) end it 'should have an expiration of one day pre-selected' do visit '/' expect(page).to have_select('expire', :selected => '1 day') end end context 'when the default expiration is one month' do before do allow(Coquelicot.settings).to receive(:default_expire).and_return(60 * 24 * 30) end it 'should have an expiration of one month pre-selected' do visit '/' expect(page).to have_select('expire', :selected => '1 month') end end context 'when I explicitly request french' do it 'should display a page in french' do visit '/' click_link 'fr' expect(page).to have_content('Partager') reset_session! end context 'when I upload an empty file' do around do |example| file = Tempfile.new('coquelicot') begin visit '/' click_link 'fr' fill_in 'upload_password', :with => upload_password attach_file 'file', file.path click_button 'submit' example.run ensure file.close! reset_session! end end it 'should display an error in french' do expect(page).to have_content('Le fichier est vide') end end end end context 'when my browser prefers french' do include_context 'browser prefers french' context 'when I do nothing special' do it 'should display a page in french' do visit '/' expect(page).to have_content('Partager') end context 'when the max upload size is 1 KiB' do around do |example| begin max_file_size = app.max_file_size app.set :max_file_size, 1024 example.run ensure app.set :max_file_size, max_file_size end end it 'should display "1 Kio" as the max upload size' do visit '/' expect(page).to have_content('1 Kio') end context 'when I upload something bigger' do before do visit '/' fill_in 'upload_password', :with => upload_password attach_file 'file', __FILE__ click_button 'submit' end it 'should display an error in french' do expect(page).to have_content('plus gros que la taille maximale') end end end # will fail without ordered Hash, see: # context 'when I upload an empty file' do around do |example| file = Tempfile.new('coquelicot') begin visit '/' fill_in 'upload_password', :with => upload_password attach_file 'file', file.path click_button 'submit' example.run ensure file.close! end end it 'should display an error in french' do expect(page).to have_content('Le fichier est vide') end end end context 'when I explicitly request german' do around(:each) do |example| visit '/' click_link 'de' example.run reset_session! end it 'should display a page in german' do expect(page).to have_content('Verteile') end # will fail without ordered Hash, see: # context 'after an upload' do before do fill_in 'upload_password', :with => upload_password attach_file 'file', __FILE__ click_button 'submit' end it 'should display a page in german' do expect(page).to have_content('Verteile eine weitere Datei') end end end end context 'when an "about text" is set for English and French"' do before(:each) do app.set :about_text, 'en' => 'This is an about text', 'fr' => 'Ceci est un texte' end context 'using the default language' do it 'should display the "about text" in English' do visit '/' expect(find('.about').text).to be == 'This is an about text' end end context 'when I explicitly request French' do it 'should display the "about text" in French' do visit '/' click_link 'fr' expect(find('.about').text).to be == 'Ceci est un texte' reset_session! end end context 'when my browser prefers french' do include_context 'browser prefers french' it 'should display the "about text" in French' do visit '/' expect(find('.about').text).to be == 'Ceci est un texte' end end end context 'when a local Git repository is usable' do before(:each) do # Might be pretty brittle… but will do for now Coquelicot::Helpers.module_eval('remove_class_variable :@@can_provide_git_repository if defined? @@can_provide_git_repository') allow(File).to receive(:readable?).and_return(true) end it 'should offer a "git clone" to the local URI' do visit '/' expect(find('#footer')).to have_content('git clone http://www.example.com/coquelicot.git') end end context 'when a local Git repository is not usable' do before(:each) do # Might be pretty brittle… but will do for now Coquelicot::Helpers.module_eval('remove_class_variable :@@can_provide_git_repository') allow(File).to receive(:readable?) { |p| p.end_with?('.git') } end it 'should offer a link to retrieve the source' do visit '/' expect(find('#footer').text).to match /curl.*gem unpack.*\.gem$/ end it 'should log a warning' do logger = double('Logger') expect(logger).to receive(:warn).with(/Unable to provide access to local Git repository/) allow_any_instance_of(app).to receive(:logger).and_return(logger) visit '/' end it 'should log a warning only on the first request' do logger = double('Logger') expect(logger).to receive(:warn).once allow_any_instance_of(app).to receive(:logger).and_return(logger) visit '/' visit '/' end end context 'when there is no local Git repository' do before(:each) do # Might be pretty brittle… but will do for now Coquelicot::Helpers.module_eval('remove_class_variable :@@can_provide_git_repository') allow(File).to receive(:readable?).and_return(false) end it 'should offer a link to retrieve the source' do visit '/' expect(find('#footer').text).to match /curl.*gem unpack.*\.gem$/ end end end describe 'get /style.css' do before do visit '/style.css' end it 'should send a stylesheet' do expect(page).to have_content('background.jpg') end end describe 'get /README' do before do visit '/README' end it 'should display the README file' do title = File.open(File.expand_path('../../../README', __FILE__)) { |f| f.readline.strip } expect(find('h1')).to have_content(title) end end describe 'get /about-your-data' do it 'should display some info about data retention' do visit '/about-your-data' expect(find('h1')).to have_content('About your data…') end context 'when using SSL' do it 'should notice the connection is encrypted' do visit 'https://example.com/about-your-data' expect(page).to have_content('Exchanges between your computer and example.com are encrypted.') end end context 'when not using SSL' do it 'should notice the connection is encrypted' do visit 'http://example.com/about-your-data' expect(page).to_not have_content('Exchanges between your computer and example.org are encrypted.') end end end describe 'get /source' do context 'when the server hostname is one-cool-hostname' do before(:each) do Coquelicot::Helpers.module_eval('remove_class_variable :@@hostname if defined? @@hostname') allow(Socket).to receive(:gethostname).and_return('one-cool-hostname') visit '/source' end it 'should send a file to be saved' do expect(page.response_headers['Content-Type']).to be == 'application/octet-stream' expect(page.response_headers['Content-Disposition']).to match /^attachment;/ end it 'should send a file with a proposed name correct for coquelicot gem' do expect(page.response_headers['Content-Disposition']).to match /filename="coquelicot-.*\.gem"/ end if defined? Gem::Package.new context 'the downloaded gem' do around(:each) do |example| Tempfile.open('coquelicot-downloaded-gem', :encoding => 'binary') do |gem_file| gem_file.write(page.driver.response.body) @gem = Gem::Package.new(gem_file.path) example.run gem_file.unlink end end it 'should be named "coquelicot"' do expect(@gem.spec.name).to be == 'coquelicot' end it "should have a version containing 'onecoolhostname' for the hostname" do expect(@gem.spec.version.to_s).to match /\.onecoolhostname\./ end it "should have a version containing today's date" do Timecop.freeze(Time.now) do date_str = Date.today.strftime('%Y%m%d') expect(@gem.spec.version.to_s).to match /\.#{date_str}$/ end end it 'should at least contain this spec file' do this_file = __FILE__.gsub(/^.*\/spec/, 'spec') content = nil @gem.spec.files.each do |file| content = File.read(file, :encoding => 'binary') if file.end_with?(this_file) end expect(content).to be == File.read(__FILE__, :encoding => 'binary') end end else context 'the downloaded gem' do around(:each) do |example| Gem::Package.open(StringIO.new(page.driver.response.body)) do |gem| @gem = gem example.run end end it 'should be named "coquelicot"' do expect(@gem.metadata.name).to be == 'coquelicot' end it "should have a version containing 'onecoolhostname' for the hostname" do expect(@gem.metadata.version.to_s).to match /\.onecoolhostname\./ end it "should have a version containing today's date" do Timecop.freeze(Time.now) do date_str = Date.today.strftime('%Y%m%d') expect(@gem.metadata.version.to_s).to match /\.#{date_str}$/ end end it 'should at least contain this spec file' do this_file = __FILE__.gsub(/^.*\/spec/, 'spec') content = nil @gem.each do |file| content = file.read if file.full_name.end_with?(this_file) end expect(content).to be == File.open(__FILE__, 'rb').read end end end end end describe 'post /authenticate' do context 'when sending no password' do before do xhr '/authenticate', :as => :post end it 'should return 403' do expect(last_response.status).to be == 403 end end context 'when giving the right password' do before do xhr '/authenticate', :as => :post, :upload_password => upload_password end it 'should return 200' do expect(last_response.status).to be == 200 end end context 'when giving a wrong password' do before do xhr '/authenticate', :as => :post, :upload_password => 'wrong' end it 'should return 403' do expect(last_response.status).to be == 403 end end context 'when given a request with too much input' do before do # background image is bigger than 5 kiB path = File.expand_path('../../../public/images/background.jpg', __FILE__) post '/authenticate', :file => Rack::Test::UploadedFile.new(path, 'text/plain') end it 'should get status 413 (Request entity too large)' do expect(last_response.status).to be == 413 end end end end describe Coquelicot, '.run!' do include_context 'with Coquelicot::Application' context 'when given no option' do it 'should display help and exit' do stderr = capture(:stderr) do expect { Coquelicot.run! %w{} }.to raise_error(SystemExit) end expect(stderr).to match /Usage:/ end end context 'when using "-h"' do it 'should display help and exit' do stderr = capture(:stderr) do expect { Coquelicot.run! %w{-h} }.to raise_error(SystemExit) end expect(stderr).to match /Usage:/ end end context 'when using "-c "' do it 'should use the given setting file' do settings_file = File.expand_path('../../../conf/settings-default.yml', __FILE__) expect(Coquelicot::Application).to receive(:config_file).with(settings_file) stderr = capture(:stderr) do expect { Coquelicot.run! ['-c', settings_file] }.to raise_error(SystemExit) end end context 'when the given settings file exists' do around(:each) do |example| settings = Tempfile.new('coquelicot') begin settings.write(YAML.dump({ 'depot_path' => '/nonexistent/depot', 'cache_path' => '/nonexistent/cache' })) settings.close @settings_path = settings.path example.run ensure settings.unlink end end it 'should use the depot path defined in the given settings' do # We don't give a command, so exit is expected stderr = capture(:stderr) do expect { Coquelicot.run! ['-c', @settings_path] }.to raise_error(SystemExit) end expect(Coquelicot.settings.depot_path).to be == '/nonexistent/depot' end it 'should use the cache path defined in the given settings' do # We don't give a command, so exit is expected stderr = capture(:stderr) do expect { Coquelicot.run! ['-c', @settings_path] }.to raise_error(SystemExit) end expect(Coquelicot.settings.cache_path).to be == '/nonexistent/cache' end end context 'when the given settings file does not exist' do it 'should display an error' do stderr = capture(:stderr) do expect { Coquelicot.run! %w{-c non-existent.yml} }.to raise_error(SystemExit) end expect(stderr).to match /cannot access/ end end end context 'when given an invalid option' do it 'should display an error' do stderr = capture(:stderr) do expect { Coquelicot.run! %w{--invalid-option} }.to raise_error(SystemExit) end expect(stderr).to match /not a valid option/ end end context 'when given "whatever"' do it 'should display an error' do stderr = capture(:stderr) do expect { Coquelicot.run! %w{whatever} }.to raise_error(SystemExit) end expect(stderr).to match /not a valid command/ end end shared_context 'command accepts options' do context 'when given "--help" option' do it 'should display help and exit' do stderr = capture(:stderr) do expect { Coquelicot.run!([command, '--help']) }.to raise_error(SystemExit) end expect(stderr).to match /Usage:/ end end context 'when given an invalid option' do it 'should display an error' do stderr = capture(:stderr) do expect { Coquelicot.run!([command, '--invalid-option']) }.to raise_error(SystemExit) end expect(stderr).to match /not a valid option/ end end end context 'when given "start"' do let(:command) { 'start' } include_context 'command accepts options' before(:each) do # :stdout_path and :stderr_path should not be set, otherwise RSpec will break! app.set :log, nil end context 'with default options' do it 'should daemonize' do expect(::Unicorn::Launcher).to receive(:daemonize!) allow(::Rainbows::HttpServer).to receive(:new).and_return(double('HttpServer').as_null_object) Coquelicot.run! %w{start} end it 'should start the web server' do allow(::Unicorn::Launcher).to receive(:daemonize!) server = double('HttpServer') expect(server).to receive(:start).and_return(double('Thread').as_null_object) allow(::Rainbows::HttpServer).to receive(:new).and_return(server) Coquelicot.run! %w{start} end end context 'when given the --no-daemon option' do it 'should not daemonize' do expect(::Unicorn::Launcher).to receive(:daemonize!).never allow(::Rainbows::HttpServer).to receive(:new).and_return(double('HttpServer').as_null_object) Coquelicot.run! %w{start --no-daemon} end it 'should set the default configuration' do app.set :pid, @depot_path app.set :listen, ['127.0.0.1:42'] allow_any_instance_of(::Rainbows::HttpServer).to receive(:start) do server = ::Rainbows.server expect(server.config.set[:pid]).to be == @depot_path expect(server.config.set[:listeners]).to be == ['127.0.0.1:42'] double('Thread').as_null_object end Coquelicot.run! %w{start --no-daemon} end it 'should start the web server' do server = double('HttpServer') expect(server).to receive(:start).and_return(double('Thread').as_null_object) allow(::Rainbows::HttpServer).to receive(:new).and_return(server) Coquelicot.run! %w{start --no-daemon} end end context 'when the path setting is set to /coquelicot' do before(:each) do app.set :log, nil $stderr = StringIO.new app.set :path, '/coquelicot' end it 'should map the application to /coquelicot' do allow(::Unicorn::Launcher).to receive(:daemonize!) allow(Coquelicot).to receive(:monkeypatch_half_close) allow(::Rainbows::HttpServer).to receive(:new) do |app, opts| session = Rack::Test::Session.new(app.call) session.get('/coquelicot/') expect(session.last_response).to be_ok expect(session.last_response.body).to match /Coquelicot/ session.get('/') expect(session.last_response.status).to eql(404) double('HttpServer').as_null_object end Coquelicot.run! %w{start} end end end context 'when given "stop"' do let(:command) { 'stop' } include_context 'command accepts options' context 'when the pid file is correct' do let(:pid) { 42 } before(:each) do File.open("#{@depot_path}/pid", 'w') do |f| f.write(pid.to_s) end app.set :pid, "#{@depot_path}/pid" end it 'should stop the web server' do expect(Process).to receive(:kill).with(:TERM, pid) Coquelicot.run! %w{stop} end end context 'when the pid file does not exist' do it 'should error out' do app.set :pid, '/nonexistent' stderr = capture(:stderr) do expect { Coquelicot.run! %w{stop} }.to raise_error(SystemExit) end expect(stderr).to match /Unable to read/ end end context 'when the pid file contains garbage' do before(:each) do File.open("#{@depot_path}/pid", 'w') do |f| f.write('The queerest of the queer') end app.set :pid, "#{@depot_path}/pid" end it 'should errour out' do stderr = capture(:stderr) do expect { Coquelicot.run! %w{stop} }.to raise_error(SystemExit) end expect(stderr).to match /Bad PID file/ end end end context 'when given "gc"' do let(:command) { 'gc' } include_context 'command accepts options' it 'should call gc!' do expect(Coquelicot.depot).to receive(:gc!).once Coquelicot.run! %w{gc} end end context 'when given "migrate-jyraphe"' do let(:args) { %w{all args} } it 'should call the migrator' do expect(Coquelicot::JyrapheMigrator).to receive(:run!).with(args) Coquelicot.run!(%w{migrate-jyraphe} + args) end end end tmpfhJ2Ca/spec/coquelicot/jyraphe_migrator_spec.rb0000644000175000017500000003553212575035521021703 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'coquelicot/jyraphe_migrator' require 'tmpdir' require 'digest/md5' require 'timecop' module Coquelicot describe JyrapheMigrator do include_context 'with Coquelicot::Application' around do |example| @jyraphe_var_path = Dir.mktmpdir('coquelicot') begin Dir.mkdir(File.expand_path('files', @jyraphe_var_path)) Dir.mkdir(File.expand_path('links', @jyraphe_var_path)) example.run ensure FileUtils.remove_entry_secure @jyraphe_var_path end end def add_file_to_jyraphe(file, options = {}) options = { :mime_type => 'text/plain', :expire_at => (Time.now + 3600).to_i }.merge(options) md5 = Digest::MD5.hexdigest(File.read(file)) FileUtils.cp file, File.expand_path('files', @jyraphe_var_path) prefix = options[:one_time_only] ? 'O' : 'R' File.open(File.expand_path("links/#{prefix}#{md5}", @jyraphe_var_path), 'w') do |f| f.write("#{File.basename(file)}\n") f.write("#{options[:mime_type]}\n") f.write("#{File.stat(file).size}\n") f.write("#{options[:file_key]}\n") f.write("#{options[:expire_at]}\n") end end def get_first_migrated_file(pass = nil) old, new = migrator.migrated.to_a[0] if pass.nil? file, pass = new.split('-') else file = new end Coquelicot.depot.get_file(file, pass) end describe '#new' do context 'when the given directory is not a Jyraphe "var" directory' do it 'should raise an error' do expect { JyrapheMigrator.new(Coquelicot.settings.depot_path) }.to raise_error(ArgumentError) end end end describe '#migrate!' do let(:output) { double.as_null_object } let(:migrator) { JyrapheMigrator.new(@jyraphe_var_path, output) } context 'when there is a file in Jyraphe' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby') end it 'should add a new file to Coquelicot' do expect { migrator.migrate! }.to change { Coquelicot.depot.size }.by(1) end context 'when I read the file in Coquelicot' do before(:each) { migrator.migrate! } subject { get_first_migrated_file } it 'should have the same length' do expect(subject.meta['Length']).to be == File.stat(__FILE__).size end it 'should have the same mime type' do expect(subject.meta['Content-type']).to be == 'application/x-ruby' end end end context 'when there is two files in Jyraphe' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby') add_file_to_jyraphe(File.expand_path('../../../README', __FILE__), :mime_type => 'text/plain') end it 'should add two files to Coquelicot' do expect { migrator.migrate! }.to change { Coquelicot.depot.size }.by(2) end end context 'when there is a "one-time only" file in Jyraphe' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby', :one_time_only => true) end it 'should add a file to Coquelicot' do expect { migrator.migrate! }.to change { Coquelicot.depot.size }.by(1) end context 'when I read the file in Coquelicot' do before(:each) { migrator.migrate! } subject { get_first_migrated_file } it 'should be labeled as "one-time only"' do expect(subject.meta['One-time-only']).to be true end end end context 'when there is a password protected file in Jyraphe' do let(:pass) { 'secret' } before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby', :file_key => pass) end it 'should add a file to Coquelicot' do expect { migrator.migrate! }.to change { Coquelicot.depot.size }.by(1) end context 'when I read the file in Coquelicot' do before(:each) { migrator.migrate! } it 'should need a pass' do stored_file = get_first_migrated_file expect(stored_file.meta).not_to include('Content-type') end it 'should be readable with a wrong pass' do expect { get_first_migrated_file('wrong') }.to raise_error(BadKey) end it 'should be readable with the same pass' do stored_file = get_first_migrated_file(pass) expect(stored_file.meta).to include('Content-type') end end end context 'when there is a never expiring file in Jyraphe' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby', :expire_at => -1) end it 'should issue a warning' do expect(output).to receive(:puts). with(/^W: R[0-9a-z]{32} expiration time has been reduced/) migrator.migrate! end it 'should add a file to Coquelicot' do expect { migrator.migrate! }.to change { Coquelicot.depot.size }.by(1) end context 'when I read the file in Coquelicot' do it 'should have the maximum expiration time' do Timecop.freeze(Time.now) do migrator.migrate! stored_file = get_first_migrated_file expect(stored_file.meta['Expire-at']).to be == (Time.now + Coquelicot.settings.maximum_expire * 60).to_i end end end end context 'when there is a file in Jyraphe which expires after the maximum allowed time' do before(:each) do add_file_to_jyraphe( __FILE__, :mime_type => 'application/x-ruby', :expire_at => (Time.now + Coquelicot.settings.maximum_expire * 60 + 5).to_i) end it 'should issue a warning' do expect(output).to receive(:puts). with(/^W: R[0-9a-z]{32} expiration time has been reduced/) migrator.migrate! end it 'should add a file to Coquelicot' do expect { migrator.migrate! }.to change { Coquelicot.depot.size }.by(1) end context 'when I read the file in Coquelicot' do it 'should have the maximum expiration time' do Timecop.freeze(Time.now) do migrator.migrate! stored_file = get_first_migrated_file expect(stored_file.meta['Expire-at']).to be == (Time.now + Coquelicot.settings.maximum_expire * 60).to_i end end end end context 'when there is a file in Jyraphe which has a bad expiration time' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby', :expire_at => 'unparseable') end it 'should issue a warning' do expect(output).to receive(:puts). with(/^W: R[0-9a-z]{32} has an unparseable expiration time\. Skipping\./) migrator.migrate! end it 'should not add a file to Coquelicot' do expect { migrator.migrate! }.to_not change { Coquelicot.depot.size } end end context 'when the file associated with a link is missing' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby') FileUtils.rm(File.expand_path(File.basename(__FILE__), "#{@jyraphe_var_path}/files")) end it 'should issue a warning' do expect(output).to receive(:puts). with(/^W: R[0-9a-z]{32} refers to a non-existent file\. Skipping\./) migrator.migrate! end it 'should not add a file to Coquelicot' do expect { migrator.migrate! }.to_not change { Coquelicot.depot.size } end end context 'when a file size does not match the link size' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby') File.truncate(File.expand_path(File.basename(__FILE__), "#{@jyraphe_var_path}/files"), 0) end it 'should issue a warning' do expect(output).to receive(:puts). with(/^W: R[0-9a-z]{32} refers to a file with mismatching size\. Skipping\./) migrator.migrate! end it 'should not add a file to Coquelicot' do expect { migrator.migrate! }.to_not change { Coquelicot.depot.size } end end end describe '#apache_rewrites' do let(:output) { double.as_null_object } let(:migrator) { JyrapheMigrator.new(@jyraphe_var_path, output) } context 'when there was nothing to migrate' do before(:each) { migrator.migrate! } subject { migrator.apache_rewrites } it { should == '' } end context 'when there was a file migrated' do before(:each) do add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby') migrator.migrate! end it 'should begin with "RewriteEngine on"' do expect(migrator.apache_rewrites).to satisfy do |s| s.start_with?('RewriteEngine on') end end context 'when given no prefix' do it 'should contain a rule appropriate for an .htaccess' do jyraphe, coquelicot = migrator.migrated.to_a[0] expect(migrator.apache_rewrites.split("\n")).to include( "RewriteRule ^file-#{jyraphe}$ #{coquelicot} [L,R=301]") end end context 'when given a prefix' do it 'should contain rules with the prefix' do jyraphe, coquelicot = migrator.migrated.to_a[0] expect(migrator.apache_rewrites('/dl/').split("\n")).to include( "RewriteRule ^/dl/file-#{jyraphe}$ /dl/#{coquelicot} [L,R=301]") end end end context 'when there was two files migrated' do before(:each) do add_file_to_jyraphe(File.expand_path('../../../README', __FILE__), :mime_type => 'text/plain', :one_time_only => true) add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby') migrator.migrate! end context 'when given no prefix' do it 'should contain two rule appropriate for an .htaccess' do jyraphe, coquelicot = migrator.migrated.to_a[0] expect(migrator.apache_rewrites.split("\n")).to include( "RewriteRule ^file-#{jyraphe}$ #{coquelicot} [L,R=301]") jyraphe, coquelicot = migrator.migrated.to_a[1] expect(migrator.apache_rewrites.split("\n")).to include( "RewriteRule ^file-#{jyraphe}$ #{coquelicot} [L,R=301]") end end end end describe '.run!' do context 'when given no option' do before(:each) do allow(JyrapheMigrator).to receive(:new).and_return(double.as_null_object) end it 'should display usage and exit with an error' do stderr = capture(:stderr) do expect { JyrapheMigrator.run! [] }.to raise_error(SystemExit) end expect(stderr).to match /Usage:/ expect(stderr).to match /--help for more details/ end end context 'when given a path to a random directory' do it 'should display an error' do path = File.expand_path('files', @jyraphe_var_path) stderr = capture(:stderr) do expect { JyrapheMigrator.run! [path] }.to raise_error(SystemExit) end expect(stderr).to match /is not a Jyraphe/ end end context 'when given a path to a Jyraphe var directory' do it 'should use the default depot path' do allow(JyrapheMigrator).to receive(:new).and_return(double.as_null_object) capture(:stdout) do JyrapheMigrator.run! [@jyraphe_var_path] end expect(Coquelicot.settings.depot_path).to be == @depot_path end it 'should migrate using the given Jyraphe var directory' do migrator = double('JyrapheMigrator').as_null_object expect(migrator).to receive(:migrate!) expect(JyrapheMigrator).to receive(:new).with(@jyraphe_var_path). and_return(migrator) capture(:stdout) do JyrapheMigrator.run! [@jyraphe_var_path] end end it 'should print rewrite rules after migrating' do migrator = double('JyrapheMigrator').as_null_object expect(migrator).to receive(:migrate!).ordered expect(migrator).to receive(:apache_rewrites).ordered.and_return('rules') allow(JyrapheMigrator).to receive(:new).and_return(migrator) stdout = capture(:stdout) do JyrapheMigrator.run! [@jyraphe_var_path] end expect(stdout.strip).to be == 'rules' end end context 'when using "-p"' do it 'should print rewrite rules using the given prefix' do migrator = double('JyrapheMigrator').as_null_object expect(migrator).to receive(:apache_rewrites).with('/prefix/') allow(JyrapheMigrator).to receive(:new).and_return(migrator) capture(:stdout) do JyrapheMigrator.run! ['-p', '/prefix/', @jyraphe_var_path] end end end context 'when using "-h"' do it 'should display help and exit' do stderr = capture(:stderr) do expect { JyrapheMigrator.run! ['-h'] }.to raise_error(SystemExit) end expect(stderr).to match /Usage:/ end end end end end tmpfhJ2Ca/spec/coquelicot/depot_spec.rb0000644000175000017500000002460412575035533017451 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'spec_helper' require 'timecop' module Coquelicot describe Depot do describe '.new' do it 'should record the given path' do depot = Depot.new('/test') expect(depot.path).to be == '/test' end end around do |example| Dir.mktmpdir('coquelicot') do |tmpdir| @tmpdir = tmpdir example.run end end let(:depot) { Depot.new(@tmpdir) } let(:pass) { 'secret'} let(:expire) { 60 } def add_file content = 'Content' depot.add_file(pass, { 'Expire-at' => Time.now + expire }) do buf, content = content, nil buf end end describe '#add_file' do it 'should generate a random file name' do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file end context 'when it generates a name that is already in use' do it 'should find another name' do FileUtils.touch File.expand_path('file', @tmpdir) expect(depot).to receive(:gen_random_file_name). and_return('file', 'another', 'link') add_file end end context 'when it generates the same name with another client' do it 'should find another name' do expect(depot).to receive(:gen_random_file_name). and_return('file', 'another', 'link') raised = false expect(StoredFile).to receive(:create).ordered. with(/\/file$/, pass, instance_of(Hash)). and_raise(Errno::EEXIST.new(File.expand_path('file', @tmpdir))) expect(StoredFile).to receive(:create).ordered. with(/\/another$/, pass, instance_of(Hash)) add_file end end context 'when the given block raises an error' do it 'should not add a file to the depot' do expect { begin depot.add_file(pass, {}) { raise StandardError.new } rescue StandardError # pass end }.to_not change { depot.size } end it 'should cleanup any created files' do expect { begin depot.add_file(pass, {}) { raise StandardError.new } rescue StandardError # pass end }.to_not change { Dir.entries(@tmpdir) } end end context 'when given a fine content provider block' do it 'should add a new file in the depot' do expect { add_file }.to change { depot.size }.by(1) end it 'should return a randomly generated link name' do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') link = add_file expect(link).to be == 'link' end it 'should add the new link to the ".links" file' do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file expect(File.read(File.expand_path('.links', @tmpdir))).to match /^link\s+file$/ end context 'when it generates a link name that is already taken' do before(:each) do allow(depot).to receive(:gen_random_file_name). and_return('file', 'link', 'another', 'link', 'another_link') # add 'link' pointing to 'file' add_file # now it should add 'another_link' -> 'another' @link = add_file end it 'should not overwrite the existing link' do expect(depot.size).to be == 2 end it 'should find another name' do expect(@link).to be == 'another_link' end end end end describe '#get_file' do context 'when there is no link with the given name' do it 'should return nil' do expect(depot.get_file('link')).to be_nil end end context 'when there is a link with the given name' do before(:each) do @link = add_file end it 'should return a StoredFile' do expect(depot.get_file(@link)).to be_a(StoredFile) end it 'should return the right file' do stored_file = depot.get_file(@link, pass) buf = '' stored_file.each { |data| buf << data } expect(buf).to be == 'Content' end end context 'when there is a link with no matching file' do before(:each) do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') @link = add_file File.unlink File.expand_path('file', @tmpdir) end it 'should return nil' do expect(depot.get_file(@link)).to be_nil end end end describe '#file_exists?' do subject { depot.file_exists?('link') } context 'when there is no link with the given name' do it { should_not be true } end context 'when there is a link with the given name' do before(:each) do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file end it { should be true } end context 'when there is a link with no matching file' do before(:each) do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file File.unlink File.expand_path('file', @tmpdir) end it { should_not be true } end end describe '#gc!' do context 'when there is no files' do it 'should do nothing' do expect { depot.gc! }.to_not change { Dir.entries(@tmpdir) } end end context 'when there is a file' do before(:each) do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file end context 'before it is expired' do subject { depot.gc! } it 'should not remove links' do expect { subject }.to_not change { depot.size } end it 'should not remove files' do expect { subject }.to_not change { Dir.entries(@tmpdir) } end it 'should not empty the file' do expect { subject }.to_not change { File.stat(File.expand_path('file', @tmpdir)).size } end end context 'after it is expired' do subject { Timecop.travel(Date.today + 2) { depot.gc! } } it 'should not remove links' do expect { subject }.to_not change { depot.size } end it 'should not remove files' do expect { subject }. to_not change { Dir.entries(@tmpdir) } end it 'should empty the file' do expect { subject }. to change { File.stat(File.expand_path('file', @tmpdir)).size }.to(0) end end context 'after the gone period and two collections' do let(:now) { Time.now + Coquelicot.settings.gone_period * 61 } subject { Timecop.travel(now) { depot.gc!; depot.gc! } } it 'should remove links' do expect { subject }.to change { depot.size }.from(1).to(0) end it 'should remove files' do subject expect(Dir.glob("#{@tmpdir}/file*")).to be_empty end end end context 'when there is a file that expires after the gone period' do let(:expire) { Coquelicot.settings.gone_period + 42 } before(:each) do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file end context 'after the gone period' do let(:now) { Time.now + Coquelicot.settings.gone_period * 61 } subject { Timecop.travel(now) { depot.gc! } } it 'should not remove links' do expect { subject }.to_not change { depot.size } end it 'should not remove files' do expect { subject }. to_not change { Dir.entries(@tmpdir) } end end end context 'when there is a link but no associated file' do before(:each) do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file File.unlink File.expand_path('file', @tmpdir) end it 'should remove the link' do expect { depot.gc! }.to change { depot.size }.from(1).to(0) end end context 'when there is a corrupted file' do before(:each) do expect(depot).to receive(:gen_random_file_name). and_return('file', 'link') add_file @file_path = File.expand_path('file', @tmpdir) File.open(@file_path, 'w') do |f| f.write('gibberish') end end it 'should print a warning on stderr' do stderr = capture(:stderr) do depot.gc! end expect(stderr).to match /^W: #{@file_path} is not a Coquelicot file\. Skipping\./ end it 'should not remove files' do capture(:stderr) do expect { depot.gc! }.to_not change { Dir.entries(@tmpdir) } end end end end describe '#size' do subject { depot.size } context 'when there is no files' do it { should == 0 } end context 'when there is a file' do before(:each) { add_file } it { should == 1 } end context 'when there is two files' do before(:each) { add_file ; add_file } it { should == 2 } end end end end tmpfhJ2Ca/spec/spec_helper.rb0000644000175000017500000000653312575035533015447 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ENV['RACK_ENV'] = 'test' require 'rubygems' require 'bundler' Bundler.require(:default, :development) Bundler.setup require 'rack/test' require 'rspec' require 'stringio' # Set a default time zone. Otherwise tests using Timecop fail when # activesupport is loaded. require 'active_support/time' Time.zone = 'UTC' require 'coquelicot' shared_context 'with Coquelicot::Application' do def app Coquelicot::Application end before do app.set :environment, :test end around(:each) do |example| path = Dir.mktmpdir('coquelicot') begin @depot_path = File.join(path, 'depot') FileUtils.mkdir(@depot_path) app.set :depot_path, @depot_path @cache_path = File.join(path, 'cache') FileUtils.mkdir(@cache_path) app.set :cache_path, @cache_path example.run ensure FileUtils.remove_entry_secure path end end end module StoredFileHelpers FIXTURES = { 'LICENSE-secret-1.0' => '1.0', 'small-secret-1.0' => 'small 1.0', 'LICENSE-secret-2.0' => '2.0' } shared_context 'with a StoredFile fixture' do |name| let(:stored_file_path) { File.expand_path("../fixtures/#{name}/stored_file", __FILE__) } let(:stored_file) { Coquelicot::StoredFile.open(stored_file_path, 'secret') } let(:reference) { YAML.load_file(File.expand_path("../fixtures/#{name}/reference", __FILE__)) } end def for_all_file_versions(&block) FIXTURES.each_pair do |name, description| context "with a #{description} file" do include_context 'with a StoredFile fixture', name instance_eval &block end end end end module CoquelicotSpecHelpers # written by cash on # http://rails-bestpractices.com/questions/1-test-stdin-stdout-in-rspec def capture(*streams) streams.map! { |stream| stream.to_s } begin result = StringIO.new streams.each { |stream| eval "$#{stream} = result" } yield ensure streams.each { |stream| eval("$#{stream} = #{stream.upcase}") } end result.string end if defined? Encoding def slurp(path) File.read(path, :encoding => 'binary') end else def slurp(path) File.read(path) end end # borrowed from https://gist.github.com/cyx/1325708 def xhr(path, params = {}) verb = params.delete(:as) || :get send(verb, path, params, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest') end end ::RSpec.configure do |c| c.extend StoredFileHelpers c.include CoquelicotSpecHelpers c.expect_with :rspec do |e| e.syntax = :expect end end tmpfhJ2Ca/spec/fixtures/0000755000175000017500000000000013026234541014463 5ustar lunarlunartmpfhJ2Ca/spec/fixtures/LICENSE-secret-2.0/0000755000175000017500000000000013026234541017325 5ustar lunarlunartmpfhJ2Ca/spec/fixtures/LICENSE-secret-2.0/reference0000644000175000017500000010764612574355361021237 0ustar lunarlunar--- Created-at: 1331051078 Content: " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. \n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n Preamble\n\n The GNU Affero General Public License is a free, copyleft license for\n\ software and other kinds of works, specifically designed to ensure\n\ cooperation with the community in the case of network server software.\n\n The licenses for most software and other practical works are designed\n\ to take away your freedom to share and change the works. By contrast,\n\ our General Public Licenses are intended to guarantee your freedom to\n\ share and change all versions of a program--to make sure it remains free\n\ software for all its users.\n\n When we speak of free software, we are referring to freedom, not\n\ price. Our General Public Licenses are designed to make sure that you\n\ have the freedom to distribute copies of free software (and charge for\n\ them if you wish), that you receive source code or can get it if you\n\ want it, that you can change the software or use pieces of it in new\n\ free programs, and that you know you can do these things.\n\n Developers that use our General Public Licenses protect your rights\n\ with two steps: (1) assert copyright on the software, and (2) offer\n\ you this License which gives you legal permission to copy, distribute\n\ and/or modify the software.\n\n A secondary benefit of defending all users' freedom is that\n\ improvements made in alternate versions of the program, if they\n\ receive widespread use, become available for other developers to\n\ incorporate. Many developers of free software are heartened and\n\ encouraged by the resulting cooperation. However, in the case of\n\ software used on network servers, this result may fail to come about.\n\ The GNU General Public License permits making a modified version and\n\ letting the public access it on a server without ever releasing its\n\ source code to the public.\n\n The GNU Affero General Public License is designed specifically to\n\ ensure that, in such cases, the modified source code becomes available\n\ to the community. It requires the operator of a network server to\n\ provide the source code of the modified version running there to the\n\ users of that server. Therefore, public use of a modified version, on\n\ a publicly accessible server, gives the public access to the source\n\ code of the modified version.\n\n An older license, called the Affero General Public License and\n\ published by Affero, was designed to accomplish similar goals. This is\n\ a different license, not a version of the Affero GPL, but Affero has\n\ released a new version of the Affero GPL which permits relicensing under\n\ this license.\n\n The precise terms and conditions for copying, distribution and\n\ modification follow.\n\n TERMS AND CONDITIONS\n\n 0. Definitions.\n\n \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n \"Copyright\" also means copyright-like laws that apply to other kinds of\n\ works, such as semiconductor masks.\n\n \"The Program\" refers to any copyrightable work licensed under this\n\ License. Each licensee is addressed as \"you\". \"Licensees\" and\n\ \"recipients\" may be individuals or organizations.\n\n To \"modify\" a work means to copy from or adapt all or part of the work\n\ in a fashion requiring copyright permission, other than the making of an\n\ exact copy. The resulting work is called a \"modified version\" of the\n\ earlier work or a work \"based on\" the earlier work.\n\n A \"covered work\" means either the unmodified Program or a work based\n\ on the Program.\n\n To \"propagate\" a work means to do anything with it that, without\n\ permission, would make you directly or secondarily liable for\n\ infringement under applicable copyright law, except executing it on a\n\ computer or modifying a private copy. Propagation includes copying,\n\ distribution (with or without modification), making available to the\n\ public, and in some countries other activities as well.\n\n To \"convey\" a work means any kind of propagation that enables other\n\ parties to make or receive copies. Mere interaction with a user through\n\ a computer network, with no transfer of a copy, is not conveying.\n\n An interactive user interface displays \"Appropriate Legal Notices\"\n\ to the extent that it includes a convenient and prominently visible\n\ feature that (1) displays an appropriate copyright notice, and (2)\n\ tells the user that there is no warranty for the work (except to the\n\ extent that warranties are provided), that licensees may convey the\n\ work under this License, and how to view a copy of this License. If\n\ the interface presents a list of user commands or options, such as a\n\ menu, a prominent item in the list meets this criterion.\n\n 1. Source Code.\n\n The \"source code\" for a work means the preferred form of the work\n\ for making modifications to it. \"Object code\" means any non-source\n\ form of a work.\n\n A \"Standard Interface\" means an interface that either is an official\n\ standard defined by a recognized standards body, or, in the case of\n\ interfaces specified for a particular programming language, one that\n\ is widely used among developers working in that language.\n\n The \"System Libraries\" of an executable work include anything, other\n\ than the work as a whole, that (a) is included in the normal form of\n\ packaging a Major Component, but which is not part of that Major\n\ Component, and (b) serves only to enable use of the work with that\n\ Major Component, or to implement a Standard Interface for which an\n\ implementation is available to the public in source code form. A\n\ \"Major Component\", in this context, means a major essential component\n\ (kernel, window system, and so on) of the specific operating system\n\ (if any) on which the executable work runs, or a compiler used to\n\ produce the work, or an object code interpreter used to run it.\n\n The \"Corresponding Source\" for a work in object code form means all\n\ the source code needed to generate, install, and (for an executable\n\ work) run the object code and to modify the work, including scripts to\n\ control those activities. However, it does not include the work's\n\ System Libraries, or general-purpose tools or generally available free\n\ programs which are used unmodified in performing those activities but\n\ which are not part of the work. For example, Corresponding Source\n\ includes interface definition files associated with source files for\n\ the work, and the source code for shared libraries and dynamically\n\ linked subprograms that the work is specifically designed to require,\n\ such as by intimate data communication or control flow between those\n\ subprograms and other parts of the work.\n\n The Corresponding Source need not include anything that users\n\ can regenerate automatically from other parts of the Corresponding\n\ Source.\n\n The Corresponding Source for a work in source code form is that\n\ same work.\n\n 2. Basic Permissions.\n\n All rights granted under this License are granted for the term of\n\ copyright on the Program, and are irrevocable provided the stated\n\ conditions are met. This License explicitly affirms your unlimited\n\ permission to run the unmodified Program. The output from running a\n\ covered work is covered by this License only if the output, given its\n\ content, constitutes a covered work. This License acknowledges your\n\ rights of fair use or other equivalent, as provided by copyright law.\n\n You may make, run and propagate covered works that you do not\n\ convey, without conditions so long as your license otherwise remains\n\ in force. You may convey covered works to others for the sole purpose\n\ of having them make modifications exclusively for you, or provide you\n\ with facilities for running those works, provided that you comply with\n\ the terms of this License in conveying all material for which you do\n\ not control copyright. Those thus making or running the covered works\n\ for you must do so exclusively on your behalf, under your direction\n\ and control, on terms that prohibit them from making any copies of\n\ your copyrighted material outside their relationship with you.\n\n Conveying under any other circumstances is permitted solely under\n\ the conditions stated below. Sublicensing is not allowed; section 10\n\ makes it unnecessary.\n\n 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n No covered work shall be deemed part of an effective technological\n\ measure under any applicable law fulfilling obligations under article\n\ 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n\ similar laws prohibiting or restricting circumvention of such\n\ measures.\n\n When you convey a covered work, you waive any legal power to forbid\n\ circumvention of technological measures to the extent such circumvention\n\ is effected by exercising rights under this License with respect to\n\ the covered work, and you disclaim any intention to limit operation or\n\ modification of the work as a means of enforcing, against the work's\n\ users, your or third parties' legal rights to forbid circumvention of\n\ technological measures.\n\n 4. Conveying Verbatim Copies.\n\n You may convey verbatim copies of the Program's source code as you\n\ receive it, in any medium, provided that you conspicuously and\n\ appropriately publish on each copy an appropriate copyright notice;\n\ keep intact all notices stating that this License and any\n\ non-permissive terms added in accord with section 7 apply to the code;\n\ keep intact all notices of the absence of any warranty; and give all\n\ recipients a copy of this License along with the Program.\n\n You may charge any price or no price for each copy that you convey,\n\ and you may offer support or warranty protection for a fee.\n\n 5. Conveying Modified Source Versions.\n\n You may convey a work based on the Program, or the modifications to\n\ produce it from the Program, in the form of source code under the\n\ terms of section 4, provided that you also meet all of these conditions:\n\n a) The work must carry prominent notices stating that you modified\n it, and giving a relevant date.\n\n b) The work must carry prominent notices stating that it is\n released under this License and any conditions added under section\n 7. This requirement modifies the requirement in section 4 to\n \"keep intact all notices\".\n\n c) You must license the entire work, as a whole, under this\n License to anyone who comes into possession of a copy. This\n License will therefore apply, along with any applicable section 7\n additional terms, to the whole of the work, and all its parts,\n regardless of how they are packaged. This License gives no\n permission to license the work in any other way, but it does not\n invalidate such permission if you have separately received it.\n\n d) If the work has interactive user interfaces, each must display\n Appropriate Legal Notices; however, if the Program has interactive\n interfaces that do not display Appropriate Legal Notices, your\n work need not make them do so.\n\n A compilation of a covered work with other separate and independent\n\ works, which are not by their nature extensions of the covered work,\n\ and which are not combined with it such as to form a larger program,\n\ in or on a volume of a storage or distribution medium, is called an\n\ \"aggregate\" if the compilation and its resulting copyright are not\n\ used to limit the access or legal rights of the compilation's users\n\ beyond what the individual works permit. Inclusion of a covered work\n\ in an aggregate does not cause this License to apply to the other\n\ parts of the aggregate.\n\n 6. Conveying Non-Source Forms.\n\n You may convey a covered work in object code form under the terms\n\ of sections 4 and 5, provided that you also convey the\n\ machine-readable Corresponding Source under the terms of this License,\n\ in one of these ways:\n\n a) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by the\n Corresponding Source fixed on a durable physical medium\n customarily used for software interchange.\n\n b) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by a\n written offer, valid for at least three years and valid for as\n long as you offer spare parts or customer support for that product\n model, to give anyone who possesses the object code either (1) a\n copy of the Corresponding Source for all the software in the\n product that is covered by this License, on a durable physical\n medium customarily used for software interchange, for a price no\n more than your reasonable cost of physically performing this\n conveying of source, or (2) access to copy the\n Corresponding Source from a network server at no charge.\n\n c) Convey individual copies of the object code with a copy of the\n written offer to provide the Corresponding Source. This\n alternative is allowed only occasionally and noncommercially, and\n only if you received the object code with such an offer, in accord\n with subsection 6b.\n\n d) Convey the object code by offering access from a designated\n place (gratis or for a charge), and offer equivalent access to the\n Corresponding Source in the same way through the same place at no\n further charge. You need not require recipients to copy the\n Corresponding Source along with the object code. If the place to\n copy the object code is a network server, the Corresponding Source\n may be on a different server (operated by you or a third party)\n that supports equivalent copying facilities, provided you maintain\n clear directions next to the object code saying where to find the\n Corresponding Source. Regardless of what server hosts the\n Corresponding Source, you remain obligated to ensure that it is\n available for as long as needed to satisfy these requirements.\n\n e) Convey the object code using peer-to-peer transmission, provided\n you inform other peers where the object code and Corresponding\n Source of the work are being offered to the general public at no\n charge under subsection 6d.\n\n A separable portion of the object code, whose source code is excluded\n\ from the Corresponding Source as a System Library, need not be\n\ included in conveying the object code work.\n\n A \"User Product\" is either (1) a \"consumer product\", which means any\n\ tangible personal property which is normally used for personal, family,\n\ or household purposes, or (2) anything designed or sold for incorporation\n\ into a dwelling. In determining whether a product is a consumer product,\n\ doubtful cases shall be resolved in favor of coverage. For a particular\n\ product received by a particular user, \"normally used\" refers to a\n\ typical or common use of that class of product, regardless of the status\n\ of the particular user or of the way in which the particular user\n\ actually uses, or expects or is expected to use, the product. A product\n\ is a consumer product regardless of whether the product has substantial\n\ commercial, industrial or non-consumer uses, unless such uses represent\n\ the only significant mode of use of the product.\n\n \"Installation Information\" for a User Product means any methods,\n\ procedures, authorization keys, or other information required to install\n\ and execute modified versions of a covered work in that User Product from\n\ a modified version of its Corresponding Source. The information must\n\ suffice to ensure that the continued functioning of the modified object\n\ code is in no case prevented or interfered with solely because\n\ modification has been made.\n\n If you convey an object code work under this section in, or with, or\n\ specifically for use in, a User Product, and the conveying occurs as\n\ part of a transaction in which the right of possession and use of the\n\ User Product is transferred to the recipient in perpetuity or for a\n\ fixed term (regardless of how the transaction is characterized), the\n\ Corresponding Source conveyed under this section must be accompanied\n\ by the Installation Information. But this requirement does not apply\n\ if neither you nor any third party retains the ability to install\n\ modified object code on the User Product (for example, the work has\n\ been installed in ROM).\n\n The requirement to provide Installation Information does not include a\n\ requirement to continue to provide support service, warranty, or updates\n\ for a work that has been modified or installed by the recipient, or for\n\ the User Product in which it has been modified or installed. Access to a\n\ network may be denied when the modification itself materially and\n\ adversely affects the operation of the network or violates the rules and\n\ protocols for communication across the network.\n\n Corresponding Source conveyed, and Installation Information provided,\n\ in accord with this section must be in a format that is publicly\n\ documented (and with an implementation available to the public in\n\ source code form), and must require no special password or key for\n\ unpacking, reading or copying.\n\n 7. Additional Terms.\n\n \"Additional permissions\" are terms that supplement the terms of this\n\ License by making exceptions from one or more of its conditions.\n\ Additional permissions that are applicable to the entire Program shall\n\ be treated as though they were included in this License, to the extent\n\ that they are valid under applicable law. If additional permissions\n\ apply only to part of the Program, that part may be used separately\n\ under those permissions, but the entire Program remains governed by\n\ this License without regard to the additional permissions.\n\n When you convey a copy of a covered work, you may at your option\n\ remove any additional permissions from that copy, or from any part of\n\ it. (Additional permissions may be written to require their own\n\ removal in certain cases when you modify the work.) You may place\n\ additional permissions on material, added by you to a covered work,\n\ for which you have or can give appropriate copyright permission.\n\n Notwithstanding any other provision of this License, for material you\n\ add to a covered work, you may (if authorized by the copyright holders of\n\ that material) supplement the terms of this License with terms:\n\n a) Disclaiming warranty or limiting liability differently from the\n terms of sections 15 and 16 of this License; or\n\n b) Requiring preservation of specified reasonable legal notices or\n author attributions in that material or in the Appropriate Legal\n Notices displayed by works containing it; or\n\n c) Prohibiting misrepresentation of the origin of that material, or\n requiring that modified versions of such material be marked in\n reasonable ways as different from the original version; or\n\n d) Limiting the use for publicity purposes of names of licensors or\n authors of the material; or\n\n e) Declining to grant rights under trademark law for use of some\n trade names, trademarks, or service marks; or\n\n f) Requiring indemnification of licensors and authors of that\n material by anyone who conveys the material (or modified versions of\n it) with contractual assumptions of liability to the recipient, for\n any liability that these contractual assumptions directly impose on\n those licensors and authors.\n\n All other non-permissive additional terms are considered \"further\n\ restrictions\" within the meaning of section 10. If the Program as you\n\ received it, or any part of it, contains a notice stating that it is\n\ governed by this License along with a term that is a further\n\ restriction, you may remove that term. If a license document contains\n\ a further restriction but permits relicensing or conveying under this\n\ License, you may add to a covered work material governed by the terms\n\ of that license document, provided that the further restriction does\n\ not survive such relicensing or conveying.\n\n If you add terms to a covered work in accord with this section, you\n\ must place, in the relevant source files, a statement of the\n\ additional terms that apply to those files, or a notice indicating\n\ where to find the applicable terms.\n\n Additional terms, permissive or non-permissive, may be stated in the\n\ form of a separately written license, or stated as exceptions;\n\ the above requirements apply either way.\n\n 8. Termination.\n\n You may not propagate or modify a covered work except as expressly\n\ provided under this License. Any attempt otherwise to propagate or\n\ modify it is void, and will automatically terminate your rights under\n\ this License (including any patent licenses granted under the third\n\ paragraph of section 11).\n\n However, if you cease all violation of this License, then your\n\ license from a particular copyright holder is reinstated (a)\n\ provisionally, unless and until the copyright holder explicitly and\n\ finally terminates your license, and (b) permanently, if the copyright\n\ holder fails to notify you of the violation by some reasonable means\n\ prior to 60 days after the cessation.\n\n Moreover, your license from a particular copyright holder is\n\ reinstated permanently if the copyright holder notifies you of the\n\ violation by some reasonable means, this is the first time you have\n\ received notice of violation of this License (for any work) from that\n\ copyright holder, and you cure the violation prior to 30 days after\n\ your receipt of the notice.\n\n Termination of your rights under this section does not terminate the\n\ licenses of parties who have received copies or rights from you under\n\ this License. If your rights have been terminated and not permanently\n\ reinstated, you do not qualify to receive new licenses for the same\n\ material under section 10.\n\n 9. Acceptance Not Required for Having Copies.\n\n You are not required to accept this License in order to receive or\n\ run a copy of the Program. Ancillary propagation of a covered work\n\ occurring solely as a consequence of using peer-to-peer transmission\n\ to receive a copy likewise does not require acceptance. However,\n\ nothing other than this License grants you permission to propagate or\n\ modify any covered work. These actions infringe copyright if you do\n\ not accept this License. Therefore, by modifying or propagating a\n\ covered work, you indicate your acceptance of this License to do so.\n\n 10. Automatic Licensing of Downstream Recipients.\n\n Each time you convey a covered work, the recipient automatically\n\ receives a license from the original licensors, to run, modify and\n\ propagate that work, subject to this License. You are not responsible\n\ for enforcing compliance by third parties with this License.\n\n An \"entity transaction\" is a transaction transferring control of an\n\ organization, or substantially all assets of one, or subdividing an\n\ organization, or merging organizations. If propagation of a covered\n\ work results from an entity transaction, each party to that\n\ transaction who receives a copy of the work also receives whatever\n\ licenses to the work the party's predecessor in interest had or could\n\ give under the previous paragraph, plus a right to possession of the\n\ Corresponding Source of the work from the predecessor in interest, if\n\ the predecessor has it or can get it with reasonable efforts.\n\n You may not impose any further restrictions on the exercise of the\n\ rights granted or affirmed under this License. For example, you may\n\ not impose a license fee, royalty, or other charge for exercise of\n\ rights granted under this License, and you may not initiate litigation\n\ (including a cross-claim or counterclaim in a lawsuit) alleging that\n\ any patent claim is infringed by making, using, selling, offering for\n\ sale, or importing the Program or any portion of it.\n\n 11. Patents.\n\n A \"contributor\" is a copyright holder who authorizes use under this\n\ License of the Program or a work on which the Program is based. The\n\ work thus licensed is called the contributor's \"contributor version\".\n\n A contributor's \"essential patent claims\" are all patent claims\n\ owned or controlled by the contributor, whether already acquired or\n\ hereafter acquired, that would be infringed by some manner, permitted\n\ by this License, of making, using, or selling its contributor version,\n\ but do not include claims that would be infringed only as a\n\ consequence of further modification of the contributor version. For\n\ purposes of this definition, \"control\" includes the right to grant\n\ patent sublicenses in a manner consistent with the requirements of\n\ this License.\n\n Each contributor grants you a non-exclusive, worldwide, royalty-free\n\ patent license under the contributor's essential patent claims, to\n\ make, use, sell, offer for sale, import and otherwise run, modify and\n\ propagate the contents of its contributor version.\n\n In the following three paragraphs, a \"patent license\" is any express\n\ agreement or commitment, however denominated, not to enforce a patent\n\ (such as an express permission to practice a patent or covenant not to\n\ sue for patent infringement). To \"grant\" such a patent license to a\n\ party means to make such an agreement or commitment not to enforce a\n\ patent against the party.\n\n If you convey a covered work, knowingly relying on a patent license,\n\ and the Corresponding Source of the work is not available for anyone\n\ to copy, free of charge and under the terms of this License, through a\n\ publicly available network server or other readily accessible means,\n\ then you must either (1) cause the Corresponding Source to be so\n\ available, or (2) arrange to deprive yourself of the benefit of the\n\ patent license for this particular work, or (3) arrange, in a manner\n\ consistent with the requirements of this License, to extend the patent\n\ license to downstream recipients. \"Knowingly relying\" means you have\n\ actual knowledge that, but for the patent license, your conveying the\n\ covered work in a country, or your recipient's use of the covered work\n\ in a country, would infringe one or more identifiable patents in that\n\ country that you have reason to believe are valid.\n\n If, pursuant to or in connection with a single transaction or\n\ arrangement, you convey, or propagate by procuring conveyance of, a\n\ covered work, and grant a patent license to some of the parties\n\ receiving the covered work authorizing them to use, propagate, modify\n\ or convey a specific copy of the covered work, then the patent license\n\ you grant is automatically extended to all recipients of the covered\n\ work and works based on it.\n\n A patent license is \"discriminatory\" if it does not include within\n\ the scope of its coverage, prohibits the exercise of, or is\n\ conditioned on the non-exercise of one or more of the rights that are\n\ specifically granted under this License. You may not convey a covered\n\ work if you are a party to an arrangement with a third party that is\n\ in the business of distributing software, under which you make payment\n\ to the third party based on the extent of your activity of conveying\n\ the work, and under which the third party grants, to any of the\n\ parties who would receive the covered work from you, a discriminatory\n\ patent license (a) in connection with copies of the covered work\n\ conveyed by you (or copies made from those copies), or (b) primarily\n\ for and in connection with specific products or compilations that\n\ contain the covered work, unless you entered into that arrangement,\n\ or that patent license was granted, prior to 28 March 2007.\n\n Nothing in this License shall be construed as excluding or limiting\n\ any implied license or other defenses to infringement that may\n\ otherwise be available to you under applicable patent law.\n\n 12. No Surrender of Others' Freedom.\n\n If conditions are imposed on you (whether by court order, agreement or\n\ otherwise) that contradict the conditions of this License, they do not\n\ excuse you from the conditions of this License. If you cannot convey a\n\ covered work so as to satisfy simultaneously your obligations under this\n\ License and any other pertinent obligations, then as a consequence you may\n\ not convey it at all. For example, if you agree to terms that obligate you\n\ to collect a royalty for further conveying from those to whom you convey\n\ the Program, the only way you could satisfy both those terms and this\n\ License would be to refrain entirely from conveying the Program.\n\n 13. Remote Network Interaction; Use with the GNU General Public License.\n\n Notwithstanding any other provision of this License, if you modify the\n\ Program, your modified version must prominently offer all users\n\ interacting with it remotely through a computer network (if your version\n\ supports such interaction) an opportunity to receive the Corresponding\n\ Source of your version by providing access to the Corresponding Source\n\ from a network server at no charge, through some standard or customary\n\ means of facilitating copying of software. This Corresponding Source\n\ shall include the Corresponding Source for any work covered by version 3\n\ of the GNU General Public License that is incorporated pursuant to the\n\ following paragraph.\n\n Notwithstanding any other provision of this License, you have\n\ permission to link or combine any covered work with a work licensed\n\ under version 3 of the GNU General Public License into a single\n\ combined work, and to convey the resulting work. The terms of this\n\ License will continue to apply to the part which is the covered work,\n\ but the work with which it is combined will remain governed by version\n\ 3 of the GNU General Public License.\n\n 14. Revised Versions of this License.\n\n The Free Software Foundation may publish revised and/or new versions of\n\ the GNU Affero General Public License from time to time. Such new versions\n\ will be similar in spirit to the present version, but may differ in detail to\n\ address new problems or concerns.\n\n Each version is given a distinguishing version number. If the\n\ Program specifies that a certain numbered version of the GNU Affero General\n\ Public License \"or any later version\" applies to it, you have the\n\ option of following the terms and conditions either of that numbered\n\ version or of any later version published by the Free Software\n\ Foundation. If the Program does not specify a version number of the\n\ GNU Affero General Public License, you may choose any version ever published\n\ by the Free Software Foundation.\n\n If the Program specifies that a proxy can decide which future\n\ versions of the GNU Affero General Public License can be used, that proxy's\n\ public statement of acceptance of a version permanently authorizes you\n\ to choose that version for the Program.\n\n Later license versions may give you additional or different\n\ permissions. However, no additional obligations are imposed on any\n\ author or copyright holder as a result of your choosing to follow a\n\ later version.\n\n 15. Disclaimer of Warranty.\n\n THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n\ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n\ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n\ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n\ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n\ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n\ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n\ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n 16. Limitation of Liability.\n\n IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n\ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n\ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n\ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n\ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n\ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n\ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n\ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n\ SUCH DAMAGES.\n\n 17. Interpretation of Sections 15 and 16.\n\n If the disclaimer of warranty and limitation of liability provided\n\ above cannot be given local legal effect according to their terms,\n\ reviewing courts shall apply local law that most closely approximates\n\ an absolute waiver of all civil liability in connection with the\n\ Program, unless a warranty or assumption of liability accompanies a\n\ copy of the Program in return for a fee.\n\n END OF TERMS AND CONDITIONS\n\n How to Apply These Terms to Your New Programs\n\n If you develop a new program, and you want it to be of the greatest\n\ possible use to the public, the best way to achieve this is to make it\n\ free software which everyone can redistribute and change under these terms.\n\n To do so, attach the following notices to the program. It is safest\n\ to attach them to the start of each source file to most effectively\n\ state the exclusion of warranty; and each file should have at least\n\ the \"copyright\" line and a pointer to where the full notice is found.\n\n \n Copyright (C) \n\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU Affero General Public License as published by\n the Free Software Foundation, either version 3 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNU Affero General Public License for more details.\n\n You should have received a copy of the GNU Affero General Public License\n along with this program. If not, see .\n\n\ Also add information on how to contact you by electronic and paper mail.\n\n If your software can interact with users remotely through a computer\n\ network, you should also make sure that it provides a way for users to\n\ get its source. For example, if your program is a web application, its\n\ interface could display a \"Source\" link that leads users to an archive\n\ of the code. There are many ways you could offer source, and different\n\ solutions will be better for different programs; see section 13 for the\n\ specific requirements.\n\n You should also get your employer (if you work as a programmer) or school,\n\ if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n\ For more information on this, and how to apply and follow the GNU AGPL, see\n\ .\n" Expire-at: 0 Length: 34520 tmpfhJ2Ca/spec/fixtures/LICENSE-secret-2.0/stored_file0000644000175000017500000000017012574355361021560 0ustar lunarlunar--- Salt: XDhcOFw4XDhcOFw4XDhcOA== Coquelicot: "2.0" Expire-at: 0 --- kĜ_M#\ĩ`lrjvqORIX(pN$Bh&tmpfhJ2Ca/spec/fixtures/LICENSE-secret-2.0/stored_file.content0000644000175000017500000010334012574355361023234 0ustar lunarlunarWޅ(WYr`erd1aHJ@[ C/5z XZz F xJ]нИ B] ݏ9Az,mcR570ֳ*ckOr;u0G($7@/wdM*BQJ`7Cwd ̩KőSA3"FLA&Ǚ㜄M"fvmNӛ,6=BO4Q8$5u~}p*C \2ܸgܜ+ !zǟLEL=h5ŗ/ȫhV_nYv3q1iuĔ[l0W%i3fߜYOUAB7g4Wr<dz;䩦RA#?ӟIK X,ķϗ^"\Ѹ#0q#HwmT8CŴu#&A_? ZU=96z0t~mEUQچ;n9Ϧ* #j@uJ4/Z6A-VU]g[>nvwNIX6 òygx(rh|x{Դq8HSpl PR.S;dAÉJB|tO l]1+҇+$!><zש9_kfYGwpydu˾\0QV~C(˦UL\ha%[KBmKێCF`N.&@>9+J}DD솦rJYzÿ\ܞc ݢOy<PA3(Q޾Mm؜ )8&G;0iwEzF>so%Q=nð•{9ۈ>×h%Ľv&[#vcKͯwoW'. pjp{v:'NLX<JC^@Ѧ3E c\Cbwַ燨YeaRou{.,f \S 1F)o澍?]/[$gAh|g3ΤRl bO ` ע^Ҿ;n( tdb+.Iw!J+\H$<0Yw&8wYdԾ23WZ$ub݌'Ir WŽo:SG׹LY4Bq A\ՖH]f`Db&r+5)8ǁ{Uoj)rhƞ+vh&dCY -GfZ E:^5ȋEqUR%R9CxAa OF *ͧo"n9 i&y#u5V 9 w#١H#NCwo 64ԼAg-:vGuDl2CDE $kQ=)8!&}ZC~r{AoD2'>e8jσGvE'/ ܰqUle i[ƨT}G2TdBDdaI3W`/cy9%O%s*˲ T[}]Ԣ 4c4-0'Sdk+Ւa)I)n{'>#KB)dfeݡ"~{/qCŚD[zB$ =ǏŒ՘<49dZx?L.ZL7y&fCd7sV;9%tg~Ln>W# <ɇ|:up< |M@`Ch:is,S{bSǟl*h7ikw P>83zrt^'o|Ⱥ.BGlX ;Tw' WW jFFh@I ;08J@buvItf3駃Dp5MvdL e:np0|'&\ٰ%rVͲT*zz.z (o5tHm$9Ex_LPiL77Q8o4A$()Dcѐ-CiԺ,f]Se~-2:#yH_q,I!:-k\{||0- "܌쨨'8)1KAlsVng&N{ÃEgxG$03Q3.K[e!r|@sZX!1#ArB"zM7RHBd2Zٵ^dXF ,XOm`5-H+L?T|*/5Zg&v\d^x2~R?R1T,2[ؘz1]Μĺ\W*4eF}]dN&XadS>unf2/kF8--fz.l3 h^zӨ5>% <m`ؙh_/sWĦ nn n"P\8)D>".ET.gՍ:#"!w`XY!Y\}l`O.hyջu=MYꎢʳ# ̈́xW-$I3$\-fBg`< +sZ/5Í3~uĀgC b#v~YL614 q:g~1/gP fR #|mp]:tPdцnA?Qv!W^&'prm(G7?KՏ1؝ik %p:Lt;-B 7=GLUleӣ735e=̨N [*]E]KX'͖*cuCZ鲊PʮIs^:G""̛4YKhӟl#3HMF6gvU^C(XGR8c_6f2't|$HӅ'P7`YzXhHU_yz)(gSnq,u1ֈPbKY|OuD!m[k0'Ae)7IKG Ruň7w :̨0Uؾn ;sj\`2MIV3&(`b L=7#M__!Xh^;?#Vrwٵw?*pqoHz=ohk&7.Qo'3LĄ:lk IzP,q>qOP<@^cF@G u)כBZA~6)Is1 0Y'5^aRwOt Sɟa.YS.)vO@SWH,E2Zj31NU1 򜑝%d1iS6eUB~#*Kv[$2ÛzcUaGlK<̷3d<;wͻ2<i,'+FѹT [2yGQcgoMR0Nncʣl R?$$E3%2={(GȄC5^|5 ԃa.b|sQ8T5EDy^-3ӯvԭhXF[VE_àB¸NM{n v"=,V&SdqVwݏO,@w?o h4BU)5޷e8C4V$^JB4}K-_%&j otLc7 cM\@lXtN]r|d6sxwN %tn+ڼa } {XP g6:+Z|1ٲK-^vdA;$2c5F=`1 L+m~*?׷Á $Ox0.k$ܼpE&@-$9X^ n ui|gxg@t}?xq<@V)<ީ/NθʰGDmZ0C|tʳ-G@MĻLǔ* HqXaR7a,cb>N/`fʎMѪl/{u}-{3 p*aXaq**s 'D}+K˪q?Ɖӟ'HP*-Vn=aj5N_O^i_׫y؊LI>y zh4t_+.A>r-z1a+Bs= \ SfɐwG]1~/;HrګoWI쎔E0 (j+mǫDzdt쥲,#gS5m%(Bpgʤ"<:wPTbMk%qXӭ;ґ\^.ɘvt.Q 㽍ÎoF l ܑJtB!z)(ۇ3KjϝTp\@űvQfzt\&4q͜)KXUd쏔KXk_r[{o;ER |)[ۚUzC^ZJJJ #d?(,^8 2(Vk83coõqWԙr`_? sggSTDyb/<ғ&o߇0]]z| OD6rJ8[$6!4ؐ]l-ōޘl0^`/qꙀUcwNa\=8 ^_vw2>L4ŵr) 55{)2<$׌ HBhɗFrUg[Z^U#GePͭ0 -JZbS<f[)6\~3>}Cܞ/ѹ Pމ"կV9~"r7D!7y?11s[O7Um:oF/-%-2Lha͌sc j>+NbɰwӓwB&({iFzsX\-V@klW^` ӳ5>҆{%vvyHS=t>rNo$Fz1#{Q\??!Mw?.WMᨧ4XRz?4/y ª'|ۅSc,&DyS~)w bBJ,3@'}:ܹ؀ޚM2i9Xm 렓a\[..-]++Xư@͙JlS ׼BcsLYW64[?Z@Zo2 /7SXҗz>R@JjAR\ZDv SbM4G0$%R :Tp]n;n3"RDU/%xR J[A^Sуj8&aJ|BL=">P1' xf_k[ᷪ S 'Z;( KȔOx^:JfDP %0/~y%twY!Yx3!b0K"d"8G)=ͭ.tWԐB^Dts_P2NkΊ)PZ$g<$kt-TD4d;9g%%2CPD@>2J6ЛkIãYR=-L _#ۚ/^p/IaO=&i+fqh( jD4PJk}5*؋hPg!gBW]rhqݽ D6N67ې,_H#NȅV\a Fm4 ,~UlP!5_`{ "HUDѸVh c@i ]; Z`aTʱ_i|=\PѲB@D!CA> J B}D5i{ @-m^KVO>2mz}oR (⮓B#ˁ Ρ1|~rNp2sBJk" /Ǫ&o5qR&PNLYH3Dk e]&{J[nF!)Kzi]D";TYĕ+R\ 6 cm w:+]Ӛx%1G|0?:)o5#b0QGoj@sAڥD8&e` 9a:q.nOpC1`h~CSF'= 뚟M%> girhgW ;[lQ+0C_dT!0[?t.[6ޢ`[{%uG>JF:b7~E~c^N'ݾ֑cB1@t,XIw >b_ b&Ja;ǵ0ݓϦj8~Hjs(2$@ һ0ECGReۆK["t! @·L>~&_ӧ1)3aHjgxraeG' fgoXzHyP5z]iٛC]0s 9py:.J'nLw Go^RyV(pJ#NvhN{y\BRU@kP/^f蜾ōGos[)RWosC;1ѱ:B"N$p` aգ$Vekg!AN.aEo{*\ZafwrVx5&vei&rzP+-;t5ƋLK@1+]voq\eֿ-9YwcyQˀ w" $ QpҦnh?PL9A ЍNP$|dqƮֿ$@*a 皼bXնwqCYLW5gjߊcـQ>^%g/Hoy d RNWu.z%s ??;ĢK_1B^o~ ]CܗM켭 uL.Es.,%7ƽ,G;-C<7-˯æޓ(:&~o߸K񃭳3f ;mZi-3/d?U)W7Hޘ(Λ_(YSװ,FO{0|y/ɱK/ {%Kq<٠ݧ?.Jk~V6%JAf98[)D0]xÁ"ăpmҭ@ABz}+;)DN(gW zak` k"vo5xj"m_%o9'A"g6džT-."sJnkG4IxM%? /}7Kn&V-xsXaT'l`<vO M'k7QvXwiK~IHf.e5fFjW=6"I"/⥄s*B3r.!Dlxl~[/Y*X3v,`9Fi$65ڑz"DndOQaA ( ؍Ӗe^CwHCA--AfA ʉlo )Va5ߩ.3Mf&Le@ mZ܍zB՛d,Q?VAo/eYI?@6~!fo7T(Z`<uOi-Zb:~0IsGu|_b~WIOЁOx6{%( a@K4oyW"3W5BGWhkcgr{G;7CQ{3綍6R<~)3'J,ݒNw[PaddL(O65#Fl5lЁrJ"? -"jRs[ Md)Xd8V zJz}W As_0"L£F'HJ<7D(M˴#u&wWv+7%d|\6̯A=7_r9Ӵ7y>A~!Y=Vy סe 4Q P碗7q>S:y"\[~ /ǻ߽+`n@&vLJ=B-zPu>Ʈ _Lh˖ջwl0 WAZi5`F6jљ0' 9 B'iGCt֝FOY!{G# g0م"G0%1(%LDZUM ⣘EVRwUrS112 \Y?xY95ԯU<11yrRPSr4U%;>Xđ)T{ĺQSh? `N['a8 r@!;oE9M$n@+M4L jN3$ӨBy =x-]~0i@(F"/ƃd;7¬~ nƑDJL<=[DWdmGt7H];>jjFфNȆ Vc[o xd P"HyتkeT;XqiObFgKWH*" :=_WE^aBVE)y"\J"zv>ށCr/sS$&s`cuW&%?6!P&HE`讎:8E] IN]:GQme{=s,`o۟`c7`Su7%;=W M1shN;O(K9s2!C}njbLxohǃ{]g W?J+5+cR58? ďyOAVY׻a7Gsg#IN|>hU܊{oHxQ|^C3ރZ@Cd \}érp6{{J=O&D,CPac#^ڐki#W$Ega +* ^Fq|<k ch\&`;zA/Dw>XTl4q>꯺ƛџIZka.gZ"AB]8W̎ GjW3ӕ:<{3Jg6zgR$qg@݇Sm~:n[^7X C\]\= e /!e%$4)h +qX]тTzvDiaay菭d&lwpis|33СpZuTJ(;yƼc$yyH-Z iIوli \!M.qA62NukUcF\my<c=73UBN-8g&, RL&haeOhXwhOfk,\9&whsN5.ܑ55I@󺇖ğДU{޳LLO^zS!,̪ ߄//S<O8h7A,>54}P s(D ]`fGOO.m#R1˿ 3N8 c";V T\5a_ĢcB;XLxm"&dLt;}X^&ޅT.;EbQ9 8axQ<<">~ {:/_ A뚘+foj<3[$+ǹK ;)h) P9;1n,~;5b8)MO1t;Ր9y)nVkɃLN? nD: YF?di'e`l{d@Ⱥϔ֎3u>Ih~m9i:O,XEK1dzgrnv9/ִOˮvT~X_혊ƆќL u8x_@&yhw%rۍ[n )isCdMugJ4Tx j)(H:L"hck4AA!|9ƪd&\Y atx5uŜ Ǣa?,H*"bk h1mstu_H1ȏPPCQ8d\Ҫ v","-P|"ˠ>h!KIQlk@\at:>}E`k,FMt]:7c* %UHmKCd8X)a؎8_w-f1JOI_iC.B01.Pb2OV ㄊ\d1єjfTM?JX5?0=ym4]#6v$G VP$ T*pm Aatp>;r= >rJ4,U)n7r?TaԀL+Qj~&e5 RMBz4D_g^j*=aHXMѾJa!ysXFID/F^6 =~4xUd3ǧ `yr8l?b)Ħgk.im#bd'= H^-o鋕 "ǻ|NrL$U^ob'f /SUh`6; lFx7`i҉=P'$JGSl;TiC)"_9D'16h ]%n袏2Dt0m0 9 Ii2vbX\:2Ɖ5? |M*Er준qJK_ PyžQ5!Zg?;M6ԪLj 4 ll\ NWr1q1*¯j/GB\\ԏ }e +Ζ1 #h{[#AgU`G䖗h}Ҟv,۬` ֡ن__;+FpUS7;hX&⭭j 4X"jt+F4e)6><zI ݩ&g5IAo 65] aEf/#֭-VĐ|`@sH/ڑaW.NIx'F??X-%J-KS24EbR/]_ *bsH&} V{4M-3V T83o' 0>&eM[!b!t$nKݼ~ 3)?|/HDY<^>{c[7u`@ ݌X`|e!-TTlM7 QNy*Ӝ0H0M+9!90ƍ^<=M$ =2_5D>t~I}fИj=j#v^\ '2ŭ>0lfU nyk%'X-|~[. FAyJn[I>rwwfId+͝&d8jHq ɖ76e'#k 41/V肔dˎl[5^" z|˂G |{yËYHcT4 WG#ѩ˂/_fvuWh-, OP/fH3ňIS~t W/EIG0q8q"f1 ~ .gbY;DQ%6U+|i*/.5[=-S,ߚIBSVCDpH3}z f׾/waeLo[cj[ «Pnp˟֥` e•rS":I%21E.Z KG^5Hg `QCWx45BU 0A!kl#EC]vYl r2q~ ]v]GpPLi$T#LgIUy"lMi.DۛFۦ-%m=`Vu|mc'v3P~{vq(9o(SE{"p?4Q%@;nXzCtbm %_&Μ7 $:C y l%0rC&)2t$+9xx'jĿUҴL{Z-ь}"x\*Q-}U PS$}X*S0Jc#϶ǶXwNcrM9Km HzjJfޘlӉbh SV$<}xmY%WkɱWʃOzD䙍.bZ{B.ÖPxɚ{VcW".z(o[VH$*ەgn_@*C3$X˕^5cʵZ%X;A;hF{ov.i.& 46k]VURo} }h"ޝ&<[!ٿftH_Ax EE'~ꠙ~A|sH k4Ǥw*hB¨B<:OI".m{H\J_Pf,Ѣ !~ҒOZʳ7DNx*=bDs)DLSkh-rjgA)[Wj뉲ny1n#?,O֮ 6T=UNSB(whda ,0 %NUH æQz P%9Z|Ao;Y`oDL!̑]B#WXi0 L=L/Vg$QdcTw0,k Ql>#<%um?[_\?Y~(8s)^3?at\: .3Ժ}<ɔ/vVyk;(\|>VuYDp=v^$ G㔰D"8LAk7x 3:%Ρ }V`2O71{Az g7c4NɃ'/|y[x_ \UI#F=]%DJ>'B=5=Ma 2̛y%ġrr-K"X&~҆7`8 C./ NrzH&V:YBp!I}ӧ'"D|&:0zzuGYXJb;Z˹:#qhk|r(lQs$'y )f>}\~)ow<YK~2nr#x9&^ypkDJ&M(b \m\w Z$ݡJe> {: Y'RE斧8¬@/urt.T=ȬRQ;ʔ:?YCgX4TE9ݙxht鴻 Tps\Uf&5~rg'u$Q#[r2Icd2T IZYc%O!tLRg,ZH+=%m7̇f.np Si+r&x{Cw]9K eT"9hIiN)?Z\5/fdvy۲;;z-6x@=A%m6u@;Mܘim]l4ُݟ|.~,4~qa{ɥҽ߸s<'#C 'Gj6xݧN˰սOL|iצ|zKTۮaձt-ѫZu}Oz]=K#p (Ó-Qf= s(s(Kn[j@Gp"=uje_!שSَnc-gf0CL? mƄw"ʘ대n,zF.ˋd?@sIg?pP) Av7+A>A+9R/S!^ \f9ߔ ).p 劌-"SDCMtA]?Gjヴ^eѻ._N&/Bz 0U[m=dX!ɂ|SL۵ ̓9/𔳭]%Y̧ԂjYKL@#6r?Yo_ѵ#^D4ԇ!Ptʅ̵}s.A~B㧪juEv SH 0t`c) W؝~3kXf;kg(rL+Gilnޜn{ye@OU<18;* ;>~6p:xC8RrJEit[GnˎJ[m¥wDCܯax&f8FêQW9؁b9YjH1u(J%:|}m2l($WWdxm ve SiqY槡Z pL|$^Hz ^ F\Q_{R&og*w&`<g o):TC"mB[iաN ~5U&$DgN/XʚQ%z:5K4dԢ˟wCr'U+-vXآe+ohQX8qT2_ U)ښ}9ꛧ/r4 5+ We:.*̷rN\?%V]~|x-KrB+*uS 7+@ܼ>g)y2!ec}?W38[@w=k T6$9^^B{zM>L-cY/ H˜"]D#6Tv 銟l߶(]oTrD A^3+9[oڄZ0%Sթ2z)'BofBSn-9Z'g pRi,{~v$`ϵ LpOk j@ݴ;)uA&>K]@~ YЋn{'7ZzzoH9S|ՃZ\qpKaOR.诉 )gx Q o!fAZXRDI[oiŰfSh?_u7z%3RB꺁Kڏ4xF\ IɱtW>֔ra?KA]RY=``mO>2=͠Nsi $Jb#cLrolqSuD SU $yáLg*nGpizTP]}sO)z NPMt -ma]Ql2G2mYP w#?ƾ 6v.j5풅l07"d@J`~3FycD1jc=Ĩfx@M{43Vng@'Krnc|v-Q2PW!;lO&H-Y;X4)=qЧ_2(k'g0=3!g^1lW:4<{勀 9?㶮2}`|> :ó>.]93|YEZvqөQ YteNc;m.QPfj6Ry_0nQټkɲf.XY]Od\FziUЩxσPa\>˅%g\ k\^+]^ls@SMxK)T$Liߪ$,^|z>ۅBe|RK!Rݱ}`z'!dkU[cЀ:EzPPQyoƻ[Af 5zy7(VMA{;}u=qW*qU-\NDk"o=$zsΣuƠ uD7-x͊ak6fxCvfCl9W'O% 5CĢ`t]o+ 50}b k]kpgiJoyb*>%Hs}828 _]SVʱP11MQiy%#? GsS93_>@XW@|Ւ)k } ;vBe+StR8WZ5z^3oK!ш@MZNf݃*9I~Z]vd<^euYPg_:g*.؛Z f7T! h׆>)ʺ W ӛᦸp8AX ]@ /eq5|,!djJ~rYWñ|ARO c \;h3[|D;BUn|t#J*J77,]H جChWɼR$hJ6@ABZҶ jqcu@Fv|Hiu# 1Fcv> o%x1Mh+BsHAc^ vqIes8[Cu(=q#Rk@y50]lKVz#Z:G[ P&ccwO-B['415𾁌{< /קM_m{CB0MSJ-BR[m$(<'9i"7>ol׷nt ]d{byH޲B\fz>G9ךsYVY"! @d{ẂyOXy }LɓxrO'%lU 3 >M8 nV2n>LO\ޖ[(Fc$QO}MTr'zn-QZ "yqsȭJԣr&&P-~3fO4>NA\QϕUn̎D Ow K%ۚlxk=db:QwƱg׎qէ 3Ѱ:}|f|VV 㮚?:Ȼ3Δ>!,9gkoa -P.MxK" Bء']M;6bMʣ_sLW; 9IsEg*[b<߰Z`8 M_ᣰ+Rlp^ld6 t2tnipc*Nc/sN`g٧ߠH| |pk"X0[f YZXRsVՃ22W;xpDFQP_^:koݺ͓P̾{6q5wSpt3&>2;2]J2S qiדB;Ρ{I˃llZcƩÕeHB\/c9\<75$Mw 2‘aEʧOBk q_G_R`.^rA[/ݗ2 daL~ ̬#K]zXF)[MC}ūdƍLVxv:uT+őǣaa]G_3uUh搧+is!nqGA6cӳFՓ*Wir"c{<Dl}G=c)_#>h"Ҟk]4DHœ$8QYo_A[cK% t.Y߂A:ϢS8,+UVwPϣ)X0z~'toDYce],Yu_mw:hyAkp9P#^AAz3J2 "{|pUL,HeDd.$‹kdm2gWM; xl͵,1IM/5虤^B$x CUfsM_ 8`}+m8"$pN1VUB/ojQ8#q=h r ɲ,f3X?ha'+~=b $ 5G*&un}`y-%Zh'q`zz{_s+IU6 D6?QOMFA7XJ([;EaJ%$ mҥ fj3YR(B)iْM,;lBw}U968G "еa7HA@\ ,G{\K4\kvZlQSיah2}(MAJ`<1rѥr_~| ?^ymf}ΑK!Ƒ ;)52T!լmI)=r¿@fd=?j<]XRrZſ] PK QwL.v*䌕 ͤű++0 <k|FHtrP&AZtj6Dl䚸ʑX?K ,HBf@c}N&}s^D=Tʤ}q,-P㥿ᔣX3a1Qd;-+K0 L\ k:\hW&|wR[6_K@_bʌF0-'o7)Í$3ϫ_a$7]??ehIُ2&CQ."XIEf\x+ū,Oi/%e-۩|1gSM5;[ݸ*3i2GN ])J(pȮP6cR]lA9ḏLXF1issp>)J)z˾#WFŷ3ݎ%k=I xGR_7{kzې~ac]J 78+m܃ʝi0.헣kwKhv՚"NAFsM>(#S?$,u mPrCyHdd%kzJ KcSU[ "뎄W]?˵ɅвL0\ۏ|V <5[gՎdHx{[|͵<~w˵텩e4Y[8lROWNanrUܠɬTp+>-4ʾ0Vɠh:5@5.%:HCĊ8pQ,e#xxN0G,CU@cg{ʻE }NqbU]cCh!"ག$N %θs|G"6U`k84Gs^gn; Fo >p ,5I62ge7CPIwOu .ҲLꇯګcFO*6L:g s'h |T(SsǘZ/D0SyBVug`1C,Lk/~i`HԲߊ VTbB}!o4y/UǞBGV#G+cB-vM0t?ZJզpk;ZUيJE6mb:.NdUj]+Z)<+3k¸qo]d%U$TuVT$TЖ ||9Ehښ(I[MD-A^E\9upߩd,tM#013`u3 }a1^r2Ww)þ~ -Oۃ/'؁h~ hŚ_Ӓ9 ibۡuۇP|E`8K ] 94= 3 nDCV%˷$~rGJ(uqmGF %&V)]mXIa}?? =uM1H`%A%Ԁ~SZ A.lYCz 5MW8%8KhŁg~{8,X:6%RG5',W-0qe6tc SA g2ӶYxKAp: 턎@_Z<]y6Uπ(؊v ksWf?3vJF;^c>Uu+ryf@s"r6 f^<4}'3[PkVdgBLo6[,R)_{fZ1CDxv 2W1-qŦ|*Gd5nuf`dd8 A8fmC1$#0"8loRVf˜V,|.g>?{hVEMOK8B 6P /%73caw}紱tk1S#6EaQ$f7fD:ߥ_UF=B2ђ),mL qgPp۞P>;/_kõ?C \_;M [soSB{,=~8$-P!_)EEQY,I٥JA2%/G@3(MH]ZU&uNG ccO#dB~$]G}H l{%zc}ȚNz>%St\٨;O#^Anj KɋL %2[˔ :lb#My$ K\m)@>TUeR ;K}E t\nR>a<5)E:`DJzOu;pV,Bd)O'Ѹ ؾRxޢE`@V_byT8S7{Oh&#ǘ.RWoyyި)U^]@@K;بr\Nஓ? "lX_Xxf"M110g3 NR 2NFvEP6 supy@c活և <Hs3dwxPAZɲyZm>Ũ< 26yĶ<q.G޷!T$Fuv_RӫJŚ`hqA>介BQL;4SfP3 nI( 1E0]# p`@5u ]O_R͒4 y$‹iu+QX[ h;zx:IK5UeDapȮ{ X~v|3`f\g(\ J ,Q=u/ zNO ~LńO3T4wNf-h@aXle^)'~-F2}n!LOw@ULnڻ֫ABw{ poOv_^sdo^՝ij\"hq!T18T33T'S J\ Cq=1F+@pzQ%9I췞k7 ̓WƾX?OAkKa SQ!|)das滘?$#5Ft/5%˖S+μll,a~5 J `NY?r<G=}ŝD]-S HQ L&7ZXQ}-Oeֺh p'K^5B[oR_VcwSi"hN&mpSUW& kރi*o*?JT$5Y(#9}"QϽ0=nbS~eW}ژ]Sg8\?/ Ol&& uOVhq>bfy$j3h.Ms 3v7f\tZI!R.nJhlUVuyXӞ0Ẏkkbofŷ]td>puF 308 8\f*C 1⏙4g "0nل_%&y{~Q1Tp\1ATg, oFh\T']Vybsxc{A _F> PdGEWЩ$x*R/w^K3A*~Q Z츥GB(H,(}42d|4B9B1f{@&Ԋ=`^U> $LyMOn;nl?A`I5V̏aΚT?*Ud G/;럘fSa}İtMh;D"ywd1'-NpQp[_-?:h )*v̞d 4>*f!@km=MCO]}]C!}u!<%C9ՔeZ!4+|sZ90,CbjWs}1 9*#Iz_*H'PGiPSwdsGkR?7&烑Nw Y@ؿtW$C/ 0gP!/@JxeSxp{Y%*ˢ+IJj`դ:x5A, Q,!~J5qG{;?_K2dϵK8yw'?.{5qVb3$`>ąؑרa9`ɁIީ7NǧL7^ԣVc6dol#;#وP@~a,2%3TNc^mE9K'Hk͉d.b-'de"Һ9AWqchqS3zgߩ1f'73^q<(I U޲ccv3Hվ?˰Nѐ8GErQB,C{^{ BYYe=Ŷ0iىyJ#V>.ܜcoCeؗ'T݆D4s ց +;I,[1ycfTjcT4R:\裨R1֥( (l]δr;k7*R4ѰR&XmG,YI7h<׬~Ֆe^E_u4/S)-1砱(⺟Ê?"yc D߆}^$*=Fޱ;0VgZsŻzZ>#pnXmwCQ~<,QbW0ҹF̖TELgO`*_m^Qo_g̚] 6(m*] ]Y>%n$UxקȽsm˭'¿A*sxSaNT`a>w|Z* X]s4XI?wՍd?Rl̻|XXh"|>[4[u./&+&7;[Y׽L8F({ZN*j'8rن(>:tp/*؛V 2d,E{Lҟn}L.S`lajͧ4ޛAJ[!V(~$plWrhI ,֖mFBIvN|ߌP-lEZepӔHP+?Dj[=!t HY!-o}͈rk(GdrTÛxy֕ٙڦN5Oz`~*q1r6BQ$2?{_P8, #qfT, ҖU;AIK&a L~s~ld5?!GFԒ&Ce.JjlyӖR]􄐟kP-q!MUHD&X{8tDǩGH! :iF \#F27jB` h5m9,ī%O_:Q pH@M^O7㒦%z-_O22]$lXfvZBy5Ҕ~-QM+ +SvWx(QUày8\y$D@ 8dx28A3*҄4ZWV7Kkc4lZr*<,K-ޗz]ykn:D+E4ːUI(>zh(f1JXd#IJ>4|:31%6{Kzh6^dm~e} ɲ %eLI pOͫũAjb821Y60 EdEV#9v* Ϊ/@G9lfwI 'YώՉhAkN*}L ]%̊u|}6&m\QvZ^LE U.ym`̶U 2.0fNN (I$,CN"/q*3ĆLm0nHfDžΐ(+G1}gpu!ܞswQK!b Ǖeڏ+(`re\q*1?=YХ"A*l&)FS&֌1x{rzˆέơ֠28ԏGU1X!ٞNq7WڇTൃ'>LJg1\6JwK2eKf4q=hQ$@묠Zs]qjҘ<5w2퉛슓}EQtmpfhJ2Ca/spec/fixtures/small-secret-1.0/0000755000175000017500000000000013026234541017352 5ustar lunarlunartmpfhJ2Ca/spec/fixtures/small-secret-1.0/reference0000644000175000017500000000010412574355361021241 0ustar lunarlunar--- Created-at: 1331028513 Content: Content Expire-at: 0 Length: 7 tmpfhJ2Ca/spec/fixtures/small-secret-1.0/stored_file0000644000175000017500000000017412574355361021611 0ustar lunarlunar--- Salt: 1vpMmpYVKPA= Coquelicot: "1.0" Expire-at: 0 --- ʊ D,EG? %:イ<"U 7hp*83'5>.tmpfhJ2Ca/spec/fixtures/LICENSE-secret-1.0/0000755000175000017500000000000013026234541017324 5ustar lunarlunartmpfhJ2Ca/spec/fixtures/LICENSE-secret-1.0/reference0000644000175000017500000010764612574355361021236 0ustar lunarlunar--- Created-at: 1330939898 Content: " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. \n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n Preamble\n\n The GNU Affero General Public License is a free, copyleft license for\n\ software and other kinds of works, specifically designed to ensure\n\ cooperation with the community in the case of network server software.\n\n The licenses for most software and other practical works are designed\n\ to take away your freedom to share and change the works. By contrast,\n\ our General Public Licenses are intended to guarantee your freedom to\n\ share and change all versions of a program--to make sure it remains free\n\ software for all its users.\n\n When we speak of free software, we are referring to freedom, not\n\ price. Our General Public Licenses are designed to make sure that you\n\ have the freedom to distribute copies of free software (and charge for\n\ them if you wish), that you receive source code or can get it if you\n\ want it, that you can change the software or use pieces of it in new\n\ free programs, and that you know you can do these things.\n\n Developers that use our General Public Licenses protect your rights\n\ with two steps: (1) assert copyright on the software, and (2) offer\n\ you this License which gives you legal permission to copy, distribute\n\ and/or modify the software.\n\n A secondary benefit of defending all users' freedom is that\n\ improvements made in alternate versions of the program, if they\n\ receive widespread use, become available for other developers to\n\ incorporate. Many developers of free software are heartened and\n\ encouraged by the resulting cooperation. However, in the case of\n\ software used on network servers, this result may fail to come about.\n\ The GNU General Public License permits making a modified version and\n\ letting the public access it on a server without ever releasing its\n\ source code to the public.\n\n The GNU Affero General Public License is designed specifically to\n\ ensure that, in such cases, the modified source code becomes available\n\ to the community. It requires the operator of a network server to\n\ provide the source code of the modified version running there to the\n\ users of that server. Therefore, public use of a modified version, on\n\ a publicly accessible server, gives the public access to the source\n\ code of the modified version.\n\n An older license, called the Affero General Public License and\n\ published by Affero, was designed to accomplish similar goals. This is\n\ a different license, not a version of the Affero GPL, but Affero has\n\ released a new version of the Affero GPL which permits relicensing under\n\ this license.\n\n The precise terms and conditions for copying, distribution and\n\ modification follow.\n\n TERMS AND CONDITIONS\n\n 0. Definitions.\n\n \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n \"Copyright\" also means copyright-like laws that apply to other kinds of\n\ works, such as semiconductor masks.\n\n \"The Program\" refers to any copyrightable work licensed under this\n\ License. Each licensee is addressed as \"you\". \"Licensees\" and\n\ \"recipients\" may be individuals or organizations.\n\n To \"modify\" a work means to copy from or adapt all or part of the work\n\ in a fashion requiring copyright permission, other than the making of an\n\ exact copy. The resulting work is called a \"modified version\" of the\n\ earlier work or a work \"based on\" the earlier work.\n\n A \"covered work\" means either the unmodified Program or a work based\n\ on the Program.\n\n To \"propagate\" a work means to do anything with it that, without\n\ permission, would make you directly or secondarily liable for\n\ infringement under applicable copyright law, except executing it on a\n\ computer or modifying a private copy. Propagation includes copying,\n\ distribution (with or without modification), making available to the\n\ public, and in some countries other activities as well.\n\n To \"convey\" a work means any kind of propagation that enables other\n\ parties to make or receive copies. Mere interaction with a user through\n\ a computer network, with no transfer of a copy, is not conveying.\n\n An interactive user interface displays \"Appropriate Legal Notices\"\n\ to the extent that it includes a convenient and prominently visible\n\ feature that (1) displays an appropriate copyright notice, and (2)\n\ tells the user that there is no warranty for the work (except to the\n\ extent that warranties are provided), that licensees may convey the\n\ work under this License, and how to view a copy of this License. If\n\ the interface presents a list of user commands or options, such as a\n\ menu, a prominent item in the list meets this criterion.\n\n 1. Source Code.\n\n The \"source code\" for a work means the preferred form of the work\n\ for making modifications to it. \"Object code\" means any non-source\n\ form of a work.\n\n A \"Standard Interface\" means an interface that either is an official\n\ standard defined by a recognized standards body, or, in the case of\n\ interfaces specified for a particular programming language, one that\n\ is widely used among developers working in that language.\n\n The \"System Libraries\" of an executable work include anything, other\n\ than the work as a whole, that (a) is included in the normal form of\n\ packaging a Major Component, but which is not part of that Major\n\ Component, and (b) serves only to enable use of the work with that\n\ Major Component, or to implement a Standard Interface for which an\n\ implementation is available to the public in source code form. A\n\ \"Major Component\", in this context, means a major essential component\n\ (kernel, window system, and so on) of the specific operating system\n\ (if any) on which the executable work runs, or a compiler used to\n\ produce the work, or an object code interpreter used to run it.\n\n The \"Corresponding Source\" for a work in object code form means all\n\ the source code needed to generate, install, and (for an executable\n\ work) run the object code and to modify the work, including scripts to\n\ control those activities. However, it does not include the work's\n\ System Libraries, or general-purpose tools or generally available free\n\ programs which are used unmodified in performing those activities but\n\ which are not part of the work. For example, Corresponding Source\n\ includes interface definition files associated with source files for\n\ the work, and the source code for shared libraries and dynamically\n\ linked subprograms that the work is specifically designed to require,\n\ such as by intimate data communication or control flow between those\n\ subprograms and other parts of the work.\n\n The Corresponding Source need not include anything that users\n\ can regenerate automatically from other parts of the Corresponding\n\ Source.\n\n The Corresponding Source for a work in source code form is that\n\ same work.\n\n 2. Basic Permissions.\n\n All rights granted under this License are granted for the term of\n\ copyright on the Program, and are irrevocable provided the stated\n\ conditions are met. This License explicitly affirms your unlimited\n\ permission to run the unmodified Program. The output from running a\n\ covered work is covered by this License only if the output, given its\n\ content, constitutes a covered work. This License acknowledges your\n\ rights of fair use or other equivalent, as provided by copyright law.\n\n You may make, run and propagate covered works that you do not\n\ convey, without conditions so long as your license otherwise remains\n\ in force. You may convey covered works to others for the sole purpose\n\ of having them make modifications exclusively for you, or provide you\n\ with facilities for running those works, provided that you comply with\n\ the terms of this License in conveying all material for which you do\n\ not control copyright. Those thus making or running the covered works\n\ for you must do so exclusively on your behalf, under your direction\n\ and control, on terms that prohibit them from making any copies of\n\ your copyrighted material outside their relationship with you.\n\n Conveying under any other circumstances is permitted solely under\n\ the conditions stated below. Sublicensing is not allowed; section 10\n\ makes it unnecessary.\n\n 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n No covered work shall be deemed part of an effective technological\n\ measure under any applicable law fulfilling obligations under article\n\ 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n\ similar laws prohibiting or restricting circumvention of such\n\ measures.\n\n When you convey a covered work, you waive any legal power to forbid\n\ circumvention of technological measures to the extent such circumvention\n\ is effected by exercising rights under this License with respect to\n\ the covered work, and you disclaim any intention to limit operation or\n\ modification of the work as a means of enforcing, against the work's\n\ users, your or third parties' legal rights to forbid circumvention of\n\ technological measures.\n\n 4. Conveying Verbatim Copies.\n\n You may convey verbatim copies of the Program's source code as you\n\ receive it, in any medium, provided that you conspicuously and\n\ appropriately publish on each copy an appropriate copyright notice;\n\ keep intact all notices stating that this License and any\n\ non-permissive terms added in accord with section 7 apply to the code;\n\ keep intact all notices of the absence of any warranty; and give all\n\ recipients a copy of this License along with the Program.\n\n You may charge any price or no price for each copy that you convey,\n\ and you may offer support or warranty protection for a fee.\n\n 5. Conveying Modified Source Versions.\n\n You may convey a work based on the Program, or the modifications to\n\ produce it from the Program, in the form of source code under the\n\ terms of section 4, provided that you also meet all of these conditions:\n\n a) The work must carry prominent notices stating that you modified\n it, and giving a relevant date.\n\n b) The work must carry prominent notices stating that it is\n released under this License and any conditions added under section\n 7. This requirement modifies the requirement in section 4 to\n \"keep intact all notices\".\n\n c) You must license the entire work, as a whole, under this\n License to anyone who comes into possession of a copy. This\n License will therefore apply, along with any applicable section 7\n additional terms, to the whole of the work, and all its parts,\n regardless of how they are packaged. This License gives no\n permission to license the work in any other way, but it does not\n invalidate such permission if you have separately received it.\n\n d) If the work has interactive user interfaces, each must display\n Appropriate Legal Notices; however, if the Program has interactive\n interfaces that do not display Appropriate Legal Notices, your\n work need not make them do so.\n\n A compilation of a covered work with other separate and independent\n\ works, which are not by their nature extensions of the covered work,\n\ and which are not combined with it such as to form a larger program,\n\ in or on a volume of a storage or distribution medium, is called an\n\ \"aggregate\" if the compilation and its resulting copyright are not\n\ used to limit the access or legal rights of the compilation's users\n\ beyond what the individual works permit. Inclusion of a covered work\n\ in an aggregate does not cause this License to apply to the other\n\ parts of the aggregate.\n\n 6. Conveying Non-Source Forms.\n\n You may convey a covered work in object code form under the terms\n\ of sections 4 and 5, provided that you also convey the\n\ machine-readable Corresponding Source under the terms of this License,\n\ in one of these ways:\n\n a) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by the\n Corresponding Source fixed on a durable physical medium\n customarily used for software interchange.\n\n b) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by a\n written offer, valid for at least three years and valid for as\n long as you offer spare parts or customer support for that product\n model, to give anyone who possesses the object code either (1) a\n copy of the Corresponding Source for all the software in the\n product that is covered by this License, on a durable physical\n medium customarily used for software interchange, for a price no\n more than your reasonable cost of physically performing this\n conveying of source, or (2) access to copy the\n Corresponding Source from a network server at no charge.\n\n c) Convey individual copies of the object code with a copy of the\n written offer to provide the Corresponding Source. This\n alternative is allowed only occasionally and noncommercially, and\n only if you received the object code with such an offer, in accord\n with subsection 6b.\n\n d) Convey the object code by offering access from a designated\n place (gratis or for a charge), and offer equivalent access to the\n Corresponding Source in the same way through the same place at no\n further charge. You need not require recipients to copy the\n Corresponding Source along with the object code. If the place to\n copy the object code is a network server, the Corresponding Source\n may be on a different server (operated by you or a third party)\n that supports equivalent copying facilities, provided you maintain\n clear directions next to the object code saying where to find the\n Corresponding Source. Regardless of what server hosts the\n Corresponding Source, you remain obligated to ensure that it is\n available for as long as needed to satisfy these requirements.\n\n e) Convey the object code using peer-to-peer transmission, provided\n you inform other peers where the object code and Corresponding\n Source of the work are being offered to the general public at no\n charge under subsection 6d.\n\n A separable portion of the object code, whose source code is excluded\n\ from the Corresponding Source as a System Library, need not be\n\ included in conveying the object code work.\n\n A \"User Product\" is either (1) a \"consumer product\", which means any\n\ tangible personal property which is normally used for personal, family,\n\ or household purposes, or (2) anything designed or sold for incorporation\n\ into a dwelling. In determining whether a product is a consumer product,\n\ doubtful cases shall be resolved in favor of coverage. For a particular\n\ product received by a particular user, \"normally used\" refers to a\n\ typical or common use of that class of product, regardless of the status\n\ of the particular user or of the way in which the particular user\n\ actually uses, or expects or is expected to use, the product. A product\n\ is a consumer product regardless of whether the product has substantial\n\ commercial, industrial or non-consumer uses, unless such uses represent\n\ the only significant mode of use of the product.\n\n \"Installation Information\" for a User Product means any methods,\n\ procedures, authorization keys, or other information required to install\n\ and execute modified versions of a covered work in that User Product from\n\ a modified version of its Corresponding Source. The information must\n\ suffice to ensure that the continued functioning of the modified object\n\ code is in no case prevented or interfered with solely because\n\ modification has been made.\n\n If you convey an object code work under this section in, or with, or\n\ specifically for use in, a User Product, and the conveying occurs as\n\ part of a transaction in which the right of possession and use of the\n\ User Product is transferred to the recipient in perpetuity or for a\n\ fixed term (regardless of how the transaction is characterized), the\n\ Corresponding Source conveyed under this section must be accompanied\n\ by the Installation Information. But this requirement does not apply\n\ if neither you nor any third party retains the ability to install\n\ modified object code on the User Product (for example, the work has\n\ been installed in ROM).\n\n The requirement to provide Installation Information does not include a\n\ requirement to continue to provide support service, warranty, or updates\n\ for a work that has been modified or installed by the recipient, or for\n\ the User Product in which it has been modified or installed. Access to a\n\ network may be denied when the modification itself materially and\n\ adversely affects the operation of the network or violates the rules and\n\ protocols for communication across the network.\n\n Corresponding Source conveyed, and Installation Information provided,\n\ in accord with this section must be in a format that is publicly\n\ documented (and with an implementation available to the public in\n\ source code form), and must require no special password or key for\n\ unpacking, reading or copying.\n\n 7. Additional Terms.\n\n \"Additional permissions\" are terms that supplement the terms of this\n\ License by making exceptions from one or more of its conditions.\n\ Additional permissions that are applicable to the entire Program shall\n\ be treated as though they were included in this License, to the extent\n\ that they are valid under applicable law. If additional permissions\n\ apply only to part of the Program, that part may be used separately\n\ under those permissions, but the entire Program remains governed by\n\ this License without regard to the additional permissions.\n\n When you convey a copy of a covered work, you may at your option\n\ remove any additional permissions from that copy, or from any part of\n\ it. (Additional permissions may be written to require their own\n\ removal in certain cases when you modify the work.) You may place\n\ additional permissions on material, added by you to a covered work,\n\ for which you have or can give appropriate copyright permission.\n\n Notwithstanding any other provision of this License, for material you\n\ add to a covered work, you may (if authorized by the copyright holders of\n\ that material) supplement the terms of this License with terms:\n\n a) Disclaiming warranty or limiting liability differently from the\n terms of sections 15 and 16 of this License; or\n\n b) Requiring preservation of specified reasonable legal notices or\n author attributions in that material or in the Appropriate Legal\n Notices displayed by works containing it; or\n\n c) Prohibiting misrepresentation of the origin of that material, or\n requiring that modified versions of such material be marked in\n reasonable ways as different from the original version; or\n\n d) Limiting the use for publicity purposes of names of licensors or\n authors of the material; or\n\n e) Declining to grant rights under trademark law for use of some\n trade names, trademarks, or service marks; or\n\n f) Requiring indemnification of licensors and authors of that\n material by anyone who conveys the material (or modified versions of\n it) with contractual assumptions of liability to the recipient, for\n any liability that these contractual assumptions directly impose on\n those licensors and authors.\n\n All other non-permissive additional terms are considered \"further\n\ restrictions\" within the meaning of section 10. If the Program as you\n\ received it, or any part of it, contains a notice stating that it is\n\ governed by this License along with a term that is a further\n\ restriction, you may remove that term. If a license document contains\n\ a further restriction but permits relicensing or conveying under this\n\ License, you may add to a covered work material governed by the terms\n\ of that license document, provided that the further restriction does\n\ not survive such relicensing or conveying.\n\n If you add terms to a covered work in accord with this section, you\n\ must place, in the relevant source files, a statement of the\n\ additional terms that apply to those files, or a notice indicating\n\ where to find the applicable terms.\n\n Additional terms, permissive or non-permissive, may be stated in the\n\ form of a separately written license, or stated as exceptions;\n\ the above requirements apply either way.\n\n 8. Termination.\n\n You may not propagate or modify a covered work except as expressly\n\ provided under this License. Any attempt otherwise to propagate or\n\ modify it is void, and will automatically terminate your rights under\n\ this License (including any patent licenses granted under the third\n\ paragraph of section 11).\n\n However, if you cease all violation of this License, then your\n\ license from a particular copyright holder is reinstated (a)\n\ provisionally, unless and until the copyright holder explicitly and\n\ finally terminates your license, and (b) permanently, if the copyright\n\ holder fails to notify you of the violation by some reasonable means\n\ prior to 60 days after the cessation.\n\n Moreover, your license from a particular copyright holder is\n\ reinstated permanently if the copyright holder notifies you of the\n\ violation by some reasonable means, this is the first time you have\n\ received notice of violation of this License (for any work) from that\n\ copyright holder, and you cure the violation prior to 30 days after\n\ your receipt of the notice.\n\n Termination of your rights under this section does not terminate the\n\ licenses of parties who have received copies or rights from you under\n\ this License. If your rights have been terminated and not permanently\n\ reinstated, you do not qualify to receive new licenses for the same\n\ material under section 10.\n\n 9. Acceptance Not Required for Having Copies.\n\n You are not required to accept this License in order to receive or\n\ run a copy of the Program. Ancillary propagation of a covered work\n\ occurring solely as a consequence of using peer-to-peer transmission\n\ to receive a copy likewise does not require acceptance. However,\n\ nothing other than this License grants you permission to propagate or\n\ modify any covered work. These actions infringe copyright if you do\n\ not accept this License. Therefore, by modifying or propagating a\n\ covered work, you indicate your acceptance of this License to do so.\n\n 10. Automatic Licensing of Downstream Recipients.\n\n Each time you convey a covered work, the recipient automatically\n\ receives a license from the original licensors, to run, modify and\n\ propagate that work, subject to this License. You are not responsible\n\ for enforcing compliance by third parties with this License.\n\n An \"entity transaction\" is a transaction transferring control of an\n\ organization, or substantially all assets of one, or subdividing an\n\ organization, or merging organizations. If propagation of a covered\n\ work results from an entity transaction, each party to that\n\ transaction who receives a copy of the work also receives whatever\n\ licenses to the work the party's predecessor in interest had or could\n\ give under the previous paragraph, plus a right to possession of the\n\ Corresponding Source of the work from the predecessor in interest, if\n\ the predecessor has it or can get it with reasonable efforts.\n\n You may not impose any further restrictions on the exercise of the\n\ rights granted or affirmed under this License. For example, you may\n\ not impose a license fee, royalty, or other charge for exercise of\n\ rights granted under this License, and you may not initiate litigation\n\ (including a cross-claim or counterclaim in a lawsuit) alleging that\n\ any patent claim is infringed by making, using, selling, offering for\n\ sale, or importing the Program or any portion of it.\n\n 11. Patents.\n\n A \"contributor\" is a copyright holder who authorizes use under this\n\ License of the Program or a work on which the Program is based. The\n\ work thus licensed is called the contributor's \"contributor version\".\n\n A contributor's \"essential patent claims\" are all patent claims\n\ owned or controlled by the contributor, whether already acquired or\n\ hereafter acquired, that would be infringed by some manner, permitted\n\ by this License, of making, using, or selling its contributor version,\n\ but do not include claims that would be infringed only as a\n\ consequence of further modification of the contributor version. For\n\ purposes of this definition, \"control\" includes the right to grant\n\ patent sublicenses in a manner consistent with the requirements of\n\ this License.\n\n Each contributor grants you a non-exclusive, worldwide, royalty-free\n\ patent license under the contributor's essential patent claims, to\n\ make, use, sell, offer for sale, import and otherwise run, modify and\n\ propagate the contents of its contributor version.\n\n In the following three paragraphs, a \"patent license\" is any express\n\ agreement or commitment, however denominated, not to enforce a patent\n\ (such as an express permission to practice a patent or covenant not to\n\ sue for patent infringement). To \"grant\" such a patent license to a\n\ party means to make such an agreement or commitment not to enforce a\n\ patent against the party.\n\n If you convey a covered work, knowingly relying on a patent license,\n\ and the Corresponding Source of the work is not available for anyone\n\ to copy, free of charge and under the terms of this License, through a\n\ publicly available network server or other readily accessible means,\n\ then you must either (1) cause the Corresponding Source to be so\n\ available, or (2) arrange to deprive yourself of the benefit of the\n\ patent license for this particular work, or (3) arrange, in a manner\n\ consistent with the requirements of this License, to extend the patent\n\ license to downstream recipients. \"Knowingly relying\" means you have\n\ actual knowledge that, but for the patent license, your conveying the\n\ covered work in a country, or your recipient's use of the covered work\n\ in a country, would infringe one or more identifiable patents in that\n\ country that you have reason to believe are valid.\n\n If, pursuant to or in connection with a single transaction or\n\ arrangement, you convey, or propagate by procuring conveyance of, a\n\ covered work, and grant a patent license to some of the parties\n\ receiving the covered work authorizing them to use, propagate, modify\n\ or convey a specific copy of the covered work, then the patent license\n\ you grant is automatically extended to all recipients of the covered\n\ work and works based on it.\n\n A patent license is \"discriminatory\" if it does not include within\n\ the scope of its coverage, prohibits the exercise of, or is\n\ conditioned on the non-exercise of one or more of the rights that are\n\ specifically granted under this License. You may not convey a covered\n\ work if you are a party to an arrangement with a third party that is\n\ in the business of distributing software, under which you make payment\n\ to the third party based on the extent of your activity of conveying\n\ the work, and under which the third party grants, to any of the\n\ parties who would receive the covered work from you, a discriminatory\n\ patent license (a) in connection with copies of the covered work\n\ conveyed by you (or copies made from those copies), or (b) primarily\n\ for and in connection with specific products or compilations that\n\ contain the covered work, unless you entered into that arrangement,\n\ or that patent license was granted, prior to 28 March 2007.\n\n Nothing in this License shall be construed as excluding or limiting\n\ any implied license or other defenses to infringement that may\n\ otherwise be available to you under applicable patent law.\n\n 12. No Surrender of Others' Freedom.\n\n If conditions are imposed on you (whether by court order, agreement or\n\ otherwise) that contradict the conditions of this License, they do not\n\ excuse you from the conditions of this License. If you cannot convey a\n\ covered work so as to satisfy simultaneously your obligations under this\n\ License and any other pertinent obligations, then as a consequence you may\n\ not convey it at all. For example, if you agree to terms that obligate you\n\ to collect a royalty for further conveying from those to whom you convey\n\ the Program, the only way you could satisfy both those terms and this\n\ License would be to refrain entirely from conveying the Program.\n\n 13. Remote Network Interaction; Use with the GNU General Public License.\n\n Notwithstanding any other provision of this License, if you modify the\n\ Program, your modified version must prominently offer all users\n\ interacting with it remotely through a computer network (if your version\n\ supports such interaction) an opportunity to receive the Corresponding\n\ Source of your version by providing access to the Corresponding Source\n\ from a network server at no charge, through some standard or customary\n\ means of facilitating copying of software. This Corresponding Source\n\ shall include the Corresponding Source for any work covered by version 3\n\ of the GNU General Public License that is incorporated pursuant to the\n\ following paragraph.\n\n Notwithstanding any other provision of this License, you have\n\ permission to link or combine any covered work with a work licensed\n\ under version 3 of the GNU General Public License into a single\n\ combined work, and to convey the resulting work. The terms of this\n\ License will continue to apply to the part which is the covered work,\n\ but the work with which it is combined will remain governed by version\n\ 3 of the GNU General Public License.\n\n 14. Revised Versions of this License.\n\n The Free Software Foundation may publish revised and/or new versions of\n\ the GNU Affero General Public License from time to time. Such new versions\n\ will be similar in spirit to the present version, but may differ in detail to\n\ address new problems or concerns.\n\n Each version is given a distinguishing version number. If the\n\ Program specifies that a certain numbered version of the GNU Affero General\n\ Public License \"or any later version\" applies to it, you have the\n\ option of following the terms and conditions either of that numbered\n\ version or of any later version published by the Free Software\n\ Foundation. If the Program does not specify a version number of the\n\ GNU Affero General Public License, you may choose any version ever published\n\ by the Free Software Foundation.\n\n If the Program specifies that a proxy can decide which future\n\ versions of the GNU Affero General Public License can be used, that proxy's\n\ public statement of acceptance of a version permanently authorizes you\n\ to choose that version for the Program.\n\n Later license versions may give you additional or different\n\ permissions. However, no additional obligations are imposed on any\n\ author or copyright holder as a result of your choosing to follow a\n\ later version.\n\n 15. Disclaimer of Warranty.\n\n THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n\ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n\ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n\ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n\ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n\ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n\ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n\ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n 16. Limitation of Liability.\n\n IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n\ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n\ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n\ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n\ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n\ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n\ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n\ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n\ SUCH DAMAGES.\n\n 17. Interpretation of Sections 15 and 16.\n\n If the disclaimer of warranty and limitation of liability provided\n\ above cannot be given local legal effect according to their terms,\n\ reviewing courts shall apply local law that most closely approximates\n\ an absolute waiver of all civil liability in connection with the\n\ Program, unless a warranty or assumption of liability accompanies a\n\ copy of the Program in return for a fee.\n\n END OF TERMS AND CONDITIONS\n\n How to Apply These Terms to Your New Programs\n\n If you develop a new program, and you want it to be of the greatest\n\ possible use to the public, the best way to achieve this is to make it\n\ free software which everyone can redistribute and change under these terms.\n\n To do so, attach the following notices to the program. It is safest\n\ to attach them to the start of each source file to most effectively\n\ state the exclusion of warranty; and each file should have at least\n\ the \"copyright\" line and a pointer to where the full notice is found.\n\n \n Copyright (C) \n\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU Affero General Public License as published by\n the Free Software Foundation, either version 3 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNU Affero General Public License for more details.\n\n You should have received a copy of the GNU Affero General Public License\n along with this program. If not, see .\n\n\ Also add information on how to contact you by electronic and paper mail.\n\n If your software can interact with users remotely through a computer\n\ network, you should also make sure that it provides a way for users to\n\ get its source. For example, if your program is a web application, its\n\ interface could display a \"Source\" link that leads users to an archive\n\ of the code. There are many ways you could offer source, and different\n\ solutions will be better for different programs; see section 13 for the\n\ specific requirements.\n\n You should also get your employer (if you work as a programmer) or school,\n\ if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n\ For more information on this, and how to apply and follow the GNU AGPL, see\n\ .\n" Expire-at: 0 Length: 34520 tmpfhJ2Ca/spec/fixtures/LICENSE-secret-1.0/stored_file0000644000175000017500000010351412574355361021565 0ustar lunarlunar--- Salt: 9i/Bc69m7N4= Coquelicot: "1.0" Expire-at: 0 --- gj.]lS'ŽyNQ%6 M5i;윈qos}TEGX/ ֙4]˹Q|FrHn1Bʸyod,dv2D >+K#cZޛ8 5|OR5O tLp*Jtu~ə8  {9E^|u$A)҇FܪaXM\~z0옾O^;KZ'M(z겚o; _3s}oQBD a-j`đ,{Sh K(4+7ÓjIG7GxqL^k>U1d:@=؈ f،;I%`>'ϖ 7fI|0K J%t$H֩/˘H =:JBso+t69 *^'tWޑ<_CX—TUiM4&{i$["ۼê@>?C:s6O)sK{8W[[:ևuW¹d-(t:Y G`C);)Vc &7r5FšvPsW㴪<ķ*NʖN[1ryhʝzC]ia4M4x3:(bx󚇄$A)Hצ/`o6baBa4A=xm_`b8V :I"+H4wGL`4ҏ ¯u&k}RJJоNH`W9nbaJuGUx+ُqu?^Z+ ´?_aIa;WZ0 7RcV~ܼ e5&c N[kPPP;<5+ه>zxpd,∗OHWİh,uVF&ђ|Z9v*F7+Lf8gFMkҐqWUJ\JtKE/LQ}GPV:g,bVFvbNbĝ0e\ȯEaMt̂(xogfj%ؖϗc5 럙3Y,,^<1J;-0jP./ HI2AS˟:wyAQ_\K&%WyqlѠI_<s-X{gFEь3 h:]Sέv,ʁN&gD̙Q&Ԙ!1BMKӼJ< ઍ)ZX _ xTҰN[bT#mLY+5N3]ѧF{6/kNx͔q֜=xZ)΄:EɌ*yUN$!c?}Z׀3g=M{&y6J=G¢h\?-'v ?}x!=^?ذ ȌxjѭiC]s|IL +eTM\\#v hr&PO Fjnm/o1(^׽uӳDz$D8n[pOr= B&)S5ǬiQ@iC #rcd_{22YzJZb`~|fE酵>r eFiAɨ'5oT.C Shxkahc![jfxvYEQ*kC8WcI/D:-OFktr3psD&l7DRX\I>N \\B&zWKr/ rWDjCd w+^`קgA@ KTygeWJ'N/r+F{KUFܛ}/>Ga߱䯊 Œq:bO%ҵ!2THq ^R/%!oJĈzC Gu<$JiK`#g='*'*eӐ7bey`ѬCݲAt[3ske6xHQ JZ œdQe!ھ:2ML7}s;CO5@+ &nJvY*9vhwa<5l x'S#ʄ   9\1 {1T\ ;߾஍f}L ?y2Թzpmf? b=8R߷^uh{"+;Q̀({t֦sm!mYԚ0Q5@W`Jp@X5宕8ԗ/ivr.O1#TGW@  YX4SIH[>f=<Wk $לU^-Z*QhvNH6Hxո٢Aj{LFӎZ*.V=x r6]p;HZ60ujlj"RZrЕVp\!N'o˦rIR Nꠒ?l5dOTL,܇1ρEknWڹ&m>YDϼBѐ*u+Y􀔁8 BTNk+ռ+ʳO>Vq$B1/!E282Ru &b)urZI[PpBxd&z^'?'N -9$*QKl&o2,&F' 4\=vt+h oKy7'}sC 9AݲDd!ؚ9$R+U ӏa"  0)!LJMmUw1.q=,qkU ءD"B_w<*chv(h2+bGT0 ~p=O^gLp:Ԁ5@n MQ˔qDr 4&Q}}fMܦ\aym?],$ct޲{0f]~dm5OSBsB$!kfgpY_$Ծ|d!`K-r+c2Ļ^Te cQT3nm$'S7}M3i{Pw 2phyK"!W_g1C9Cyr,\rzux 1|&D{9LѬx rkEe5-%s⩕,HE˘ ]vJ MB\vhދJ Bً ^_>U ;H90.3T^.\>%d`髫$HjFY&VS|fz Z@M"#^; - ' T2D0?='ң0tܡ񃭜T ;,4!ѳS˳7!]\`PKd48pËX(GbÆ`Fjf_h.Ns_KSTi\VE*%vI,7gvG~A<0J5ѶbJle+q*=Ўɑrs[b&h\*Y76HTf'[Aªo/Y2+ОZI>:@*˝Sתªp(IzD |xoUamEDrXTJ8d `+K _Qx]͙~x4iNxP2z? ./4i뵿{*mE tUH=i.`c6U_r51>>J" ![jk4RS/!'*S _yѹRA)yfM2y:sQYp)Fۀ\c“]vRKa&*e9|Q$aoq6]>A|'f'#& q{Ey}a05ԯsґ/BKd h,~Mvy%5(O|󒚓 ͦj6L>hL,"DV0F)H:Uzer70aCF0@,Ek$OMcJ)+g!;aɷ*:~vdff gIdٯ>yRZu̡un`zWkkxW\mڄqbm7^ڷ%>P;*K \ql*=q0 {P3%nbşK#HY7pEH)revY ` DcW@pfJ'#.\e,##j&m@m\# *iӋ!vkѧ ۲`?D qLvq\=mT'{]O.$f /02eEpg-a6$åte;hI/J 9jiw<_1{B7'Rj^ʌWzU%KDz)Ak)qTJ]5;ssA@x1 *|}I 9%c:hޞTc||Cy)_1Ʈ0{h̃g󧟭7f-7,-.)N G;5PGeVI)-UheSqe漻+y)£[WP-23~5ʗ@R#]4_XpgwȌ?h* ѣQβ/k~ f]08F-d:¹vbYQjy4Ý[7c^D瑮%0UM]ǽFIx j{ّ flCN4tMΠR JjN 3H a!i[SE9eiqeyK ΞeCC˿SONw-@SءLfz,.[!dn)0WvmhY  Mh(CANV,L#E8`R#I,ПH<8yx9L̦~X4Gǚ/%泈nY2 *(,\;Z- Jsg5eNTSݴF%hCN>`:r\ 9,ȍ@=҄Opu6Ą'x9Jw)AW',(wHeDMls+ N$aT(C5@#Sh ͇! ~JB<͇7p:&ܫr t.k"pXvR0K ;\=iTLc]dIN%`_UPXFz;)aMf[LMV>? \e[lL!NšhBSL=9ruoĖ0-ڟR["1-yقagUmKVU5jkgaRCtʁn1VpS a'"yn=;K/Yi'%a%fiкKSyE ;Ӵ@*զ,e0bm\BcyPs}xE nֿ!+7}nC 0f=? )  ߞ2(q/B:wHNi+Sg9lz-=/" jJԜ%Jqs $?P1vAW.ˤ(e#dn9f^{2"un/ҫUQgoFm/s-FG:VPzZ8v 3_Z4Het^}wRGk >&/ExPSG1ؓe/u?-k ‹'+ qY g?sG>]Kœq[B~#6U׫i`t 0E~ ﯔ;|/lQ"6m~Xax;+a%ӯ

&/ֶqUxȃ{ӏD1 a:=՛,(0Y_>LdcEC$6-BjYyr;1B')̎w/p{QUǧ%_lΤhMێ_6'N,aْ4G)1vs;6sP%8 %gA9'i֭֙u5H+nA_hp~~,m f k ֣K5j3#- jG钪`QqeЮ'7:B0%6Z 6<#-7q+a,Z"Ȋ_z8"Fԑ{Ǘq|(_f7jhOA 8zޓy8 xq^ L#R^@AXɇ[stz1IG=M"jllP>)>Ǝt Hi5y|yWܘx3Evak4?^nFhsw!D{fYsm|̛IW+NċVZR{+@t,ݴ1å1fFN|@I)߮WDenvgyґ3mVK/߱w}J,4"k]vb?RPB.)jҪ< |܅梘`xE9߭ȸ 蕅GY%zM^(mbeܒ S}֙g/}$WA(z3O`:Pd7&q̈cW-ئ4JAhEFdZ7h?b=%,LϧF @"/@5tSaiBLdu1Ge=U-P"11\Y| 7Sd܈wS4`ft02S8'Ua /eb,_QRqءCWyI8B,}-F[ǰڬUŀGn_H[ݬ4aG9Άwl*> #a|G5P KvXd\RuW `PO_m^COEɣ۸PO\_빒>Z"E# _HP)+fgUZfa۞-.rLGD%.o6a(pD:D=ˮ (&/k|n!nߑv`syz-/7sm{ O=2 یTgu8/dKhYpOVlw8 \:g*ju@2u90!i.B-L3d v1 Ko;4 \OfLt2."4(+w[qTm[XVUzf)[@K1OY*{Fma=MΟx2ƴՊjW+TNZ]Fy.M>>_9iR̖$%F*uM'OO[Dߑ猘U2c?6,5QmW4/o{O.gR'y1ɪnɘv}LzCc wY jbw 6{NF@bVp[Y{x#yRѢ[U>#QҵL_5/lYjՌgov(~=K2|Tfb<[Z?עxQhfd1QچD{V$ڮQqLRREt`vzۧTo7 $8~AI*#jz3r?又VyRTG#z؜x|5$q <%@ؤ-ﮕQ@Dx3a +0%5}nÁ+w$Սܫ(T"8d9j;;)[R)ͰB٣;*qN\\~Zc蝋:s Cr<s_nH5l7"0بߓS ߿= XQ%19 na;y]&[&ڼ;T$dH'.# CxP.a9Oi^:>uP\W| ŷ x\4)]}RTx@1֖j^5A橓L#qpSڋhzTsn ,1Yxʺ ؼbw} CG3BUW@PzmŬfƺID1mh8КKjC\s}XOFP) *Q2UQ7Xz,"^*B|͓i E>$w~rdK:ϳ"-l};7kOδ3Lj(b+ y q$8aƐv>p)СZ 8Ҁ<Щ[ 73u|3<['3'}ee(K]F}u$gIP| /vµt͌,o-O#}w2IȴEFN)?i=^ )gZmM/RxFQ4ADs^&gԙ&q.:x)N}g I#ԓV:CHVAVGz:PO/需~Htp?T$'tU ;PWVxxC@Aqrb̥VQȓ%B?nkSrTʶI(8jiq]0rU1.ai Ҋw7U(W,qWϚQIH{7rP9 TRn0v^YWyy !t G6"z1sg}L!yr~=J>tqx2obC(EPY,B\TqPDY<\ʫ{ܳvcJsPG̈1 c_`@a(hvkOBd~y9ss G`9Urw)˃y1+3g]m"BBZˈŹ./Z%U0P-oٵ&\ ;/ŏy?zӦ ` 36$~# !gaZ\eƱp<'8ٞQEUѰ4ˏ g?,I,V h{`#!,vB%v!7U C> |wƇI=}~hݴ,|Ӈ6os3gm9ckHksBFҟEp 6Λ"=:JgZ>廞cϺ<{y:az>LPYwH+ g̏]®I7u=lճ&\pP\ژ|\bSn,bJ@bXYlvxPN2Sp]ǥ,=hw\G xtpijLq=M`(#3hTj6! y)m[tY;Em\!Oǻkы2g7H٧M  ܿo:w+R^hrb3=A/,:Eߛ7KbדTc32x #nr` \{Ut=bO܉jWu@ER\8R; GO/>nDurTT9[(an (yF1Gs-סRdֲG>ȅn!48)卷9=, +6[hh/3k&#./[V23`e\pTk^>+bP2w9"1a2#L%-cd!FC&{̬Z2]k cEX3 , ιTh2 ${8(ʩDU5`~i]-ycabkX^MgD1-iI ɞ˶qfa(/1=̎remP0a]G^㎏Q?>V(GLz2< g3ٞ5n0wmnW.R#ph$8>%zuЦ 08b?:\V&k#CuyBKȑqR3՚B 娔$v `rN%g¼)HJ]BJ:BbQ4T&;ڡ2qB2ഖѽB?(@%iJɮtuAȺ DxV%Hr=ģta|jˤ#Va *֗ߖ}\P !/"ќz`$RZ9슣yygҫ-}K9l#),õOPB:HesۨeӉkN,'8NB(4wZ֔Xah?WBg U`iz0YW<*QK:EgrѬNMcq$f~t솦]_:'9h)bW`υ[Cqyc.1LX<(eІRL3^ɀ$~K $3EI%04j,bu ⛅oL6GkNvxnP[K: XLj dž쯢5PC-N?6Q'<'hm>bT,#n }C "/|sQ<;CXm ;u,6n΄J(ܸlAq/J0 ;sŁOͬ`<71;rzdFhKJYi3QMl(ڹf7iȓhIgw@ O&lf)V *.w|(A9#Ge!w50=8M7&5T-'ƫ}LZ#GT>{vDIWp#~aM1;I[TX*Kp@"^@[Ңb>+Lg~qmo&*33{[Mδc)Y4ll$Yjr2tOkr#9|L D}EPCPˮx`Zo];y\k@x{ !lpb!ت=t@Վz/NHWzbPEL.;BS59{L34XUI }~O@%)/rF,Ta%*ڙ2:3I&`t+P.9 19`Bqu%-d #R$q^ ˰1):\P K!M+|&9q=>\␍n]nM?CSsj\ˁSûՉRVÚ_$ԥq{ۨ7K~"Q{OvDZJ6qjAN t=LƻhbcE9 NSlK=0& 3պkh?q풲>gB&O)VZh~\DIky3I 3p"bh[|KqC#.,讙G7eøz[ت$tB#ZÚoScF s gdq5Y.Ʈ);!ҐVpp͠0RB-qܪ@eխb Yc'$fIzU۠P`~ VA4(*ÜXB,wRWRm`ܓ_Ge4֫ٿLT5zVf"Qzu#:3ʽ#2**/2J^{q i^=Z{/ GaWx R5lw >J9. & 67եM)GqѣYL\r]quپLXX /XDGÖZZ'+Q?FsE)}WKg|"|Z"M&¿@y "Xw ' vx'i,JIC`+ln jY&,b[C!&3[WsOŧZX;\TQ5!Oj`X$Ð|]6ġ_%ӹ(՟eI ?02#9`#es AM4H=!q8Ӂn~5a>C~h(dc~~ޕ8sU}KYQTZ,m۫<0׀:#J/_@cߧ5h۹XЏQcLÍh% :7";E @M/q~e=.օeSSÁ x CM}|e )W+vw -z?QKWh+ża>L'fu/NE`t)>hnثr+/qC|. *JcTluBxƷ  =jh #]y}P3z&W(&KڛV %87"hWkSXBSL w X3!BxBhiX4Ga ozK`bmhʽltq;B,wQ Hfp[S߽FOPP8S3Y' uY3R5P7k; E#<`!T"LюX˚ebVEt/R.G\t¤7^>3gjH9)W'i.4M/ -\('Lv0ڊB!?+l$ ǽw G"̆TEw@sA?%ņ6Fx2^wik=pUfG!%a%bNqC~4;nWvR@|Jm6%Ǐ%\_ԕDr`JA—%* t t?$ ?Ԛl 7}響bU5$sdHljGJ1=b!tEH&HX汰+QQ~S.H{piAHG$;X;e)w & ]ςZ[l,/^w{h9n}gړ%mxu>ofZ3maY]mB@bmf\}uBQskɊ30<%U! \ن3' 4Z~:i"0عANP -;MO}fJS(ԖG` Q#cpſSGo.>("3hwC%!%' n+ZX$a7buzUzI`n,S"OC%Q8c~?y1J,⽢3uշ#‰ldC8je rz!=iˡP(.OF`V "IZXkCYkE,|7/w<<ڊi9[i/Fz6wve֡PH1 e 9e#6 |uAN lKY,R屭HSG8K˞olp?c' *+pGӰp.I1NⰐ,R$7=]n@KD!K=KhzH*ڨSz$ ݥZ{v }ڧR<: Mn#h)3 G꼳: ?#T~oԼR%D庋uc=V?q5>O/Mo e{Y{\}4酋>)Ί^ =?NEm3zz$k^p!1E&t0yZMry.Ąv,1^" hQK|L5 q3S1g yӨExIte9㔴 AO#My3˛~oL|njϑYϸ- #wɹL xH;wtE^EQ[UgyޞFeD:H6!B\#rP@꽱.j&iDBFLhnي 6̝l9D}wXcXj.::sdBv'-]Mo8VK:}B>twdl)jWsa6~-4w=H΅[7嘟DxXNs D͎ :8.x%Ћ }9J32$#|M S?k͉ظExh$6)ɍJR-y3RX.n){seǽpkQn,Bi[^Al`)bSX-8$:Sr G {P'4&]{>rܽq,ꠐ,;Z_u*!Yar:Aco sc|do~9 OÙNTNmS W~ '],Nja} =f p凟f1cc 7wxԷƗ~ߘT42n[> CqYwrǸaOQ(h2,,@NV_v1N+U '<61 `#=܇ ~#&li1~^wB^SZT8M 8rJF1\}puMs6.|32ɫԳAu5?*eЬB SEe82.Z^)CQQ3#dYݤWSez79?QF&ߡ٢4 oM,#D+ĹHsT2Qs;=VSHI76Y&ڥ7a|&;616t>;*`;դ|zUw t,=$۹!=$4/a2*{$啰c" ?6>I?zTiT}bkR@iM%Q2IaNm0ˣ>_c~-{fyo$dєuXER j)D Oq]eCu'bTs߻ۇ_FB"}Oʑ2Q#u9'^;Ya(ؽc}G+a9o;]d?jՂZhV8+7 < RiXM[3;*]v9nGQ7K6n tY2meҾ7y/A1 •?+K@5rw:Q\_MGګUlwnw!'&t-jhL!3:-U=;V}CC3l8Bcl>HY}0uuP֤bBkO uY zPʅ ,RL_8OAwfIC4aH>L[κqi#䶢 {5SnH2MZ٭+Gu$S>yCʑ̂kC#k89X牵wO ?,-{홓@3E ߢD ]۶ے9T۱Ҫ6Teשr 6Rl,:Pa39&8++: d5@{Zî͒fa9 4xSǑm&#?ђ4\?^"_ nnϏvn /Q2͔wJ8_G@PN\1@Jo~!8֯&!i4^Q{:` - <p gm;Mx x]pii^Q1b`Yhj cNn-۱P\<ʚ'OJQ{<$F@w=ڐk cndA^Պp?(=d#3GKV9tZ}eĠS2+חDhҨ-NrJ4 5yw?zeږqM ]twW9oZW"8Gbrf &rEÏ&nwmYO#>74~</{mIU__Y?)AqjMldQfܭ9eVIUU;Ntfi!M`y@.dlJFܮx4lZZ%6L۔Jz 9%p;S&^-t8!P&S#8WF?&m&L:%@Iu V20A-gfw;5Nv?;)ۖCnU:h4(#~}K,;9" 5o~sMK$JWiԑ8T践{E,]6f_<l[b%/ urR朖GZ1>~O:򹪎gbIG`ȤϧǧtQR6TD?Vn1$7@5{㋾t_wG(W&6Id$]OM (FڴS&׶/ -:PB]@H8LA/enK8 y@`NRuWT=@gz=̛AMQ&5R%bA>N4'NʤI (SG$zJ}#;/M~O4)G~2&bppSC!x/ٷl6'kSH?d*^˄'LZWB(H?.A j0"*K)C0CDҦϵ66?sDAZݡ7G~7ҍx ưLh"@A5\%$^ʊB+5PbL׷&b#%&>r~~'(M>.?:7GZ`,+qO:YЦKmV:'|V?(wU_UQRs.a6ٖkUɔ@Qx&j_M)Y4 vkgvq,UOڇwz iЪ<j ~BPߚ +rݮЧrP^*BnqD N8Wz΄CLqcb4k046Di= <_cWy"L˜t薺r3{nrdQKzx:Lx]~%&3E9)emu9[j Ħ`%AB`#QS<pJsmJ9Uh@=!Ibܾ$wݤ%!،Cξ^ E<3wZe4!wixp%]#*hNffsi?ɥ"WȦ%Jο%\ qmԘM I(0 ܌: yI'ŝt#Mn|r`,:YdGWHq z=(3$.BGAKb(=9SA m䌭LfڍL̢H3CO|\I3vטcQtF&>q$ u۹o>geCu<+d/6 yGݤ3c s(O|pC͌A$T>1FZ'gft ̸y{u/$|RV]O"!C"[xEl h@26).RBJ$f0sGgه+79uxZ8r ѯ`-·>snD 7`wC5!zhQݿg2rE ^FP[ocH,&ܯuƠiQ$e|1&]v&:=vC*v 2l E#ig>=?'}|g~K_WFP>q5})_y pBG:U: '{C5$w;X}lJdS̖uitt _BF›^ks@zJAʒw&-1E?M-iA* IKxdNpϮ539p.D6iV9Dڛh#UZGrԑ}Id@x;eR )p~@:_{DɢUеr{d{>wV -aZ;np݈#[焏Yda4 {8 ~B6ͺneb/>yYC4~ut9)O Y:Sx:|φbAb}˅58=V `4-HxKL4HVw[3{pjz7eAh}`@H\q Wg6{x{fUvbɘ5do.FH'c!|'K qjڳ '[SOc҂`erOχf GBf $mfg{d^",[Kr`~#BI%GOChݒۂElQ!=xo8+bQŚ SF,?d'w8}a;ϟH Iۨ `uB I ~KM7ݶDQޙ6NL>7|#@BZэbG~}%Vk?-Ȳ9_P*|/={Laչ%jNe)OzST+ hv%׿fe~ :VpuN8'.ʮ[y}jh~Ng|[1`~rUF51 1꺢5Gݸ=v2vQ{_3s`tVJqJ +cv뻊yx65%`i$ᠳصMF^*g3ՒNj`qCA}'B 7l1M^RDC3$9C:9\5Upi7aaeNfXt! DY\Q3&6C%SvQKL$mZɗd2Jqtprr*7sh;>S c .ﬣ;WA3;;lÊDZ|&%{<~$Fܖ _}]>[82ӑ}4f[P4[ G"0Kqf{Mv:+eO!UidO7c6b3Eٮ gSeUdgw?-2MwV /=kjg䦨!'<'4rtlzgx$"V~hf`0{9Koxw\wׁ8ZQOT?"mӔ^SǶJ'< R,8kSA5شwd~u9Z@NmIs,Zƅ72& G]u.I }Hq-zGF]|s+c W-/N0 |˥o߇NP4Ė7$1)^N)ci5Y/sD*eNDiEGŸor*x*+\EIdZZp8G cT>:h5|}k+VǬCVYŲtj$kQt#-8S,iH5nH|^T¦'6߷9ڦ;HcQ֧VNe<|Hd#9{+'ֈHd"%n*¾?H5DK? -6cSa]-_Ee+D4TӶoHA跣o]4ɒ60r+v-<3bp]nC?1PZ?7Z 怰*; F~-KʁNZ,ryqH3b}1Xҽ* YpJɃnYQ(ڏ*zڿ |I`^3wyzoV{}h-|Q\5r7/ηTFZHIJ1{'UIm Xv ]js \W:fRPvE7^Z4‚|+m h&kN.ɺZhw#dRĄd0}ht2Ǒz9 ~ C/VQjQMbz(R~gPAS(O@Sl7fw$>vf$maHA'0 d#5[|M:qe%mBO"qy_(o=wHRnZ^I ;$-6ӣCt.0.[cfswBDHL(Ky\>ݣ,Xs>Q"ouc<*2$ۿ>­ÞN8T9c+_0tA=ka$:$?jfx)w$!{R_ui \F=ϖ@|Ɣo):,Y״A1r9ulS42Cq0׉Zb{0z(Bz}b$ sQ0N*qʋ0rW#CX{TLW 1yAPyjA{A pV8{*_ 4 @0,oYXf>zL|QCV;/ u"en,J٨Ӵ %~/:'e$"HZyt,~07EmZp;Ư| ߃ՉCk{\epU8h,G.{5*{Q/Y,ę-ѡu!{ Ca8(/þymUح-lf(m{$Y,ctݩ3Ǜ ÃՊcc:1EU a4t1D]%v9ʖRuV  !|4ՠ$ݦ1[qpO(VsHH(i[b$|j7Gzߠ#{, ',ߒb'%c *?]{s(,3cgE,|o@΋Eh/gGNĸA ڥG AMDlKI%(Qr>&u1mʼn9b^ v 9x: 2Ï*KJ WQr,@ed}jheGB fo׎~(7*礶Gc-S'?3%Fd@\r%G7žed qXH7{ fPfovC--&G(mNp%B_lvRy(]-EB>m'%WFrcN2&X"Z@40s0LƎUJ,dL^6qLZB)eY^#i?;QLlW*i'!"Ah^$uE5R fѢ*Asp&pu6вFEdJl(bJY{DP=TQ2-:( c)huy)@I8t&~WzS]C uŰ-]Б^:q"YOz8\Cե9+1tF(tL"Z&],@B; /iĂ6+/.pqe:QwF^hUi?zz;MKm҇[8mNJg4r^3oxNEyDϕ#Vz4\cJk,{Md7O[H - بә|e,gŲ]HgS¡Zǵ6/@jU4͗Hk*­l<_8 r =y.1sg5 + w@Rs) # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'coquelicot/version' require 'coquelicot/auth' require 'coquelicot/stored_file' require 'coquelicot/depot' require 'coquelicot/rack/multipart_parser' require 'coquelicot/num' require 'coquelicot/helpers' require 'coquelicot/base_app' require 'coquelicot/rack/upload' require 'coquelicot/app' tmpfhJ2Ca/lib/coquelicot/0000755000175000017500000000000013026234541014575 5ustar lunarlunartmpfhJ2Ca/lib/coquelicot/rack/0000755000175000017500000000000013026234541015515 5ustar lunarlunartmpfhJ2Ca/lib/coquelicot/rack/upload.rb0000644000175000017500000001543412574525230017342 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'sinatra/base' require 'rack/utils' require 'rack/rewindable_input' require 'tempfile' module Coquelicot::Rack # a Request class that leaves applications to deal with POST data class Request < Sinatra::Request def POST {} end end class Upload < Coquelicot::BaseApp set :logging, true set :views, Proc.new { Coquelicot.settings.views } set :additional_css, Proc.new { Coquelicot.settings.additional_css } def call(env) if handle_request?(env) input = env['rack.input'] input = input.input if input.is_a? Upr::InputWrapper if !@warned_of_rewind && input.respond_to?(:rewind) env['rack.logger'].warn <<-MESSAGE.gsub(/\n */m, ' ').strip It looks like the input stream is "rewindable". This means that somewhere along the process, the input request is probably buffered, either into memory or in a temporary file. In both case Coquelicot will not scale to big files, and in the later one, it might be a breach of privacy: the temporary file might be written to disk. Please use Rainbows! to serve web request for Coquelicot, which has been tested to provide and work with a fully streamed input. MESSAGE @warned_of_rewind = true end dup.call!(env) else unless env['rack.input'].respond_to? :rewind env['rack.input'] = Rack::RewindableInput.new(env['rack.input']) end @app.call(env) end end protected def handle_request?(env) env['REQUEST_METHOD'] == 'POST' && env['PATH_INFO'] == '/upload' end # This acts much like Sinatra's, but without request parsing, # as we have our own method here. def call!(env) @env = env @request = Request.new(env) @response = Sinatra::Response.new @response['Content-Type'] = nil invoke { dispatch! } invoke { error_block!(response.status) } unless @response['Content-Type'] if Array === body and body[0].respond_to? :content_type content_type body[0].content_type else content_type :html end end @response.finish end def dispatch! filter! :before catch(:pass) do return process! end forward end def error_block!(status) return unless (400..599).include? status haml <<-HAML.gsub(/^ */, '') %h1 Oops… %p= response.body.join HAML end def process! # Stop users right now if input has already said the file is too big. length = @env['CONTENT_LENGTH'] unless length.nil? length = length.to_i error_for_max_length(length) if length > Coquelicot.settings.max_file_size end MultipartParser.parse(@env) do |p| p.start do @expire = Coquelicot.settings.default_expire @file_key = '' @pass = Coquelicot.gen_random_pass end p.many_fields do |params| @auth_params = params begin @authenticated = Coquelicot.settings.authenticator.authenticate(@auth_params) rescue Coquelicot::Auth::Error => ex error 503, ex.message end end p.field :expire do |value| if value.to_i > Coquelicot.settings.maximum_expire error 403, 'Forbidden: expiration time too big' end @expire = value end p.field :one_time do |value| @one_time_only = value && value == 'true' end p.field :file_key do |value| @pass = @file_key = value unless value.empty? end p.file :file do |filename, type, reader| error 403, 'Forbidden' unless @authenticated max_length = Coquelicot.settings.max_file_size # We still compute the length of the received data manually, in case # input was lying. length = 0 @link = Coquelicot.depot.add_file( @pass, 'Expire-at' => Time.now + 60 * @expire.to_i, 'One-time-only' => @one_time_only, 'Filename' => filename, 'Content-Type' => type) do data = reader.call unless data.nil? length += data.bytesize error_for_max_length if length > max_length else error_for_empty if length == 0 end data end end p.field :submit p.finish do unless @link.nil? redirect to(@file_key.empty? ? "/ready/#{@link}-#{@pass}" : "/ready/#{@link}") else params = @auth_params || {} params['expire'] = @expire params['one_time'] = 'true' if @one_time_only rewrite_input! params pass # will forward to the next Rack middlware end end end rescue EOFError => e raise unless e.message.start_with?('Unexpected part') error 400, 'Bad Request: fields in unacceptable order' end def forward # The following is to authenticate the request arriving # in Coquelicot::Application @env['X_COQUELICOT_FORWARD'] = 'Yes' super end def error_for_max_length(length = nil) if length message = _('File is bigger than maximum allowed size: %s would exceed the maximum allowed %s.') % [length.as_size, Coquelicot.settings.max_file_size.as_size] else message = _('File is bigger than maximum allowed size %s.') % [Coquelicot.settings.max_file_size.as_size] end error 413, message end def error_for_empty error 403, _('File has no content') end # This will create a new (rewindable) input with the given params def rewrite_input!(params) @env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' data = Rack::Utils.build_nested_query(params) env['rack.input'] = StringIO.new(data) end end end tmpfhJ2Ca/lib/coquelicot/rack/multipart_parser.rb0000644000175000017500000001602012574355361021451 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'rack/multipart' require 'rack/utils' require 'multipart_parser/reader' module Coquelicot::Rack class ManyFieldsStep < Struct.new(:block) def initialize(block) super @params = Rack::Utils::KeySpaceConstrainedParams.new end def call_handler block.call(indifferent_params(@params.to_params_hash)) unless block.nil? end def add_param(name, data) Rack::Utils.normalize_params(@params, name, data) end # borrowed from Sinatra::Base def indifferent_params(params) params = indifferent_hash.merge(params) params.each do |key, value| next unless value.is_a?(Hash) params[key] = indifferent_params(value) end end # borrowed from Sinatra::Base def indifferent_hash Hash.new {|hash,key| hash[key.to_s] if Symbol === key } end end class FieldStep < Struct.new(:name, :block) ; end class FileStep < Struct.new(:name, :block) ; end class MultipartParser BUFFER_SIZE = 4096 class << self alias :create :new def parse(env, &block) parser = MultipartParser.create(env) yield parser parser.send(:run) end alias :new :parse end # Run the given block before first field def start(&block) raise ArgumentError.new('#start requires a block') if block.nil? @start << block end # Parse any number of fields and execute the given block once the next step # has been reached def many_fields(&block) @steps << ManyFieldsStep.new(block) end # Parse the given field and execute the given block. If no block is given # the content of the field will be lost. def field(name, &block) @steps << FieldStep.new(name, block) end # Parse a file field with the given name # # The block will receive the filename, content type and a 'reader' proc. # The later MUST use the '#call' method to retreive the next part of file # content. It will return 'nil' once end of file has been reached. def file(name, &block) @steps << FileStep.new(name, block) end # Run the given block when reaching the end of the multipart data # # Subsequent calls will be run in the reverse order def finish(&block) raise ArgumentError.new('#finish requires a block') if block.nil? @finish.unshift block end private def initialize(env) @env = env @start = [] @finish = [] @steps = [] end def run @io = @env['rack.input'] @reader = ::MultipartParser::Reader.new(boundary) @reader.on_error do |msg| @events << [:error, msg] end @reader.on_part do |part| @events << [:part, part] part.on_data { |data| @events << [:part_data, data] } part.on_end { @events << [:part_end] } end @start.each { |block| block.call } @events = [] parse_input do |event, *args| case event when :error msg = args.shift raise EOFError.new("Unable to parse request body: #{msg}") when :part part = args.shift handle_part part when :part_data, :part_end raise StandardError.new("Out of order: #{event}") end end @current_step.call_handler if @current_step.is_a? ManyFieldsStep @finish.each { |block| block.call } end def parse_input(&block) loop do block.call(*@events.shift) until @events.empty? buf = @io.read(BUFFER_SIZE) break if buf.nil? @reader.write buf break if @reader.ended? && @events.empty? end end def boundary ::MultipartParser::Reader.extract_boundary_value(@env['CONTENT_TYPE']) end def handle_part(part) previous, @current_step = @current_step, lookup_steps!(part.name) if @current_step.nil? if previous.is_a? ManyFieldsStep # we can still parse more fields @current_step, previous = previous, nil else # a new part and no more steps, something is wrong! raise EOFError.new("Unexpected part #{part.name}") end end if previous.is_a? ManyFieldsStep # call handler if we are moving to more specific steps previous.call_handler unless @current_step.is_a? ManyFieldsStep end case @current_step when ManyFieldsStep buf = '' parse_input do |event, *args| case event when :part_data data = args.shift buf << data when :part_end @current_step.add_param(part.name, buf) return when :error, :part raise StandardError.new("Out of order: #{event}") end end when FieldStep buf = '' parse_input do |event, *args| case event when :part_data data = args.shift buf << data when :part_end @current_step.block.call(buf) unless @current_step.block.nil? return when :error, :part raise StandardError.new("Out of order: #{event}") end end when FileStep @current_step.block.call(part.filename, part.mime, lambda { value = nil parse_input do |event, *args| case event when :part_data value = args.shift break when :part_end value = nil break when :error, :part raise StandardError.new("Out of order: #{event}") end end value }) end end def lookup_steps!(name) index = 0 found = nil while current = @steps[index] case current when FieldStep, FileStep if current.name.to_s == name found = index break end when ManyFieldsStep unless found && @steps[found].is_a?(ManyFieldsStep) found = index end end index += 1 end return nil if found.nil? @steps.slice!(0, found) @steps[0] end end end tmpfhJ2Ca/lib/coquelicot/jyraphe_migrator.rb0000644000175000017500000001303312574355361020503 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . module Coquelicot class JyrapheMigrator class << self def run!(args) parser.parse!(args) usage_and_exit if args.empty? jyraphe_var = args.shift migrator = nil begin migrator = JyrapheMigrator.new(jyraphe_var) rescue ArgumentError usage_and_exit "#{jyraphe_var} is not a Jyraphe 'var' directory" end migrator.migrate! $stdout.puts migrator.apache_rewrites(options[:rewrite_prefix]) end private def usage_and_exit(message = nil) unless message.nil? $stderr.puts message $stderr.puts end $stderr.puts parser.banner $stderr.puts "Run #{$0} --help for more details." exit 1 end def options @options ||= {} end def parser @parser ||= OptionParser.new do |opts| opts.banner = "Usage: #{opts.program_name} [options] migrate-jyraphe [command options] JYRAPHE_VAR > REWRITE_RULES" opts.separator "" opts.separator "Command options:" opts.on "-p", "--rewrite-prefix PREFIX", "prefix URL in rewrite rules" do |prefix| options[:rewrite_prefix] = prefix end opts.on_tail("-h", "--help", "show this message") do $stderr.puts opts.to_s exit end end end end attr_reader :files_path, :links_path, :migrated def initialize(jyraphe_var, output = $stderr) @files_path = File.expand_path('files', jyraphe_var) @links_path = File.expand_path('links', jyraphe_var) unless File.directory?(@files_path) && File.directory?(@links_path) raise ArgumentError.new("#{jyraphe_var} is not a Jyraphe 'var' directory.") end @output = output end def warn(str) @output.puts "W: #{str}" end def info(str) @output.puts "I: #{str}" end def migrate! max_expire_at = (Time.now + Coquelicot.settings.maximum_expire * 60).to_i migrated = {} get_links.each do |link| begin file = JyrapheFile.new(self, link) rescue Errno::ENOENT warn "#{link} refers to a non-existent file. Skipping." next rescue SizeMismatch warn "#{link} refers to a file with mismatching size. Skipping." next end pass = file.file_key || Coquelicot.gen_random_pass if file.expire_at == -1 || file.expire_at > max_expire_at expire_at = max_expire_at warn "#{link} expiration time has been reduced." info "#{link} will expire on #{Time.at(max_expire_at).strftime '%c'}." elsif file.expire_at == 0 warn "#{link} has an unparseable expiration time. Skipping." next else expire_at = file.expire_at end options = { 'Expire-at' => expire_at, 'One-time-only' => file.one_time_only, 'Filename' => file.filename, 'Length' => file.length, 'Content-type' => file.mime_type } coquelicot_name = file.open do |f| Coquelicot.depot.add_file(pass, options) do f.eof ? nil : f.read end end coquelicot_link = coquelicot_name coquelicot_link << "-#{pass}" unless file.file_key migrated[link] = coquelicot_link end @migrated = migrated end def apache_rewrites(prefix = '') return '' if @migrated.empty? rewrites = [] rewrites << 'RewriteEngine on' migrated.each_pair do |jyraphe, coquelicot| rewrites << "RewriteRule ^#{prefix}file-#{jyraphe}$ #{prefix}#{coquelicot} [L,R=301]" end rewrites.join "\n" end private def get_links Dir.entries(@links_path).select { |n| n =~ /^[RO][0-9a-z]{32}$/ } end class SizeMismatch < StandardError; end class JyrapheFile attr_reader :filename, :one_time_only, :mime_type, :length, :file_key, :expire_at def initialize(migrator, link) @migrator = migrator @one_time_only = link[0] == ?O File.open(File.expand_path(link, migrator.links_path)) do |f| @filename = f.readline.strip @mime_type = f.readline.strip @length = f.readline.strip.to_i if File.stat(file_path).size != length raise SizeMismatch.new("#{filename} size does not match what is in #{link}.") end key = f.readline.strip @file_key = key.empty? ? nil : key @expire_at = f.readline.strip.to_i end end def file_path File.expand_path(filename, @migrator.files_path) end def open(*args, &block) File.open(file_path, *args, &block) end end end end tmpfhJ2Ca/lib/coquelicot/stored_file.rb0000644000175000017500000001532712574526002017434 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'base64' require 'lockfile' require 'openssl' require 'yaml' module Coquelicot class BadKey < StandardError; end class StoredFile BUFFER_LEN = 4096 attr_reader :path, :meta, :expire_at def self.open(path, pass = nil) StoredFile.new(path, pass) end def created_at Time.at(@meta['Created-at']) end def expired? @expire_at < Time.now end def one_time_only? @meta['One-time-only'] end def self.create(path, pass, meta) salt = gen_salt clear_meta = { "Coquelicot" => COQUELICOT_VERSION, "Salt" => Base64.encode64(salt).strip, "Expire-at" => meta.delete('Expire-at'), } cipher = get_cipher(pass, salt, :encrypt) length = 0 File.open("#{path}.content", File::WRONLY|File::EXCL|File::CREAT, :encoding => 'binary') do |dest| until (buf = yield).nil? length += buf.bytesize dest.write(cipher.update(buf)) end dest.write(cipher.final) end cipher.reset File.open(path, File::WRONLY|File::EXCL|File::CREAT, :encoding => 'binary') do |dest| dest.write(YAML.dump(clear_meta) + YAML_START) dest.write(cipher.update( YAML.dump(meta.merge('Created-at' => Time.now.to_i, 'Length' => length)))) dest.write(cipher.final) end rescue Errno::EEXIST # do not remove the file if it already existed before! raise rescue FileUtils.rm path, :force => true FileUtils.rm "#{path}.content", :force => true raise end def empty! # XXX: probably this should be locked paths = [@path] paths.unshift "#{@path}.content" unless @features.include? :meta_include_content paths.each do |path| # zero the content before truncating File.open(path, 'r+', :encoding => 'binary') do |f| f.seek 0, IO::SEEK_END length = f.tell f.rewind while length > 0 do write_len = [StoredFile::BUFFER_LEN, length].min length -= f.write("\0" * write_len) end f.fsync end File.truncate(path, 0) end end def lockfile @lockfile ||= Lockfile.new "#{File.expand_path(@path)}.lock", :timeout => 4 end # used by Rack streaming mechanism def each raise BadKey.new if @cipher.nil? # output content if @features.include? :meta_include_content yield @initial_content @initial_content = nil file = @file else file = File.open("#{path}.content", :encoding => 'binary') @cipher.reset end unless file.eof? until (buf = file.read(BUFFER_LEN)).nil? yield @cipher.update(buf) end yield @cipher.final end @fully_sent = true end def close if @cipher @cipher.reset @cipher = nil end @file.close if one_time_only? empty! if @fully_sent lockfile.unlock end end private YAML_START = "--- \n" YAML_START_RE = /^---( |\n)/ CIPHER = 'AES-256-CBC' SALT_LEN = 8 COQUELICOT_VERSION = '2.0' COQUELICOT_FEATURES = { '1.0' => [:meta_include_content], COQUELICOT_VERSION => [:current] } def self.get_cipher(pass, salt, method) cipher = OpenSSL::Cipher.new CIPHER hmac = OpenSSL::PKCS5.pbkdf2_hmac_sha1( pass, salt, 2000, cipher.key_len + cipher.iv_len) cipher.method(method).call cipher.key = hmac.slice!(0, cipher.key_len) cipher.iv = hmac cipher end def self.gen_salt OpenSSL::Random::random_bytes(SALT_LEN) end def initialize(path, pass) @path = path @file = File.open(@path, :encoding => 'binary') if @file.lstat.size == 0 then @expire_at = Time.now - 1 return end unless YAML_START_RE =~ (buf = @file.readline) raise ArgumentError.new("unknown file, read #{buf.inspect}") end parse_clear_meta return if pass.nil? init_decrypt_cipher pass yaml = find_meta @meta.merge! YAML.load(yaml) end def parse_clear_meta meta = '' until YAML_START_RE =~ (line = @file.readline) do meta += line end @meta = YAML.load(meta) @features = COQUELICOT_FEATURES[@meta['Coquelicot']] unless @features raise ArgumentError.new('unknown file') end if @meta['Expire-at'].respond_to? :to_time @expire_at = @meta['Expire-at'].to_time else @expire_at = Time.at(@meta['Expire-at']) end end def init_decrypt_cipher(pass) salt = Base64.decode64(@meta["Salt"]) @cipher = StoredFile::get_cipher(pass, salt, :decrypt) end def find_meta return find_meta_in_meta_and_content if @features.include? :meta_include_content begin content = @cipher.update(@file.read) content << @cipher.final raise BadKey.new unless content =~ YAML_START_RE content rescue OpenSSL::Cipher::CipherError raise BadKey.new end end def find_meta_in_meta_and_content yaml = '' buf = @file.read(BUFFER_LEN) begin content = @cipher.update(buf) content << @cipher.final if @file.eof? raise BadKey.new unless content =~ YAML_START_RE rescue OpenSSL::Cipher::CipherError raise BadKey.new end yaml << YAML_START block = content.split(YAML_START, 3) yaml << block[1] if block.length == 3 then @initial_content = block[2] return yaml end until (buf = @file.read(BUFFER_LEN)).nil? do content = @cipher.update(buf) block = content.split(YAML_START, 3) yaml << block[0] break if block.length == 2 end @initial_content = block[1] yaml end end end tmpfhJ2Ca/lib/coquelicot/base_app.rb0000644000175000017500000000311113026223065016667 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'sinatra/base' require 'haml' require 'haml/magic_translations' module Coquelicot AVAILABLE_LOCALES = %w(en es fr de el) class BaseApp < Sinatra::Base include FastGettext::Translation helpers Coquelicot::Helpers FastGettext.add_text_domain 'coquelicot', :path => File.expand_path('../../../po', __FILE__), :type => 'po' FastGettext.available_locales = AVAILABLE_LOCALES Haml::MagicTranslations.enable(:fast_gettext) before do FastGettext.text_domain = 'coquelicot' if params && params[:lang] locale = session[:lang] = params[:lang] elsif session[:lang] locale = session[:lang] else locale = request.env['HTTP_ACCEPT_LANGUAGE'] || 'en' end FastGettext.locale = locale end end end tmpfhJ2Ca/lib/coquelicot/auth.rb0000644000175000017500000000314712574355361016103 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . module Coquelicot module Auth module Extension def authentication_method=(options) method = options.delete('name') || options.delete(:name) method = method.to_s if method.is_a? Symbol require "coquelicot/auth/#{method}" set :authenticator, Coquelicot::Auth. const_get("#{method.to_s.capitalize}Authenticator").new(self) options.each{|k,v| set k,v } end end class Error < StandardError; end class AbstractAuthenticator def initialize(app) @app = app end def settings @app end def authenticate(params) raise NotImplementedError.new('Authenticator needs to override the `authenticate` method!') end end end end tmpfhJ2Ca/lib/coquelicot/app.rb0000644000175000017500000003560612575035751015726 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'lockfile' require 'sinatra/config_file' require 'tilt/haml' require 'tilt/sass' require 'sass' require 'digest/sha1' require 'fast_gettext' require 'upr' require 'moneta' require 'unicorn/launcher' require 'rainbows' require 'optparse' require 'rubygems/package' module Coquelicot class << self def settings (class << self; Application; end) end def depot @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path @depot end def sass_cache if @sass_cache.nil? @sass_cache = File.join(settings.cache_path, 'sass-cache') FileUtils.mkdir_p(@sass_cache) end @sass_cache end # Called by the +coquelicot+ script. def run!(args = []) parser = OptionParser.new do |opts| opts.banner = "Usage: #{opts.program_name} [options] COMMAND [command options]" opts.separator "" opts.separator "Common options:" opts.on "-c", "--config FILE", "read settings from FILE" do |file| if File.readable? file settings.config_file File.expand_path(file) else $stderr.puts "#{opts.program_name}: cannot access configuration file '#{file}'." exit 1 end end opts.on("-h", "--help", "show this message") do $stderr.puts opts.to_s exit end opts.separator "" opts.separator "Available commands:" opts.separator " start Start web server" opts.separator " stop Stop web server" opts.separator " gc Run garbage collection" opts.separator " migrate-jyraphe Migrate a Jyraphe repository" opts.separator "" opts.separator "See '#{opts.program_name} COMMAND --help' for more information on a specific command." end begin parser.order!(args) do |command| if %w{start stop gc migrate-jyraphe}.include? command return self.send("#{command.gsub(/-/, '_')}!", args) else $stderr.puts("#{parser.program_name}: '#{command}' is not a valid command. " + "See '#{parser.program_name} --help'.") exit 1 end end rescue OptionParser::InvalidOption => ex $stderr.puts("#{parser.program_name}: '#{ex.args[0]}' is not a valid option. " + "See '#{parser.program_name} --help'.") exit 1 end # if we reach here, no command was given $stderr.puts parser.to_s exit end def monkeypatch_half_close # This implements the behaviour outlined in Section 8 of # . # # Half-closing the write part first and draining our input makes sure the # client will properly receive an error message instead of TCP RST (a.k.a. # "Connection reset by peer") when we interrupt it in the middle of a POST # request. # # Thanks Eric Wong for these few lines. See # for # the discussion that lead him to propose what follows. Rainbows::Client.class_eval <<-END_OF_METHOD def close close_write buf = "" loop do kgio_wait_readable(2) break unless kgio_tryread(512, buf) end ensure super end END_OF_METHOD end def start!(args) options = {} parser = OptionParser.new do |opts| opts.banner = "Usage: #{opts.program_name} [options] start [command options]" opts.separator "" opts.separator "'#{opts.program_name} start' will start the web server in background." opts.separator "Use '#{opts.program_name} stop' to stop it when done serving." opts.separator "" opts.separator "Command options:" opts.on_tail("-n", "--no-daemon", "do not daemonize (stay in foreground)") do options[:no_daemon] = true end opts.on_tail("-h", "--help", "show this message") do $stderr.puts opts.to_s exit end end parser.parse!(args) Unicorn::Configurator::DEFAULTS.merge!({ :pid => settings.pid, :listeners => settings.listen, :use => :ThreadSpawn, :rewindable_input => false, :client_max_body_size => nil }) unless options[:no_daemon] if settings.log Unicorn::Configurator::DEFAULTS.merge!({ :stdout_path => settings.log, :stderr_path => settings.log }) end end # daemonize! and start pass data around through rainbows_opts rainbows_opts = {} ::Unicorn::Launcher.daemonize!(rainbows_opts) unless options[:no_daemon] path = settings.path app = lambda do ::Rack::Builder.new do Coquelicot.monkeypatch_half_close use ::Rack::ContentLength use ::Rack::Chunked use ::Rack::CommonLogger, $stderr map path do run Application end end.to_app end server = ::Rainbows::HttpServer.new(app, rainbows_opts) server.start.join end def stop!(args) parser = OptionParser.new do |opts| opts.banner = "Usage: #{opts.program_name} [options] stop [command options]" opts.separator "" opts.separator "'#{opts.program_name} stop' will stop the web server." opts.separator "" opts.separator "Command options:" opts.on_tail("-h", "--help", "show this message") do $stderr.puts opts.to_s exit end end parser.parse!(args) unless File.readable? settings.pid $stderr.puts "Unable to read #{settings.pid}. Are you sure Coquelicot is started?" exit 1 end pid = File.read(settings.pid).to_i if pid == 0 $stderr.puts "Bad PID file #{settings.pid}." exit 1 end Process.kill(:TERM, pid) end def gc!(args) parser = OptionParser.new do |opts| opts.banner = "Usage: #{opts.program_name} [options] gc [command options]" opts.separator "" opts.separator "'#{opts.program_name} gc' will clean up expired files from the current depot." opts.separator "Depot is currently set to '#{Coquelicot.depot.path}'" opts.separator "" opts.separator "Command options:" opts.on_tail("-h", "--help", "show this message") do $stderr.puts opts.to_s exit end end parser.parse!(args) depot.gc! end def migrate_jyraphe!(args = []) require 'coquelicot/jyraphe_migrator' Coquelicot::JyrapheMigrator.run! args end end class Application < Coquelicot::BaseApp register Sinatra::ConfigFile register Coquelicot::Auth::Extension enable :sessions # When sessions are enabled, Rack::Protection (added by Sinatra) # will choke on our lack of rewind method on our input. Let's # deactivate the protections which needs to parse parameters, then. set :protection, :except => [:session_hijacking, :remote_token] set :root, Proc.new { app_file && File.expand_path('../../..', app_file) } set :depot_path, Proc.new { File.join(root, 'files') } set :cache_path, Proc.new { File.join(root, 'tmp/cache') } set :max_file_size, 5 * 1024 * 1024 # 5 MiB set :default_expire, 60 * 24 # 1 day set :maximum_expire, 60 * 24 * 30 # 1 month set :gone_period, 60 * 24 * 7 # 1 week set :filename_length, 20 set :random_pass_length, 16 set :about_text, 'en' => '' set :additional_css, '' set :pid, Proc.new { File.join(root, 'tmp/coquelicot.pid') } set :log, Proc.new { File.join(root, 'tmp/coquelicot.log') } set :listen, [ "127.0.0.1:51161" ] set :path, '/' set :show_exceptions, false set :authentication_method, :name => :simplepass, :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3' config_file File.expand_path('../../../conf/settings.yml', __FILE__) set :upr_backend, Upr::Monitor.new(Moneta.new(:Memory)) use Upr, :backend => upr_backend, :path_info => %q{/upload} use Coquelicot::Rack::Upload # limit requests other than upload to an input body of 5 kiB max use Rainbows::MaxBody, 5 * 1024 not_found do @uri = env['REQUEST_URI'] haml :not_found end error 403 do haml :forbidden end error 409 do haml :download_in_progress end error 500..510 do @error = env['sinatra.error'] || response.body.join if request.xhr? "#{response.body.join}" else haml :error end end get '/style.css' do content_type 'text/css', :charset => 'utf-8' sass :style, :cache_location => Coquelicot.sass_cache end get '/' do haml :index end get '/README' do haml(":markdown\n" + File.read(File.join(settings.root, 'README')).gsub(/^/, ' ')) end get '/about-your-data' do haml :about_your_data end if defined? Gem::Package.build get '/source' do Gem::DefaultUserInteraction.ui = Gem::SilentUI.new spec = Gem::loaded_specs['coquelicot'].clone spec.version = gem_version Tempfile.open('coquelicot-gem', :encoding => 'binary') do |gem_file| Dir.mktmpdir('coquelicot-gen-gem') do |tmpdir| Dir.chdir(spec.full_gem_path) do spec.files.each do |file| dest = "#{tmpdir}/#{file}" FileUtils.mkdir_p(File.dirname(dest)) FileUtils.cp(file, dest) end end Dir.chdir("#{tmpdir}") do filename = Gem::Package.build(spec) gem_file.write(File.read(filename)) end end send_file gem_file.path, :filename => spec.file_name gem_file.unlink end end else get '/source' do Gem::DefaultUserInteraction.ui = Gem::SilentUI.new spec = Gem::loaded_specs['coquelicot'].clone Dir.chdir(spec.full_gem_path) do spec.version = gem_version spec.mark_version spec.validate Tempfile.open('coquelicot-gem', :encoding => 'binary') do |gem_file| Gem::Package.open(gem_file, 'w', nil) do |pkg| pkg.metadata = spec.to_yaml spec.files.each do |file| next if File.directory?(file) stat = File.stat(file) mode = stat.mode & 0777 size = stat.size pkg.add_file_simple(file, mode, size) do |tar_io| tar_io.write(open(file, "rb") { |f| f.read }) end end end send_file gem_file.path, :filename => spec.file_name gem_file.unlink end end end end get '/random_pass' do "#{Coquelicot.gen_random_pass}" end get '/ready/:link' do |link| not_found if link.nil? link, pass = link.split '-' if link.include? '-' file = Coquelicot.depot.get_file(link, nil) not_found if file.nil? @expire_at = file.expire_at @name = "#{link}" unless pass.nil? @name << "-#{pass}" @unprotected = true end @url = uri(@name) haml :ready end post '/authenticate' do pass unless request.xhr? begin unless authenticate(params) error 403, "Forbidden" end 'OK' rescue Coquelicot::Auth::Error => ex error 503, ex.message rescue => ex dump_errors! ex error 500, "Issue has been logged." end end get '/progress' do response.headers.update(Upr::JSON::RESPONSE_HEADERS) data = Upr::JSON.new(:env => request.env, :backend => settings.upr_backend, :upload_id => params['X-Progress-ID'])._once halt 200, { 'Content-Type' => 'application/json' }, data end post '/upload' do # Normally handled by Coquelicot::Rack::Upload, only failures # will arrive here. error 500, 'Rack::Coquelicot::Upload failed' if @env['X_COQUELICOT_FORWARD'].nil? if params[:file].nil? then @error = "No file selected" return haml(:index) end error 500, 'Something went wrong: this code should never be executed' end def expired throw :halt, [410, haml(:expired)] end def send_stored_file(file) response['Content-Length'] = "#{file.meta['Length']}" response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream' last_modified file.created_at.httpdate attachment file.meta['Filename'] throw :halt, [200, file] end def send_link(link, pass) file = Coquelicot.depot.get_file(link, pass) return false if file.nil? return expired if file.expired? if file.one_time_only? begin # unlocking done in file.close file.lockfile.lock rescue Lockfile::TimeoutLockError error 409, "Download currently in progress" end end send_stored_file(file) end get '/:link-:pass' do |link, pass| not_found if link.nil? || pass.nil? link = Coquelicot.remap_base32_extra_characters(link) pass = Coquelicot.remap_base32_extra_characters(pass) begin not_found unless send_link(link, pass) rescue Coquelicot::BadKey not_found end end get '/:link' do |link| not_found if link.nil? link = Coquelicot.remap_base32_extra_characters(link) not_found unless Coquelicot.depot.file_exists? link @link = link haml :enter_file_key end post '/:link' do |link| pass = params[:file_key] return 403 if pass.nil? or pass.empty? begin # send Forbidden even if file is not found return 403 unless send_link(link, pass) rescue Coquelicot::BadKey => ex 403 end end end end tmpfhJ2Ca/lib/coquelicot/auth/0000755000175000017500000000000013026234541015536 5ustar lunarlunartmpfhJ2Ca/lib/coquelicot/auth/imap.rb0000644000175000017500000000315312574637630017030 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2015 potager.org # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'net/imap' module Coquelicot module Auth class ImapAuthenticator < AbstractAuthenticator def authenticate(params) imap = Net::IMAP.new(settings.imap_server, settings.imap_port, true) imap.login(params[:imap_user], params[:imap_password]) imap.logout true rescue Net::IMAP::NoResponseError false rescue Errno::ECONNREFUSED raise Coquelicot::Auth::Error.new( 'Unable to connect to IMAP server') rescue NoMethodError => ex if [:imap_server, :imap_port].include? ex.name raise Coquelicot::Auth::Error.new( "Missing '#{ex.name}' attribute in configuration.") else raise end end end end end tmpfhJ2Ca/lib/coquelicot/auth/userpass.rb0000644000175000017500000000333613026223065017734 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2016 potager.org # © 2016 Rowan Thorpe # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'bcrypt' module Coquelicot module Auth class UserpassAuthenticator < AbstractAuthenticator EMPTY_PASSWORD = BCrypt::Password.create('') def authenticate(params) upload_user = params[:upload_user] || '' upload_password = params[:upload_password] || '' return false if upload_user.empty? || upload_password.empty? # Use the empty password—we just disallowed it—for unknown users # in order to get constant time. reference_password = settings.credentials.fetch(upload_user, EMPTY_PASSWORD) return BCrypt::Password.new(reference_password) == upload_password rescue NoMethodError => ex if :credentials == ex.name raise Coquelicot::Auth::Error.new("Missing 'credentials' attribute in 'userpass' configuration.") else raise end end end end end tmpfhJ2Ca/lib/coquelicot/auth/ldap.rb0000644000175000017500000000506212574637636017031 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2015 potager.org # © 2014 Rowan Thorpe # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # TODO: set array of multiple ldap servers in settings and loop over them to # find first matching UID to connect as # TODO: add commented code showing how to direct login by full username, # without lookup # TODO: add commented code showing how to use starttls as an option instead of # dedicated SSL port, too # NB: :simple_tls ensures all communication is encrypted, but it doesn't # verify the server-certificate. A method which does *both* doesn't # seem to exist in Net::LDAP yet... require 'net/ldap' module Coquelicot module Auth class LdapAuthenticator < AbstractAuthenticator def authenticate(params) return false if params[:ldap_user].empty? || params[:ldap_password].empty? # connect anonymously & lookup user to do authenticated bind_as() next ldap = Net::LDAP.new(:host => settings.ldap_server, :port => settings.ldap_port, :base => settings.ldap_base, :encryption => :simple_tls, :auth => { :method => :anonymous }) ldap.bind_as(:base => settings.ldap_base, :filter => "(uid=#{Net::LDAP::Filter.escape(params[:ldap_user])})", :password => params[:ldap_password]) rescue Errno::ECONNREFUSED raise Coquelicot::Auth::Error.new( 'Unable to connect to LDAP server') rescue NoMethodError => ex if [:ldap_server, :ldap_port, :ldap_base].include? ex.name raise Coquelicot::Auth::Error.new( "Missing '#{ex.name}' attribute in configuration.") else raise end end end end end tmpfhJ2Ca/lib/coquelicot/auth/simplepass.rb0000644000175000017500000000232512574576322020262 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . module Coquelicot module Auth class SimplepassAuthenticator < AbstractAuthenticator def authenticate(params) return TRUE if settings.upload_password.nil? upload_password = params[:upload_password] (not upload_password.nil?) && Digest::SHA1.hexdigest(upload_password) == settings.upload_password end end end end tmpfhJ2Ca/lib/coquelicot/depot.rb0000644000175000017500000001215412574355361016253 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'lockfile' require 'openssl' module Coquelicot class Depot attr_reader :path def initialize(path) @path = path end def add_file(pass, options, &block) dst = nil # Ensure that the generated name is not already used loop do dst = gen_random_file_name begin StoredFile.create(full_path(dst), pass, options, &block) break rescue Errno::EEXIST => e raise unless e.message =~ /(?:^|\s)#{Regexp.escape(full_path(dst))}(?:\s|$)/ next # let's try again end end # retry to add the link until a free name is generated loop do link = gen_random_file_name return link if add_link(link, dst) end end def get_file(link, pass=nil) name = nil lockfile.lock do name = read_link(link) end return nil if name.nil? begin StoredFile::open(full_path(name), pass) rescue Errno::ENOENT nil end end def file_exists?(link) lockfile.lock do name = read_link(link) return name && File.exists?(full_path(name)) end end def gc! files.each do |name| path = full_path(name) unless File.exists?(path) remove_from_links { |l| l.strip.end_with? " #{name}" } next end if File.lstat(path).size > 0 begin file = StoredFile::open path rescue ArgumentError $stderr.puts "W: #{path} is not a Coquelicot file. Skipping." next end file.empty! if file.expired? elsif Time.now - File.lstat(path).mtime > (Coquelicot.settings.gone_period * 60) remove_from_links { |l| l.strip.end_with? " #{name}" } FileUtils.rm "#{path}.content", :force => true FileUtils.rm path end end end def size files.count end private LOCKFILE_OPTIONS = { :timeout => 60, :max_age => 8, :refresh => 2, :debug => false } def lockfile Lockfile.new "#{@path}/.lock", LOCKFILE_OPTIONS end def links_path "#{@path}/.links" end def add_link(src, dst) lockfile.lock do return false unless read_link(src).nil? File.open(links_path, 'a') do |f| f.write("#{src} #{dst}\n") end end true end def remove_from_links(&block) lockfile.lock do links = [] File.open(links_path, 'r+') do |f| f.readlines.each do |l| links << l unless yield l end f.rewind f.truncate(0) f.write links.join end end end def remove_link(src) remove_from_links { |l| l.start_with? "#{src} " } end def read_link(src) File.open(links_path) do |f| until f.eof? return $1 if f.readline =~ /^#{Regexp.escape(src)}\s+(.+)$/ end end nil rescue Errno::ENOENT nil end def files lockfile.lock do begin File.open(links_path) do |f| f.readlines.collect { |l| l.split[1] } end rescue Errno::ENOENT # if links file has not been created yet [] end end end def gen_random_file_name begin name = Coquelicot.gen_random_base32(Coquelicot.settings.filename_length) end while File.exists?(full_path(name)) name end def full_path(name) raise "Wrong name" unless name.each_char.collect { |c| Coquelicot::FILENAME_CHARS.include? c }.all? "#{@path}/#{name}" end end # Like RFC 4648 (Base32) FILENAME_CHARS = %w(a b c d e f g h i j k l m n o p q r s t u v w x y z 2 3 4 5 6 7) class << self def gen_random_base32(length) name = '' OpenSSL::Random::random_bytes(length).each_byte do |i| name << FILENAME_CHARS[i % FILENAME_CHARS.length] end name end def gen_random_pass gen_random_base32(settings.random_pass_length) end def remap_base32_extra_characters(str) map = {} FILENAME_CHARS.each { |c| map[c] = c; map[c.upcase] = c } map.merge!({ '1' => 'l', '0' => 'o' }) result = '' str.each_char { |c| result << map[c] if map[c] } result end end end tmpfhJ2Ca/lib/coquelicot/version.rb0000644000175000017500000000163013026225500016602 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . module Coquelicot VERSION = '0.9.6' end tmpfhJ2Ca/lib/coquelicot/num.rb0000644000175000017500000000353013026223065015721 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'fast_gettext' module Coquelicot::Num include FastGettext::Translation # found on: http://codereview.stackexchange.com/questions/9107/ def as_size prefix = [# TRANSLATORS: Abbreviated unit of storage. See https://en.wiktionary.org/wiki/tebibyte N_('TiB'), # TRANSLATORS: Abbreviated unit of storage. See https://en.wiktionary.org/wiki/gibibyte N_('GiB'), # TRANSLATORS: Abbreviated unit of storage. See https://en.wiktionary.org/wiki/mebibyte N_('MiB'), # TRANSLATORS: Abbreviated unit of storage. See https://en.wiktionary.org/wiki/kibibyte N_('KiB'), # TRANSLATORS: Abbreviated unit of storage. See https://en.wiktionary.org/wiki/byte N_('B')] s = self.to_f i = prefix.length - 1 while s > 512 && i > 0 s /= 1024 i -= 1 end ((s > 9 || s.modulo(1) < 0.1 ? '%d' : '%.1f') % s) + ' ' + _(prefix[i]) end end Fixnum.send(:include, Coquelicot::Num) Bignum.send(:include, Coquelicot::Num) tmpfhJ2Ca/lib/coquelicot/helpers.rb0000644000175000017500000000546712574355361016613 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'rubygems' module Coquelicot module Helpers def can_provide_git_repository? return @@can_provide_git_repository if defined?(@@can_provide_git_repository) # Test if `git update-server-info` was executed in the local repository @@can_provide_git_repository = File.readable?(File.expand_path('coquelicot.git/info/refs', settings.public_folder)) && File.readable?(File.expand_path('coquelicot.git/objects/info/packs', settings.public_folder)) if File.readable?(File.expand_path('coquelicot.git', settings.public_folder)) && !@@can_provide_git_repository logger.warn <<-MESSAGE.gsub(/\n */m, ' ').strip Unable to provide access to local Git repository. Please ensure that you have run `git update-server-info` in the Coquelicot directory, and that the symlink `public/coquelicot.git` is properly set. MESSAGE end @@can_provide_git_repository end def gem_hostname # We need to mangle the hostname to fits Gem::Version constraints @@hostname ||= Socket.gethostname.gsub(/[^0-9a-zA-Z]/, '') end def gem_version spec = Gem::loaded_specs['coquelicot'] current_version = spec ? spec.version.to_s.gsub(/\.[0-9a-zA-Z]+\.[0-9]{8}/, '') : Coquelicot::VERSION Gem::Version.new("#{current_version}.#{gem_hostname}.#{Date.today.strftime('%Y%m%d')}") end def clone_command if can_provide_git_repository? "git clone #{uri('coquelicot.git')}" else "curl -OJ #{uri('source')} && gem unpack coquelicot-#{gem_version}.gem" end end def authenticate(params) Coquelicot.settings.authenticator.authenticate(params) end def auth_method Coquelicot.settings.authenticator.class.name.gsub(/Coquelicot::Auth::([A-z0-9]+)Authenticator$/, '\1').downcase end def about_text settings.about_text[FastGettext.locale] || settings.about_text['en'] || '' end end end tmpfhJ2Ca/bin/0000755000175000017500000000000013026234541012430 5ustar lunarlunartmpfhJ2Ca/bin/coquelicot0000755000175000017500000000205412574355361014541 0ustar lunarlunar#!/usr/bin/env ruby # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2012-2013 potager.org # © 2011 mh / immerda.ch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . begin require 'rubygems' require 'bundler' Bundler.setup rescue LoadError # Maybe all dependencies were handled by some other mechanisms end require 'coquelicot' Coquelicot.run!(ARGV) tmpfhJ2Ca/files/0000700000175000017500000000000013026225577012761 5ustar lunarlunartmpfhJ2Ca/HACKING0000644000175000017500000002003713026225500012644 0ustar lunarlunarDevelopment notes ================= [Coquelicot] is written in Ruby and should be quite easy to improve for anyone a little bit familiar with the Sinatra web framework. It is mostly written using Behaviour Driven Development, making the test suite a fine net to hack in confidence. So please go ahead! [Coquelicot]: https://coquelicot.potager.org/ Setup a work environment ------------------------ As Coquelicot uses Bundle, the first step to work on Coquelicot after cloning its Git repository is installing the proper dependencies by issuing: bundle install Basic operations ---------------- Coquelicot test suite is written using RSpec. Running the test suite is just a matter of typing: bundle exec rspec Running a test server can be done with: bundle exec coquelicot start --no-daemon To update the translation source files, use: bundle exec rake gettext:po:update This will update `po/coquelicot.pot` and merge the new strings in the various `po/*/coquelicot.po` files. Authentication mechanisms ------------------------- The authentication part of Coquelicot has been made modular. Adding a new authentication mechanism should be fairly straightforward. A new authentication mechanism needs to provide the following 3 files, with the following responsabilities: * `lib/coquelicot/auth/.rb`: A class implementing the actual authentication. This class must implement an `authenticate` method. It will receive the form fields as usual (params). This method must return true if upload should be allowed. * `public/javascripts/coquelicot.auth..js:` This file must define 'authentication' as an object with the following methods: - `getData()`: return an object of all the necessary data to authenticate on the app side. Keys must have the same name as the input fields used to authenticate without JavaScript. - `focus()`: set the focus on the first authentication form field. - (optional) `handleSuccess()`: arbitrary action upon successful authentication. This is called after the livebox with authentication fields is closed. - (optional) `handleReject()`: arbitrary action when access is denied. One can reset authentication fields after a failed authentication. - (optional) `handleFailure()`: arbitrary action when there was a problem in the authentication procedure. * `views/auth/.haml`: A template with the necessary form fields that will be used for authentication. The authentication mechanism is set in the configuration file and can include options specific to the method chosen. Implementation details ---------------------- Common application code lies in `Coquelicot::Application`, except for one specific (and important) type of requests, namely `POST /update`. These requests are handled directly at bare Rack level by `Coquelicot::Rack::Upload`. This allows to work directly with POST data as the browser is sending it, so we can directly stream the uploaded file to our encrypted on-disk containers. The POST data must be in a very specific order, as we need to handle authentication and other option fields before we start recording the file content. Thanks to the W3C, the [HTML specification] states that parts of the POST data must be delivered in the same order as the controls appear in the `

` container. `Coquelicot::Rack::Multipart` exposes a simple DSL to parse the fields as they are delivered. The later is used by `Coquelicot::Rack::Upload` to perform its logic pretty nicely. [HTML specification]: http://www.w3.org/TR/html4/interact/forms.html Watch for buffered inputs! -------------------------- Coquelicot is written in Ruby using Sinatra. Sinatra is based on the Rack webserver interface. Rack specification mandates that applications must be able to seek and rewind freely in the request content. Request data is always received as a stream through the network. So in order to comply with the specification, webservers implementing Rack either buffer the input in memory (Webrick) or in a temporary file (Thin, Passenger or Mongrel). On top of that, when parsing `multipart/form-data` POST content, `Rack::Request` (used by Sinatra) creates a new temporary file for each files in the POST request. For the specific needs of Coquelicot, these behaviours prevent users from uploading large files (if `/tmp` is in memory) or breach their privacy by writing a clear text version to disk. To overcome these limitations, Coquelicot first uses a specific feature of the Rainbows! webserver of streaming its input directly to applications, and second bypasses `Rack::Request` to directly handle POST content. Usage of any other Rack webserver is strongly discouraged and should be restricted to development and testing. Storage details --------------- Files are stored in the directory specified by the 'depot_path' setting. One file in Coquelicot is actually stored in two files: one for metadata and one for the file content. ### Metadata file The format is the following: --- Coquelicot: "2.0" Salt: <8 bytes stored as Base64> Expire-at: --- Encryption is done using OpenSSL. Cipher is AES-256-CBC with key and IV created using the `pbkdf2_hmac_sha1()` implementation of PKCS5. The later is fed using the former *Salt* and the given passphrase, using 2000 iterations. Once decrypted, the metadata have the following format: --- Created-at: Filename: "" Content-Type: "" Length: One-time-only: Headers must be valid YAML. ### Content file The content file contains the stored file in encrypted form. Encryption is done with the same algorithm and keys as the encrypted metadata (see above). The file name of the content file is the same as the one for metada, with an added suffix of '.content'. For example, if the metadata file name is `mqeb4pfcru2ymq3e6se7`, the associated content file will be `mqeb4pfcru2ymq3e6se7.content`. ### Expired files Both the content file and the metadata file are truncated to zero length when they are "expired". ### URL mapping In order to map download URLs to file name, a simple text file ".links" is used. It contains a line for each file in the form: ### Changes history version 2.0 : Current version described above. version 1.0 : File content is in the same file as the metadata. Content is put in the after the metadata and an extra "--- \n". Sending patches --------------- Please send patches to the users and developers [mailing list]. They are best prepared using `git format-patch`. [mailing list]: https://listes.potager.org/listinfo/coquelicot How to make a new release? -------------------------- 1. Bump version number in `lib/coquelicot/version.rb` and `Gemfile.lock`. Don't forget to commit the changes. 2. Add a new entry in the NEWS file. For an outline: git log --reverse --oneline $(git describe --abbrev=0).. Don't forget to commit the changes. 3. Tag the release: git tag -s coquelicot-$VERSION -m "coquelicot $VERSION" 4. Push changes to the main repository: git push origin master coquelicot-$VERSION 5. Create a source tarball: bundle exec rake create_archive 6. Sign it: gpg --armor --detach-sign coquelicot-$VERSION.tar.gz 7. Switch to the website: cd ../website 8. Move the source tarball and signature to the website: mv ../git/coquelicot-$VERSION.tar.gz* static/dist/ 9. Add them to the website repository: git add static/dist/coquelicot-$VERSION.tar.gz* 10. Update the version on the website homepage: sed -e "s/coquelicot-$PREVIOUS_VERSION/coquelicot-$VERSION/g" \ -i dynamic/index.md 11. Commit changes to the website. 12. Push the updated website: make push git push origin master 13. Announce the release on `coquelicot@potager.org` mailing-list. 14. Announce the release on `freecode.com`. tmpfhJ2Ca/Rakefile0000644000175000017500000000607413026223065013333 0ustar lunarlunar# Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2010-2013 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'rubygems' require 'bundler' Bundler.require(:default, :development) Bundler.setup require 'bundler/gem_tasks' require 'gettext/tools/task' require 'haml/magic_translations/xgettext/haml_parser' require 'cucumber/rake/task' require 'rspec/core/rake_task' GetText::Tools::XGetText.add_parser(Haml::MagicTranslations::XGetText::HamlParser) GetText::Tools::Task.define do |task| task.spec = Gem::Specification.load('coquelicot.gemspec') task.files = Dir.glob('views/**/*.{rb,haml}') + Dir.glob('lib/coquelicot/**/*.rb') task.xgettext_options = ['--msgid-bugs-address=Coquelicot developers ', '--add-comments=TRANSLATORS'] end task :create_archive do spec = Gem::Specification.load('coquelicot.gemspec') filename = "coquelicot-#{spec.version}.tar.gz" File.open(filename, 'wb') do |archive| Zlib::GzipWriter.wrap(archive) do |gzipped| Gem::Package::TarWriter.new(gzipped) do |writer| spec.files.each do |file| next if File.directory? file stat = File.stat(file) mode = stat.mode & 0777 size = stat.size mtime = stat.mtime name, prefix = writer.split_name(file) header = Gem::Package::TarHeader.new(:name => name, :mode => mode, :size => size, :prefix => prefix, :mtime => mtime).to_s gzipped.write header gzipped.write(open(file, 'rb') { |f| f.read }) remainder = (512 - (size % 512)) % 512 gzipped.write("\0" * remainder) end # Add empty directories where there is place holders Dir.glob('**/.placeholder') do |placeholder| dir = File.dirname(placeholder) name, prefix = writer.split_name(dir) mtime = File.stat(dir).mtime header = Gem::Package::TarHeader.new :name => name, :mode => 0700, :typeflag => "5", :size => 0, :prefix => prefix, :mtime => mtime gzipped.write header end end end end end Cucumber::Rake::Task.new(:features) do |t| t.cucumber_opts = "features --format pretty" end RSpec::Core::RakeTask.new(:spec) task :test => [:spec, :features] tmpfhJ2Ca/NEWS0000644000175000017500000001120713026225500012353 0ustar lunarlunarRelease history =============== Here is a list of changes that happened in each release of [Coquelicot]: Version 0.9.6 ------------- * Add `userpass` authentication contributed by Rowan Thrope. It stores multiple login/password credentials in a configuration file. Password are stored encrypted using bcrypt. * Properly translate storage durations in upload form. Fix by Rowan Thrope. * Use proper unit when reporting byte count during upload. * Update and clean up dependencies. * Minor improvements: - Fix views that made the latest Haml parser unhappy. * Translation improvements: - Refresh translation template and catalogs - Add bug report address to translation template - Fix a syntax error in Spanish PO file. - Add comments for translators regarding unit of storage abbreviations. - Add Greek translations. Thanks to Rowan Thrope. * Update authors in README * Mention users and developers mailing list in documentation Released 2016-12-20. Version 0.9.5 ------------- * Fix preselection of the expiration time set in the configuration. * Remove usage of the deprecated $.browser in jQuery plugins. This makes them compatible with jQuery 1.9+. * Upgrade bundled jQuery to version 1.11.3. * Add missing `require` for some Cucumber features. Released 2015-09-22. Version 0.9.4 ------------- * Make the directory for cache files configurable. This adds a new `cache_path` setting. Thanks to Rowan Thorpe for the original patch. * Make the default selection for the expiration time match the default setting. * Switch the default expiration time to one day. This should be less surprising to users uploading huge files. Thanks drkvg for prodding me long enough to do this. * Stop IMAP and LDAP authenticators to error out when authentication fails. They now properly just deny the authentication requests as they should have. * Make sure that we read and write binary files as such. This should improve compatibilities with certain Ruby installations. * Do proper integration testing using Cucumber. * Update and clean up dependencies. * Other minor improvements: - Upgrade to RSpec 3 and fix the remaining deprecation warnings. - Specify the license in the gemspec. - Explicitly require on tilt/haml and tilt/sass to avoid race conditions. Released 2015-09-12. Version 0.9.3 ------------- * Support sub-directory installations. See updated installation documentation for Apache and the new `path` setting. * Always use the current source tree as the `coquelicot` gem. * Fix an issue with the signature step in the release process. * Document commands needed to serve the local Git clone. * Fix a typo in footer when Coquelicot was installed from a gem. * Add missing Debian packages to installation steps. Thanks Alexandre Garreau for reporting the issue. * Add Spanish translation. Thanks Loïc Raimbault! * Stop spilling authentication errors to users. Thanks Rowan Thorpe for the report. * Add LDAP authentication (with uid lookup). Thanks Rowan Thorpe! * Code cleanups: - Drop support for Ruby 1.8. - Stop using unsupported gem name for `activesupport`. - Set a default time zone when running tests. - Ensure same timezone when testing file creation time. - Add support for generating gems using the newer Gem API. - Add support for the Psych YAML engine. - Switch to new RSpec expectation syntax. - Switch to GetText::Tools:Task in Rakefile. - Update bundle dependencies. Released 2013-05-07. Version 0.9.2 ------------- * Minor code cleanups: - Cleanup old stub launcher for `Coquelicot::Application`. - Fallback on version available in source code when the gem version is unavailable. - Add missing require for `Coquelicot::Helpers`. - Ensure gem files have been unlinked after they have been sent. Files created with Tempfile should be unlinked by Ruby runtime, but let's just do it when most appropriate. * Source tarball cleanup: - Stop shipping jquery.lightBoxFu.js with the executable bit set. - Ship proper "mtimes" instead of setting every dates to 1970-01-01. * Mention author and license for JavaScript libraries in README. * Document the release process. * Rework and split documentation in different files targeting different audiences. Released 2013-04-08. Version 0.9.1 ------------- * Add missing XML namespace in default layout. * Be more specific when catching loading failures. * Fix an embarassing typo which prevented Coquelicot to load with Ruby >= 1.9. * Fix upload progress tracking. Released 2013-03-21. Version 0.9 ----------- * Initial release. Released 2013-03-13. [Coquelicot]: https://coquelicot.potager.org/ tmpfhJ2Ca/tmp/0000700000175000017500000000000013026225614012447 5ustar lunarlunartmpfhJ2Ca/Gemfile.lock0000644000175000017500000000606713026225500014106 0ustar lunarlunarPATH remote: . specs: coquelicot (0.9.6) fast_gettext haml (~> 4) haml-magic-translations json lockfile (~> 2) maruku moneta (>= 0.7, < 2) multipart-parser rack (>= 1.1, < 2) rainbows sass sinatra (~> 1.4) sinatra-contrib (~> 1.4) upr GEM remote: https://rubygems.org/ specs: activesupport (5.0.0.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) backports (3.6.8) bcrypt (3.1.11) builder (3.2.2) capybara (2.11.0) addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) concurrent-ruby (1.0.3) cucumber (2.4.0) builder (>= 2.1.2) cucumber-core (~> 1.5.0) cucumber-wire (~> 0.0.1) diff-lcs (>= 1.1.3) gherkin (~> 4.0) multi_json (>= 1.7.5, < 2.0) multi_test (>= 0.1.2) cucumber-core (1.5.0) gherkin (~> 4.0) cucumber-wire (0.0.1) diff-lcs (1.2.5) fast_gettext (1.3.0) gettext (3.2.2) locale (>= 2.0.5) text (>= 1.3.0) gherkin (4.0.0) haml (4.0.7) tilt haml-magic-translations (4.2.0) haml (~> 4.0) i18n (0.7.0) json (2.0.2) kgio (2.11.0) locale (2.1.2) lockfile (2.1.3) maruku (0.7.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_portile2 (2.1.0) minitest (5.10.1) moneta (0.8.1) multi_json (1.12.1) multi_test (0.1.2) multipart-parser (0.1.1) net-ldap (0.15.0) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) public_suffix (2.0.4) rack (1.6.5) rack-protection (1.5.3) rack rack-test (0.6.3) rack (>= 1.0) rainbows (5.0.0) kgio (~> 2.5) rack (~> 1.1) unicorn (~> 5.0) raindrops (0.17.0) rake (12.0.0) rspec (3.5.0) rspec-core (~> 3.5.0) rspec-expectations (~> 3.5.0) rspec-mocks (~> 3.5.0) rspec-core (3.5.4) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) rspec-support (3.5.0) sass (3.4.22) sinatra (1.4.7) rack (~> 1.5) rack-protection (~> 1.4) tilt (>= 1.3, < 3) sinatra-contrib (1.4.7) backports (>= 2.0) multi_json rack-protection rack-test sinatra (~> 1.4.0) tilt (>= 1.3, < 3) text (1.3.1) thread_safe (0.3.5) tilt (2.0.5) timecop (0.8.1) tzinfo (1.2.2) thread_safe (~> 0.1) unicorn (5.2.0) kgio (~> 2.6) raindrops (~> 0.7) upr (0.3.0) moneta (~> 0.7) rack xpath (2.0.0) nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES activesupport bcrypt capybara coquelicot! cucumber gettext (~> 3) net-ldap rack-test rake rspec (~> 3) timecop tzinfo BUNDLED WITH 1.12.5 tmpfhJ2Ca/features/0000755000175000017500000000000013026234541013476 5ustar lunarlunartmpfhJ2Ca/features/auth/0000755000175000017500000000000013026234541014437 5ustar lunarlunartmpfhJ2Ca/features/auth/imap.feature0000644000175000017500000000144412574637655016771 0ustar lunarlunarFeature: Uploads can be limited to people owning an account on an IMAP server Background: Given the admin has configured the "imap" authentication method And the IMAP server knows "user@example.org" identified with "mailpass" Scenario: Uploads are denied without a login When I try to upload a file without a login Then I'm denied the upload Scenario: Uploads are denied with a wrong login Given I have entered "unknown@example.org" as IMAP login And I have entered "badpass" as IMAP password When I try to upload a file Then I'm denied the upload Scenario: Uploads are accepted with the right password Given I have entered "user@example.org" as IMAP login And I have entered "mailpass" as IMAP password When I try to upload a file Then the upload is accepted tmpfhJ2Ca/features/auth/userpass.feature0000644000175000017500000000141613026223065017662 0ustar lunarlunarFeature: Uploads can be limited to accounts registred in a configuration file Background: Given the admin has configured the "userpass" authentication method And the config file describes an account "user" identified with "secret" Scenario: Uploads are denied without a login When I try to upload a file without a login Then I'm denied the upload Scenario: Uploads are denied with a wrong login Given I have entered "unknown" as user login And I have entered "secret" as user password When I try to upload a file Then I'm denied the upload Scenario: Uploads are accepted with the right password Given I have entered "user" as user login And I have entered "secret" as user password When I try to upload a file Then the upload is accepted tmpfhJ2Ca/features/auth/ldap.feature0000644000175000017500000000137712574637655016770 0ustar lunarlunarFeature: Uploads can be limited to people owning an account on a LDAP server Background: Given the admin has configured the "ldap" authentication method And the LDAP server knows "user" identified with "ldappass" Scenario: Uploads are denied without a login When I try to upload a file without a login Then I'm denied the upload Scenario: Uploads are denied with a wrong login Given I have entered "unknown" as LDAP login And I have entered "badpass" as LDAP password When I try to upload a file Then I'm denied the upload Scenario: Uploads are accepted with the right password Given I have entered "user" as LDAP login And I have entered "ldappass" as LDAP password When I try to upload a file Then the upload is accepted tmpfhJ2Ca/features/auth/simplepass.feature0000644000175000017500000000126612574637655020225 0ustar lunarlunarFeature: Uploads can be limited to people having a shared password Background: Given the admin has configured the "simplepass" authentication method And the upload password is set to "uploadsecret" Scenario: Uploads are denied without a password When I try to upload a file without an upload password Then I'm denied the upload Scenario: Uploads are denied with a wrong password Given I have entered "wrong" as the upload password When I try to upload a file Then I'm denied the upload Scenario: Uploads are accepted with the right password Given I have entered "uploadsecret" as the upload password When I try to upload a file Then the upload is accepted tmpfhJ2Ca/features/links.feature0000644000175000017500000000044212574637655016217 0ustar lunarlunarFeature: Links to download files Scenario: Uploaders get an URL to give to downloaders When I upload a file Then I see an URL to give to downloaders Scenario: The original filename is kept secret When I upload a file Then the download URL does not contain the original filename tmpfhJ2Ca/features/expiration.feature0000644000175000017500000000173512574637655017267 0ustar lunarlunarFeature: Uploaders can choose how long files will stay on the server Scenario: Uploaders can select different limits When I visit the upload page Then I see a field to select how long the file will stay on the server Scenario: Download is possible before the time limit Given a file has been uploaded When I follow the download link Then I have downloaded the file Scenario: Download is impossible after the time limit Given a file has been uploaded that will expire the next day When I follow the download link two days later Then I'm told the file is gone Scenario: No special errors visible a while after the limit has been reached Given a file has been uploaded that will expire the next day When I follow the download link a month later Then I'm told the file does not exist Scenario: Expired files are cleaned up Given a file has been uploaded that will expire the next day When two days have past Then the file has been removed from the server tmpfhJ2Ca/features/upload_restrictions.feature0000644000175000017500000000061712574637655021177 0ustar lunarlunarFeature: Some restrictions exist on which files can be uploaded Scenario: Empty files are refused Given I have an empty file When I try to upload it Then the upload is refused as empty Scenario: Files bigger than the limit are refused Given the admin has set a maximum file size And I have a file bigger than the limit When I try to upload it Then the upload is refused as too big tmpfhJ2Ca/features/download.feature0000644000175000017500000000230212574640364016672 0ustar lunarlunarFeature: Uploaded files can then be downloaded Scenario: Original filename is retained Given a file named "my-super-music.mp3" has been uploaded When I download the file Then the downloaded file is named "my-super-music.mp3" Scenario: Original content is retained Given a file has been uploaded When I download the file Then the downloaded file has the same content as the uploaded file Scenario: Original size is retained Given a file has been uploaded When I download the file Then the downloaded file has the same size as the uploaded file Scenario: Upload time is sent in Last-Modified header Given a file has been uploaded When I download the file Then the Last-Modified header is set to the upload time Scenario: URLs are friendly to mixing up look alike letters Given a file has been uploaded When I enter the link mixing up 'l' and '1' Then I should get the original file Scenario: Access to an non-existing file When I try to access a non-existing file Then I should get a 404 error Scenario: Access to an existing file with a bad decryption key Given a file has been uploaded When I enter the link with a bad decryption key Then I should get a 404 error tmpfhJ2Ca/features/step_definitions/0000755000175000017500000000000013026234541017044 5ustar lunarlunartmpfhJ2Ca/features/step_definitions/storage.rb0000644000175000017500000000363512574640035021052 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . Then(/^the file is stored encrypted on the server$/) do original_excerpt = File.open(@uploaded_file, :encoding => 'binary') { |f| f.read(32) } files_in_depot = Dir.glob("#{@depot_path}/*") expect(files_in_depot.size).to be > 1 files_in_depot.each do |path| content = File.open(path, :encoding => 'binary').read expect(content).to_not include(original_excerpt) end end Then(/^the file name on the server is different from the name in the URL$/) do url = find('textarea.ready').value name_in_url = File.basename(url).split('-')[0] expect(name_in_url).to match(/[0-9a-z]/) # sanity check for the following test files_in_depot = Dir.glob("#{@depot_path}/*") expect(files_in_depot.size).to be > 1 files_in_depot.each do |path| expect(path).to_not include(name_in_url) end end When(/^two days have past$/) do Timecop.travel(Date.today + 2) do Coquelicot.run!(%w{gc}) end end Then(/^the file has been removed from the server$/) do files_in_depot = Dir.glob("#{@depot_path}/*") expect(files_in_depot.size).to eql(2) files_in_depot.each do |path| expect(File.size(path)).to eql(0) end end tmpfhJ2Ca/features/step_definitions/auth/0000755000175000017500000000000013026234541020005 5ustar lunarlunartmpfhJ2Ca/features/step_definitions/auth/imap.rb0000644000175000017500000000332412574640013021264 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'net/imap' Given(/^the IMAP server knows "([^"]*)" identified with "([^"]*)"$/) do |user, password| allow(Coquelicot.settings).to receive(:imap_server).and_return('example.org') allow(Coquelicot.settings).to receive(:imap_port).and_return(993) imap = double('Net::Imap').as_null_object allow(imap).to receive(:login) do |u, p| raise Net::IMAP::NoResponseError.new(Net::IMAP::TaggedResponse.new(nil, nil, Net::IMAP::ResponseText.new(nil, :text => 'Login failed.'))) unless u == user && p == password end allow(Net::IMAP).to receive(:new).and_return(imap) end When(/^I try to upload a file without a login$/) do visit '/' attach_file 'file', __FILE__ click_button 'Share!' end Given(/^I have entered "([^"]*)" as IMAP login$/) do |login| visit '/' fill_in :imap_user, :with => login end Given(/^I have entered "([^"]*)" as IMAP password$/) do |password| fill_in :imap_password, :with => password end tmpfhJ2Ca/features/step_definitions/auth/userpass.rb0000644000175000017500000000247113026223065022202 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2016 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require 'bcrypt' Given(/^the config file describes an account "([^"]*)" identified with "([^"]*)"$/) do |user, password| @credentials = {} if @credentials.nil? allow(Coquelicot.settings).to receive_messages( :credentials => { user => BCrypt::Password.create(password).to_s }) end Given(/^I have entered "([^"]*)" as user login$/) do |login| visit '/' fill_in :upload_user, :with => login end Given(/^I have entered "([^"]*)" as user password$/) do |password| fill_in :upload_password, :with => password end tmpfhJ2Ca/features/step_definitions/auth/ldap.rb0000644000175000017500000000302112574640020021246 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . Given(/^the LDAP server knows "([^"]*)" identified with "([^"]*)"$/) do |user, password| allow(Coquelicot.settings).to receive_messages( :ldap_server => 'example.org', :ldap_port => 389, :ldap_base => 'dc=example,dc=com') ldap = double('Net::LDAP').as_null_object allow(ldap).to receive(:bind_as) do |options| double('Net::LDAP::PDU') if options[:filter] == "(uid=#{Net::LDAP::Filter.escape(user)})" && options[:password] == password end allow(Net::LDAP).to receive(:new).and_return(ldap) end Given(/^I have entered "([^"]*)" as LDAP login$/) do |login| visit '/' fill_in :ldap_user, :with => login end Given(/^I have entered "([^"]*)" as LDAP password$/) do |password| fill_in :ldap_password, :with => password end tmpfhJ2Ca/features/step_definitions/auth/simplepass.rb0000644000175000017500000000236412574640022022521 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . Given(/^the upload password is set to "([^"]*)"$/) do |upload_password| allow(Coquelicot.settings).to receive(:upload_password).and_return(Digest::SHA1.hexdigest(upload_password)) end When(/^I try to upload a file without an upload password$/) do visit '/' attach_file 'file', __FILE__ click_button 'Share!' end Given(/^I have entered "([^"]*)" as the upload password$/) do |password| visit '/' fill_in 'upload_password', :with => password end tmpfhJ2Ca/features/step_definitions/settings.rb0000644000175000017500000000210512574640032021232 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . Given(/^the admin has set a maximum file size$/) do allow(Coquelicot.settings).to receive(:max_file_size).and_return(100) end Given(/^the admin has configured the "([^"]*)" authentication method$/) do |method| Capybara.app.set :authentication_method, :name => method.to_sym end tmpfhJ2Ca/features/step_definitions/web.rb0000644000175000017500000000751112574640045020161 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . When(/^I visit the (?:main|upload) page$/) do visit '/' end Then(/^I should see a link to the French version$/) do expect(page).to have_link('fr') end Given(/^I am on the main page$/) do visit '/' end When(/^I follow the link to the French version$/) do click_link 'fr' end Then(/^the page should be in French$/) do expect(find_button('submit').value).to eql('Partager !') end Then(/^I see an URL to give to downloaders$/) do expect(find('textarea.ready').value).to start_with('http') end When(/^I download the file$/) do expect(@download_url).to be_truthy visit @download_url end When(/^I follow the download link$/) do visit(@download_url || find('textarea.ready').value) end When(/^I enter the link mixing up '(.)' and '(.)'$/) do |from, to| new_url = @download_url.gsub(from, to) visit new_url end When(/^I try to access a non\-existing file$/) do visit '/dhut7f73u2hiwwifwyrs-gs5wj3ixjheg6dg7' end Then(/^I should get a (\d+) error$/) do |code| expect(page.driver.response.status).to eql(code.to_i) end When(/^I enter the link with a bad decryption key$/) do name, key = File.basename(@download_url).split('-') visit "/#{name}-#{key.reverse}" end Then(/^I see a field to select how long the file will stay on the server$/) do expect(page).to have_field('expire') end When(/^I follow the download link two days later$/) do download_url = find('textarea.ready').value Timecop.travel(Date.today + 2) do Coquelicot.run!(%w{gc}) visit download_url end end When(/^I follow the download link a month later$/) do download_url = find('textarea.ready').value Timecop.travel(Date.today + 31) do Coquelicot.run!(%w{gc}) Coquelicot.run!(%w{gc}) # think "a couple of days" visit download_url end end Then(/^I'm told the file is gone$/) do expect(page.driver.response.status).to eql(410) expect(page).to have_content('expired') end Then(/^I'm told the file does not exist$/) do expect(page.driver.response.status).to eql(404) expect(page).to have_content('not found') end Then(/^the download URL does not contain the decryption key$/) do expect(find('textarea.ready').value).to_not include('-') end Then(/^I see a form to enter the download password$/) do expect(page).to have_content('Password:') end When(/^I enter "([^"]*)" as the download password$/) do |password| fill_in :file_key, :with => password click_on 'Get file' end Then(/^I'm told the password is wrong$/) do expect(page).to have_content('does not allow access') expect(page.driver.response.status).to eql(403) end Then(/^I see a checkbox labeled "([^"]*)"$/) do |label| label = find('label', :text => label) expect(page).to have_selector("##{label[:for]}") end Then(/^the upload is refused as empty$/) do expect(page.driver.response.status).to eql(403) expect(page).to have_content('no content') end Then(/^the upload is refused as too big$/) do expect(page.driver.response.status).to eql(413) expect(page).to have_content('bigger') end Then(/^I'm denied the upload$/) do expect(page.driver.response.status).to eql(403) end tmpfhJ2Ca/features/step_definitions/urls.rb0000644000175000017500000000171012574640043020362 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . Then(/^the download URL does not contain the original filename$/) do expect(find('textarea.ready').value).to_not include(File.basename(@uploaded_file)) end tmpfhJ2Ca/features/step_definitions/uploads.rb0000644000175000017500000000544412574640040021051 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . When(/^I upload a file$/) do upload(__FILE__) end Given(/^a file has been uploaded$/) do upload(__FILE__) @download_url = find('textarea.ready').value end Given(/^a file named "([^"]*)" has been uploaded$/) do |file_name| expect(Dir.entries(@tempdir)).to_not include(file_name) # sanity check dest = File.join(@tempdir, file_name) FileUtils.cp __FILE__, dest upload(dest) @download_url = find('textarea.ready').value end Given(/^a file has been uploaded that will expire the next day$/) do visit '/' fill_in 'upload_password', :with => default_upload_password select '1 day', :from => 'expire' attach_file 'file', __FILE__ click_button 'Share!' end When(/^I upload a file with "([^"]*)" as the download password$/) do |password| upload(__FILE__, :file_key => password) end Given(/^a file has been uploaded with a download password$/) do upload(__FILE__, :file_key => 'downloadpassword') end Given(/^a file has been uploaded with "([^"]*)" as the download password$/) do |password| upload(__FILE__, :file_key => password) end Given(/^a file has been uploaded and set to be removed after a single upload$/) do visit '/' fill_in 'upload_password', :with => default_upload_password find('#one_time').set(true) # XXX: there's probably a nicer way to tick the box attach_file 'file', __FILE__ click_button 'Share!' @uploaded_file = __FILE__ @download_url = find('textarea.ready').value end Given(/^I have an empty file$/) do @file_to_upload = File.join(@tempdir, 'empty') FileUtils.touch(@file_to_upload) end When(/^I try to upload it$/) do upload(@file_to_upload) end Given(/^I have a file bigger than the limit$/) do @file_to_upload = File.join(@tempdir, 'bigger') File.open(@file_to_upload, 'w', :encoding => 'binary') do |f| f.write('-' * (Coquelicot.settings.max_file_size + 1)) end end When(/^I try to upload a file$/) do attach_file 'file', __FILE__ click_button 'Share!' end Then(/^the upload is accepted$/) do expect(page).to have_content('Share this!') end tmpfhJ2Ca/features/step_definitions/downloads.rb0000644000175000017500000000354612574640244021403 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . Then(/^the downloaded file is named "([^"]*)"$/) do |file_name| expect(page.driver.response.headers['Content-Disposition']).to start_with('attachment') expect(page.driver.response.headers['Content-Disposition']).to match(/filename=(['"]?)#{Regexp.escape(file_name)}\1/) end Then(/^(?:the downloaded file has the same content as the uploaded file|I should get the original file|I have downloaded the file)$/) do original_content = File.open(@uploaded_file, :encoding => 'binary').read expect(page.driver.response.body).to eql(original_content) end Then(/^the downloaded file has the same size as the uploaded file$/) do original_size = File.size(@uploaded_file) expect(page.driver.response.headers['Content-Length'].to_i).to eql(original_size) end Then(/^the Last\-Modified header is set to the upload time$/) do last_modified = Time.parse(page.driver.response.headers['Last-Modified']) expect(@upload_time - last_modified).to be <= 1 # less than a second end Given(/^it has been downloaded once$/) do expect(@download_url).to be_truthy visit @download_url end tmpfhJ2Ca/features/download_passwords.feature0000644000175000017500000000172312574637655021016 0ustar lunarlunarFeature: Uploaders can set a password required to download the files Scenario: Decryption key is not in the URL if a download password has been set When I upload a file with "secret" as the download password Then the download URL does not contain the decryption key Scenario: A form allows on to enter the download password Given a file has been uploaded with a download password When I follow the download link Then I see a form to enter the download password Scenario: Download is denied with a bad password Given a file has been uploaded with "downloadpass" as the download password When I follow the download link And I enter "wrong" as the download password Then I'm told the password is wrong Scenario: File is available with the correct password Given a file has been uploaded with "downloadpass" as the download password When I follow the download link And I enter "downloadpass" as the download password Then I have downloaded the file tmpfhJ2Ca/features/one_time_downloads.feature0000644000175000017500000000152212574637655020750 0ustar lunarlunarFeature: One time downloads Scenario: Uploaders can make a file downloadable only once When I visit the upload page Then I see a checkbox labeled "Remove after one download" Scenario: File can be downloaded a first time Given a file has been uploaded and set to be removed after a single upload When I follow the download link Then I have downloaded the file Scenario: Second attempt to download the file is denied Given a file has been uploaded and set to be removed after a single upload And it has been downloaded once When I follow the download link Then I'm told the file is gone Scenario: File must have been removed from the server after the first download Given a file has been uploaded and set to be removed after a single upload When I follow the download link Then the file has been removed from the server tmpfhJ2Ca/features/storage.feature0000644000175000017500000000061412574637655016544 0ustar lunarlunarFeature: Files storage protect users privacy Scenario: Files are stored encrypted When I upload a file Then the file is stored encrypted on the server # This is meant to make harder to match connection log with actual files Scenario: Files are stored under a different name the than the URL When I upload a file Then the file name on the server is different from the name in the URL tmpfhJ2Ca/features/i18n.feature0000644000175000017500000000047312574637655015662 0ustar lunarlunarFeature: Coquelicot is available in multiple languages Scenario: see available languages When I visit the main page Then I should see a link to the French version Scenario: request a specific language Given I am on the main page When I follow the link to the French version Then the page should be in French tmpfhJ2Ca/features/support/0000755000175000017500000000000013026234541015212 5ustar lunarlunartmpfhJ2Ca/features/support/env.rb0000644000175000017500000000277212575035756016356 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . require File.expand_path('../../../spec/spec_helper', __FILE__) require 'timecop' require 'capybara/cucumber' require 'cucumber/rspec/doubles' def default_upload_password 'secret' end class CoquelicotWorld include RSpec::Expectations include RSpec::Matchers include Capybara::DSL Capybara.app = Coquelicot::Application Capybara.app.set :environment, :test def upload(path, options={}) visit '/' fill_in 'upload_password', :with => default_upload_password options.each_pair do |field, value| fill_in field, :with => value end attach_file 'file', path click_button 'Share!' @uploaded_file = path @upload_time = Time.now end end World do CoquelicotWorld.new end tmpfhJ2Ca/features/support/hooks.rb0000644000175000017500000000241012574640052016664 0ustar lunarlunar# -*- coding: UTF-8 -*- # Coquelicot: "one-click" file sharing with a focus on users' privacy. # Copyright © 2015 potager.org # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # reset authentication method Before do Capybara.app.set :authentication_method, :name => :simplepass, :upload_password => Digest::SHA1.hexdigest(default_upload_password) end Around do |scenario, block| path = Dir.mktmpdir('coquelicot') begin @tempdir = path @depot_path = File.join(path, 'depot') Dir.mkdir(@depot_path) Capybara.app.set :depot_path, @depot_path block.call ensure FileUtils.remove_entry_secure path end end