]> WPIA git - gigi.git/commitdiff
Merge branch 'libs/jetty/local'
authorFelix Dörre <felix@dogcraft.de>
Thu, 30 Jun 2016 15:47:04 +0000 (17:47 +0200)
committerFelix Dörre <felix@dogcraft.de>
Thu, 30 Jun 2016 15:47:53 +0000 (17:47 +0200)
Change-Id: Ibdf6d6246cc33a319462381643cf059bf5b7bc70

412 files changed:
.classpath
.gitattributes
.gitignore
.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
.settings/org.eclipse.jdt.ui.prefs [new file with mode: 0644]
Gigi.MF [deleted file]
LICENSE
LICENSE.BSD [new file with mode: 0644]
LICENSE.GPL [new file with mode: 0644]
README.md
build.xml
config/.gitignore
config/generateTruststoreNRE.sh [new file with mode: 0755]
config/gigi.properties.template
config/test.properties.template
debian/.gitignore [new file with mode: 0644]
debian/cacert-gigi-testing.cacert-gigi-signer.default [new file with mode: 0644]
debian/cacert-gigi-testing.cacert-gigi-signer.init [new file with mode: 0644]
debian/cacert-gigi-testing.cacert-gigi.default [new file with mode: 0644]
debian/cacert-gigi-testing.cacert-gigi.init [new file with mode: 0644]
debian/cacert-gigi-testing.docs [new file with mode: 0644]
debian/cacert-gigi-testing.manpages [new file with mode: 0644]
debian/cacert-gigi-testing.postinst [new file with mode: 0644]
debian/cacert-gigi.cacert-gigi-signer.init [new file with mode: 0644]
debian/cacert-gigi.cacert-gigi.init [new file with mode: 0644]
debian/cacert-gigi.docs [new file with mode: 0644]
debian/cacert-gigi.manpages [new file with mode: 0644]
debian/cacert-gigi.postinst [new file with mode: 0644]
debian/changelog [new file with mode: 0644]
debian/compat [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/gigi.1 [new file with mode: 0644]
debian/rules [new file with mode: 0755]
debian/source/format [new file with mode: 0644]
doc/TemplateSyntax.txt [new file with mode: 0644]
doc/beforeYouStart.txt
doc/exoticKeys.sh [new file with mode: 0644]
doc/jenkinsJob/README.txt
doc/jenkinsJob/ci-tests-setup.txt [new file with mode: 0644]
doc/jenkinsJob/config.xml
doc/jenkinsJob/dyn-txt.php [new file with mode: 0644]
doc/scripts/.gitignore
doc/scripts/generateKeys.sh [deleted file]
doc/scripts/generateSomeCsrs.sh [new file with mode: 0755]
doc/scripts/generateTruststore.sh [deleted file]
doc/scripts/getScrypt.sh [new file with mode: 0755]
doc/scripts/gigi [new file with mode: 0755]
doc/scripts/selfsign.config [deleted file]
doc/tableStructure.sql [deleted file]
doc/wishList.md [new file with mode: 0644]
keys/.dirinfo [new file with mode: 0644]
keys/.gitignore [new file with mode: 0644]
keys/generateKeys.sh [new file with mode: 0755]
keys/generateTruststore.sh [new file with mode: 0755]
keys/selfsign.config [new file with mode: 0644]
lib/scrypt/com/lambdaworks/crypto/PBKDF.java [new file with mode: 0644]
lib/scrypt/com/lambdaworks/crypto/SCrypt.java [new file with mode: 0644]
lib/scrypt/com/lambdaworks/crypto/SCryptUtil.java [new file with mode: 0644]
locale/.gitignore
natives/Makefile
natives/org_cacert_gigi_natives_SetUID.c
src/org/cacert/gigi/DevelLauncher.java [deleted file]
src/org/cacert/gigi/Gigi.java
src/org/cacert/gigi/Gigi.templ [new file with mode: 0644]
src/org/cacert/gigi/GigiApiException.java [new file with mode: 0644]
src/org/cacert/gigi/GigiConfig.java
src/org/cacert/gigi/Language.java [deleted file]
src/org/cacert/gigi/Launcher.java
src/org/cacert/gigi/Name.java [deleted file]
src/org/cacert/gigi/PermissionCheckable.java [new file with mode: 0644]
src/org/cacert/gigi/PolicyRedirector.java
src/org/cacert/gigi/TestServlet.java [deleted file]
src/org/cacert/gigi/User.java [deleted file]
src/org/cacert/gigi/api/APIPoint.java [new file with mode: 0644]
src/org/cacert/gigi/api/CATSImport.java [new file with mode: 0644]
src/org/cacert/gigi/api/CATSResolve.java [new file with mode: 0644]
src/org/cacert/gigi/api/CreateCertificate.java [new file with mode: 0644]
src/org/cacert/gigi/api/GigiAPI.java [new file with mode: 0644]
src/org/cacert/gigi/api/RevokeCertificate.java [new file with mode: 0644]
src/org/cacert/gigi/crypto/SMIME.java [new file with mode: 0644]
src/org/cacert/gigi/crypto/SPKAC.java [new file with mode: 0644]
src/org/cacert/gigi/database/DatabaseConnection.java
src/org/cacert/gigi/database/GigiPreparedStatement.java [new file with mode: 0644]
src/org/cacert/gigi/database/GigiResultSet.java [new file with mode: 0644]
src/org/cacert/gigi/database/SQLFileManager.java [new file with mode: 0644]
src/org/cacert/gigi/database/tableStructure.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_1.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_10.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_11.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_12.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_13.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_14.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_2.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_3.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_4.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_5.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_6.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_7.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_8.sql [new file with mode: 0644]
src/org/cacert/gigi/database/upgrade/from_9.sql [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Assurance.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/CACertificate.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/CATS.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/CPS.properties [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Certificate.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/CertificateOwner.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/CertificateProfile.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Digest.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Domain.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/DomainPingConfiguration.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/DomainPingExecution.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/DomainPingType.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/EmailAddress.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Group.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/IdCachable.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Job.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Name.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/ObjectCache.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Organisation.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/SupportedUser.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/User.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/Verifyable.java [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/wrappers/DataContainer.java [new file with mode: 0644]
src/org/cacert/gigi/email/CommandlineEmailProvider.java [deleted file]
src/org/cacert/gigi/email/EmailProvider.java
src/org/cacert/gigi/email/MailProbe.java [new file with mode: 0644]
src/org/cacert/gigi/email/Sendmail.java
src/org/cacert/gigi/email/TestEmailProvider.java [deleted file]
src/org/cacert/gigi/localisation/Language.java [new file with mode: 0644]
src/org/cacert/gigi/natives/SetUID.java
src/org/cacert/gigi/output/AssurancesDisplay.java [new file with mode: 0644]
src/org/cacert/gigi/output/AssurancesDisplay.templ [new file with mode: 0644]
src/org/cacert/gigi/output/CertificateIterable.java [new file with mode: 0644]
src/org/cacert/gigi/output/CertificateTable.java [deleted file]
src/org/cacert/gigi/output/CertificateTable.templ [new file with mode: 0644]
src/org/cacert/gigi/output/CertificateValiditySelector.java [new file with mode: 0644]
src/org/cacert/gigi/output/ClientCSRGenerate.java [new file with mode: 0644]
src/org/cacert/gigi/output/ClientCSRGenerate.templ [new file with mode: 0644]
src/org/cacert/gigi/output/DataTable.java [deleted file]
src/org/cacert/gigi/output/DateSelector.java
src/org/cacert/gigi/output/Form.java [deleted file]
src/org/cacert/gigi/output/GroupSelector.java [new file with mode: 0644]
src/org/cacert/gigi/output/HashAlgorithms.java [new file with mode: 0644]
src/org/cacert/gigi/output/IMenuItem.java [new file with mode: 0644]
src/org/cacert/gigi/output/MailTable.java [deleted file]
src/org/cacert/gigi/output/Menu.java [new file with mode: 0644]
src/org/cacert/gigi/output/MenuCollector.java [new file with mode: 0644]
src/org/cacert/gigi/output/Outputable.java [deleted file]
src/org/cacert/gigi/output/PageMenuItem.java [new file with mode: 0644]
src/org/cacert/gigi/output/SimpleMenuItem.java [new file with mode: 0644]
src/org/cacert/gigi/output/Template.java [deleted file]
src/org/cacert/gigi/output/template/ForeachStatement.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/Form.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/IfStatement.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/IterableDataset.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/OutputVariableCommand.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/Outputable.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/OutputableArrayIterable.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/Scope.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/SprintfCommand.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/Template.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/TemplateBlock.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/Translatable.java [new file with mode: 0644]
src/org/cacert/gigi/output/template/TranslateCommand.java [new file with mode: 0644]
src/org/cacert/gigi/pages/AboutPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/AboutPage.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/HandlesMixedRequest.java [new file with mode: 0644]
src/org/cacert/gigi/pages/LoginPage.java
src/org/cacert/gigi/pages/LoginPage.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/LogoutPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/MainPage.java
src/org/cacert/gigi/pages/MainPage.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/MainPageNotLogin.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/OneFormPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/Page.java
src/org/cacert/gigi/pages/PasswordResetForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/PasswordResetPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/PolicyIndex.java [new file with mode: 0644]
src/org/cacert/gigi/pages/RootCertPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/RootCertPage.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/StaticPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/TestSecure.java
src/org/cacert/gigi/pages/Verify.java
src/org/cacert/gigi/pages/Verify.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/ChangeForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/ChangePasswordForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/ChangePasswordPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/History.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/History.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/MailAdd.java [deleted file]
src/org/cacert/gigi/pages/account/MailCertificates.java [deleted file]
src/org/cacert/gigi/pages/account/MailOverview.java [deleted file]
src/org/cacert/gigi/pages/account/MyDetails.java
src/org/cacert/gigi/pages/account/MyDetails.templ
src/org/cacert/gigi/pages/account/MyDetailsForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/MyDetailsForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/MyDetailsFormAssured.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/MyOrganisationsForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/MyOrganisationsForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/UserTrainings.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/UserTrainings.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/CertificateAdd.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/CertificateDisplay.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/CertificateIssueForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/CertificateIssueForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/CertificateModificationForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/CertificateModificationForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/CertificateRequest.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/Certificates.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/certs/RequestCertificate.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainAddForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainAddForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainManagementForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainManagementForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainOverview.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainOverview.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainPinglogForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/DomainPinglogForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/PingConfigForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/domain/PingConfigForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/mail/MailAddForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/mail/MailAddForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/mail/MailManagementForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/mail/MailManagementForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/account/mail/MailOverview.java [new file with mode: 0644]
src/org/cacert/gigi/pages/account/mail/MailOverview.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/TTPAdminForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/TTPAdminForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/TTPAdminPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/TTPAdminPage.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/FindDomainForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/FindDomainForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/FindDomainPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/FindUserForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/FindUserForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/FindUserPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportEnterTicketForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportEnterTicketForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportEnterTicketPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportRevokeCertificatesForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportRevokeCertificatesForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportUserDetailsForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportUserDetailsForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportUserDetailsPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/admin/support/SupportUserDetailsPage.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/error/AccessDenied.java [new file with mode: 0644]
src/org/cacert/gigi/pages/error/AccessDenied.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/error/PageNotFound.java [new file with mode: 0644]
src/org/cacert/gigi/pages/error/PageNotFound.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/main/RegisterPage.java
src/org/cacert/gigi/pages/main/RegisterPage.templ
src/org/cacert/gigi/pages/main/Signup.java
src/org/cacert/gigi/pages/main/Signup.templ
src/org/cacert/gigi/pages/orga/AffiliationForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/AffiliationForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/CreateOrgForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/CreateOrgForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/CreateOrgPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/EditOrg.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/OrgDomainAddForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/OrgDomainAddForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/ViewOrgPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/orga/ViewOrgs.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/AssuranceForm.java
src/org/cacert/gigi/pages/wot/AssuranceForm.templ
src/org/cacert/gigi/pages/wot/AssurePage.java
src/org/cacert/gigi/pages/wot/AssureeSearch.templ
src/org/cacert/gigi/pages/wot/MyListingForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/MyListingForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/MyListingPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/MyPoints.java [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/MyPoints.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/RequestTTPForm.java [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/RequestTTPForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/RequestTTPPage.java [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/RequestTTPPage.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/wot/Rules.templ [new file with mode: 0644]
src/org/cacert/gigi/ping/DNSPinger.java [new file with mode: 0644]
src/org/cacert/gigi/ping/DomainPinger.java [new file with mode: 0644]
src/org/cacert/gigi/ping/EmailPinger.java [new file with mode: 0644]
src/org/cacert/gigi/ping/HTTPFetch.java [new file with mode: 0644]
src/org/cacert/gigi/ping/PingerDaemon.java [new file with mode: 0644]
src/org/cacert/gigi/ping/SSLPinger.java [new file with mode: 0644]
src/org/cacert/gigi/util/AuthorizationContext.java [new file with mode: 0644]
src/org/cacert/gigi/util/CalendarUtil.java [new file with mode: 0644]
src/org/cacert/gigi/util/CertExporter.java [new file with mode: 0644]
src/org/cacert/gigi/util/CipherInfo.java
src/org/cacert/gigi/util/DNSUtil.java [new file with mode: 0644]
src/org/cacert/gigi/util/DayDate.java [new file with mode: 0644]
src/org/cacert/gigi/util/HTMLEncoder.java
src/org/cacert/gigi/util/KeyStorage.java [new file with mode: 0644]
src/org/cacert/gigi/util/Notary.java
src/org/cacert/gigi/util/PEM.java [new file with mode: 0644]
src/org/cacert/gigi/util/PasswordHash.java
src/org/cacert/gigi/util/PasswordStrengthChecker.java
src/org/cacert/gigi/util/PublicSuffixes.java [new file with mode: 0644]
src/org/cacert/gigi/util/RandomToken.java
src/org/cacert/gigi/util/RateLimit.java [new file with mode: 0644]
src/org/cacert/gigi/util/ServerConstants.java
static/menu.js [deleted file]
static/static/css/bootstrap.min.css [new file with mode: 0644]
static/static/css/cacert.css [new file with mode: 0644]
static/static/default.css [moved from static/default.css with 85% similarity]
static/static/images/bit.png [new file with mode: 0644]
static/static/images/cacert4-test.png [new file with mode: 0644]
static/static/images/cacert4.png [new file with mode: 0644]
static/static/images/nlnet.png [new file with mode: 0644]
static/static/images/oan.png [new file with mode: 0644]
static/static/images/tunix.png [new file with mode: 0644]
static/static/js/bootstrap.min.js [new file with mode: 0644]
static/static/js/expert.js [new file with mode: 0644]
static/static/js/jquery.min.js [new file with mode: 0644]
static/static/js/localDate.js [new file with mode: 0644]
static/static/keygenIE.js [new file with mode: 0644]
static/www/policy/AssurancePolicy.html [moved from static/policy/AssurancePolicy.html with 100% similarity]
static/www/policy/CAcertCommunityAgreement.html [moved from static/policy/CAcertCommunityAgreement.html with 100% similarity]
static/www/policy/CertificationPracticeStatement.html [moved from static/policy/CertificationPracticeStatement.html with 100% similarity]
static/www/policy/DisputeResolutionPolicy.html [moved from static/policy/DisputeResolutionPolicy.html with 100% similarity]
static/www/policy/NRPDisclaimerAndLicence.html [moved from static/policy/NRPDisclaimerAndLicence.html with 100% similarity]
static/www/policy/OrganisationAssurancePolicy.html [moved from static/policy/OrganisationAssurancePolicy.html with 100% similarity]
static/www/policy/PolicyOnPolicy.html [moved from static/policy/PolicyOnPolicy.html with 100% similarity]
static/www/policy/PrivacyPolicy.html [moved from static/policy/PrivacyPolicy.html with 100% similarity]
static/www/policy/RootDistributionLicense.html [moved from static/policy/RootDistributionLicense.html with 100% similarity]
static/www/policy/TermsOfService.html [new file with mode: 0644]
static/www/policy/cacert-draft.png [moved from static/policy/cacert-draft.png with 100% similarity]
templates/base.html [deleted file]
tests/com/lambdaworks/crypto/test/CryptoTestUtil.java [new file with mode: 0644]
tests/com/lambdaworks/crypto/test/PBKDFTest.java [new file with mode: 0644]
tests/com/lambdaworks/crypto/test/SCryptTest.java [new file with mode: 0644]
tests/com/lambdaworks/crypto/test/SCryptUtilTest.java [new file with mode: 0644]
tests/org/cacert/gigi/DomainVerification.java [new file with mode: 0644]
tests/org/cacert/gigi/LoginTest.java
tests/org/cacert/gigi/TestCalendarUtil.java [new file with mode: 0644]
tests/org/cacert/gigi/TestCertificate.java [new file with mode: 0644]
tests/org/cacert/gigi/TestCrossDomainAccess.java [new file with mode: 0644]
tests/org/cacert/gigi/TestDomain.java [new file with mode: 0644]
tests/org/cacert/gigi/TestLanguage.java [new file with mode: 0644]
tests/org/cacert/gigi/TestName.java [new file with mode: 0644]
tests/org/cacert/gigi/TestObjectCache.java [new file with mode: 0644]
tests/org/cacert/gigi/TestOrga.java [new file with mode: 0644]
tests/org/cacert/gigi/TestPasswordReset.java [new file with mode: 0644]
tests/org/cacert/gigi/TestSQL.java [new file with mode: 0644]
tests/org/cacert/gigi/TestSSL.java
tests/org/cacert/gigi/TestSecurityHeaders.java [new file with mode: 0644]
tests/org/cacert/gigi/TestSeparateSessionScope.java [new file with mode: 0644]
tests/org/cacert/gigi/TestUser.java [new file with mode: 0644]
tests/org/cacert/gigi/TestUserGroupMembership.java [new file with mode: 0644]
tests/org/cacert/gigi/api/ImportCATSResult.java [new file with mode: 0644]
tests/org/cacert/gigi/api/IssueCert.java [new file with mode: 0644]
tests/org/cacert/gigi/crypto/TestSPKAC.java [new file with mode: 0644]
tests/org/cacert/gigi/crypto/sampleSPKAC.txt [new file with mode: 0644]
tests/org/cacert/gigi/email/TestEmailProviderClass.java [new file with mode: 0644]
tests/org/cacert/gigi/email/TestSendmail.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/account/TestCertificateAdd.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/account/TestCertificateRequest.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/account/TestChangePassword.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/account/TestDomain.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/account/TestMailManagement.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/account/TestMyDetailsEdit.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/account/TestPasswordResetExternal.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/admin/TestSEAdminPageDetails.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/admin/TestSEAdminPageUserDomainSearch.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/admin/TestSEAdminPageUserMailSearch.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/admin/TestSEAdminTicketSetting.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/main/RegisterPageTest.java
tests/org/cacert/gigi/pages/orga/TestOrgDomain.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/orga/TestOrgManagement.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/wot/TestAssurance.java
tests/org/cacert/gigi/pages/wot/TestListing.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/wot/TestTTP.java [new file with mode: 0644]
tests/org/cacert/gigi/pages/wot/TestTTPAdmin.java [new file with mode: 0644]
tests/org/cacert/gigi/ping/TestDNS.java [new file with mode: 0644]
tests/org/cacert/gigi/ping/TestHTTP.java [new file with mode: 0644]
tests/org/cacert/gigi/ping/TestSSL.java [new file with mode: 0644]
tests/org/cacert/gigi/template/TestTemplate.java [new file with mode: 0644]
tests/org/cacert/gigi/testUtils/ClientTest.java [new file with mode: 0644]
tests/org/cacert/gigi/testUtils/ConfiguredTest.java [new file with mode: 0644]
tests/org/cacert/gigi/testUtils/IOUtils.java
tests/org/cacert/gigi/testUtils/InitTruststore.java
tests/org/cacert/gigi/testUtils/ManagedTest.java
tests/org/cacert/gigi/testUtils/OrgTest.java [new file with mode: 0644]
tests/org/cacert/gigi/testUtils/PingTest.java [new file with mode: 0644]
tests/org/cacert/gigi/testUtils/RegisteredUser.java [new file with mode: 0644]
tests/org/cacert/gigi/testUtils/TestEmailReceiver.java [new file with mode: 0644]
tests/org/cacert/gigi/testUtils/TestEmailReciever.java [deleted file]
tests/org/cacert/gigi/util/TestHTMLEncoder.java
tests/org/cacert/gigi/util/TestNotary.java [new file with mode: 0644]
tests/org/cacert/gigi/util/TestPasswordHash.java
tests/org/cacert/gigi/util/TestPasswordMigration.java [new file with mode: 0644]
tests/org/cacert/gigi/util/TestPasswordStrengthChecker.java
tests/org/cacert/gigi/util/TestPublicSuffixes.java [new file with mode: 0644]
tests/org/cacert/gigi/util/TestPublicSuffixes.txt [new file with mode: 0644]
util-testing/org/cacert/gigi/DevelLauncher.java [new file with mode: 0644]
util-testing/org/cacert/gigi/DevelTicketWait.templ [new file with mode: 0644]
util-testing/org/cacert/gigi/GenerateProfileOverview.java [new file with mode: 0644]
util-testing/org/cacert/gigi/TestLauncher.java [new file with mode: 0644]
util-testing/org/cacert/gigi/email/CommandlineEmailProvider.java [new file with mode: 0644]
util-testing/org/cacert/gigi/email/TestEmailProvider.java [new file with mode: 0644]
util-testing/org/cacert/gigi/localisation/FileIterable.java [new file with mode: 0644]
util-testing/org/cacert/gigi/localisation/TaintSource.java [new file with mode: 0644]
util-testing/org/cacert/gigi/localisation/TranslationCollectingVisitor.java [new file with mode: 0644]
util-testing/org/cacert/gigi/localisation/TranslationCollector.java [new file with mode: 0644]
util-testing/org/cacert/gigi/localisation/conf.txt [new file with mode: 0644]
util-testing/org/cacert/gigi/pages/Manager.java [new file with mode: 0644]
util-testing/org/cacert/gigi/pages/Manager.templ [new file with mode: 0644]
util-testing/org/cacert/gigi/pages/ManagerMails.templ [new file with mode: 0644]
util-testing/org/cacert/gigi/util/IOUtils.java [new file with mode: 0644]
util-testing/org/cacert/gigi/util/SimpleSigner.java [new file with mode: 0644]
util/org/cacert/gigi/util/DatabaseManager.java
util/org/cacert/gigi/util/FetchLocales.java

index c9c9607c035e390db37c9fb602bcd860e83be660..0a18b58341bd98d5ef7dbad0422e88a808efbe65 100644 (file)
@@ -3,9 +3,11 @@
        <classpathentry kind="src" path="lib/servlet-api"/>
        <classpathentry kind="src" path="lib/jetty"/>
        <classpathentry kind="src" path="lib/jtar"/>
+       <classpathentry kind="src" path="lib/scrypt"/>
        <classpathentry kind="src" path="src"/>
        <classpathentry kind="src" path="util"/>
        <classpathentry kind="src" path="tests"/>
+       <classpathentry excluding="org/cacert/gigi/locatisation/|org/cacert/gigi/localisation/" kind="src" path="util-testing"/>
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
        <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
        <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/MySQL"/>
index 9535ab16027f42b756787a403ba26f895630831b..10c31b96b067369f4553016c70f621fbb2877bde 100644 (file)
@@ -1,6 +1,6 @@
 *      text=auto
 
-*.java text
+*.java text diff=java
 
 *.txt  text
 *.md   text
index eda95824d8f50701da3552860c12a17429c78953..6746097e3b63ca9bf3bd3b91124f067c4fe6c11c 100644 (file)
@@ -8,4 +8,19 @@
 # Generated Stuff
 *.manifest
 
+#OS stuff
+.DS_Store
+
 /bin
+/bintest
+/binutil
+/binutil-testing
+/lib/bin
+/work
+static.tar.gz
+
+/src/org/cacert/gigi/util/effective_tld_names.dat
+/Gigi.MF
+/testKeypair
+
+/signer
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..99f26c0
--- /dev/null
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..2a0716e
--- /dev/null
@@ -0,0 +1,296 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=48
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=0
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=48
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=80
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=0
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=32
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=32
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=1
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=true
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=true
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=8192
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=4
+org.eclipse.jdt.core.formatter.use_on_off_tags=true
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=false
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=false
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644 (file)
index 0000000..4ef74ca
--- /dev/null
@@ -0,0 +1,66 @@
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_cacert-gigi
+formatter_settings_version=12
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=java;javax;org;com;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=1
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=true
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=false
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_functional_interfaces=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=true
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.insert_inferred_type_arguments=false
+sp_cleanup.make_local_variable_final=false
+sp_cleanup.make_parameters_final=false
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=true
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_redundant_type_arguments=false
+sp_cleanup.remove_trailing_whitespaces=false
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=true
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=true
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_anonymous_class_creation=false
+sp_cleanup.use_blocks=true
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_lambda=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+sp_cleanup.use_type_arguments=false
diff --git a/Gigi.MF b/Gigi.MF
deleted file mode 100644 (file)
index 614d595..0000000
--- a/Gigi.MF
+++ /dev/null
@@ -1,3 +0,0 @@
-Manifest-Version: 1.0
-Main-Class: org.cacert.gigi.Launcher
-
diff --git a/LICENSE b/LICENSE
index d7f105139782ab695d86613e343916f7372f4ac0..eafe33ad1571a6643ab0e8b74e05c4842fd41a90 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,339 +1,4 @@
-GNU GENERAL PUBLIC LICENSE
-                       Version 2, June 1991
+The code is licensed under GPLv2.
+Parts of the code carry a dual license under GPLv2+BSD based on the wishes of their authors.
 
- Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it.  (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.)  You can apply it to
-your programs, too.
-
-  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
-this service 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.
-
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must show them these terms so they know their
-rights.
-
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
-  Finally, any free program is threatened constantly by software
-patents.  We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary.  To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License.  The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language.  (Hereinafter, translation is included without limitation in
-the term "modification".)  Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
-  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
-  2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) You must cause the modified files to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    b) You must cause any work that you distribute or publish, that in
-    whole or in part contains or is derived from the Program or any
-    part thereof, to be licensed as a whole at no charge to all third
-    parties under the terms of this License.
-
-    c) If the modified program normally reads commands interactively
-    when run, you must cause it, when started running for such
-    interactive use in the most ordinary way, to print or display an
-    announcement including an appropriate copyright notice and a
-    notice that there is no warranty (or else, saying that you provide
-    a warranty) and that users may redistribute the program under
-    these conditions, and telling the user how to view a copy of this
-    License.  (Exception: if the Program itself is interactive but
-    does not normally print such an announcement, your work based on
-    the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
-    a) Accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of Sections
-    1 and 2 above on a medium customarily used for software interchange; or,
-
-    b) Accompany it with a written offer, valid for at least three
-    years, to give any third party, for a charge no more than your
-    cost of physically performing source distribution, a complete
-    machine-readable copy of the corresponding source code, to be
-    distributed under the terms of Sections 1 and 2 above on a medium
-    customarily used for software interchange; or,
-
-    c) Accompany it with the information you received as to the offer
-    to distribute corresponding source code.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form with such
-    an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it.  For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable.  However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License.  Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
-  5. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Program or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
-  7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-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
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded.  In such case, this License incorporates
-the limitation as if written in the body of this License.
-
-  9. The Free Software Foundation may publish revised and/or new versions
-of the 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 a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
-  10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-                            NO WARRANTY
-
-  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
-
-  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE 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.
-
-                     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
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    {description}
-    Copyright (C) {year}  {fullname}
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) year name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
-  `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
-  {signature of Ty Coon}, 1 April 1989
-  Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs.  If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
+For detailed license information see "debian/copyright" ...
diff --git a/LICENSE.BSD b/LICENSE.BSD
new file mode 100644 (file)
index 0000000..b7bf085
--- /dev/null
@@ -0,0 +1,24 @@
+Copyright (c) 2015, CAcert
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, 
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, 
+this list of conditions and the following disclaimer in the documentation 
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
+THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/LICENSE.GPL b/LICENSE.GPL
new file mode 100644 (file)
index 0000000..d7f1051
--- /dev/null
@@ -0,0 +1,339 @@
+GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  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
+this service 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.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE 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.
+
+                     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
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    {description}
+    Copyright (C) {year}  {fullname}
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  {signature of Ty Coon}, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
index 8f4be3bef366b165526c235b6e03e07ccd40a7e7..e4267e76088b6ab6326fe877af7a31039a72a9db 100644 (file)
--- a/README.md
+++ b/README.md
@@ -5,3 +5,6 @@ Webserver Module for CAcert
 
 
 Contains source from jetty 9.1.0.RC0
+
+Instructions for getting your local instance running are in doc/beforeYouStart.txt
+
index ff8c6d626fda8d3ed4695dc64eeece255051ac0d..6816b7d81816a35392dfa0dfc285b180f6d9e670 100644 (file)
--- a/build.xml
+++ b/build.xml
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<project basedir="." default="all" name="cacert-gigi">
-       <property environment="env"/>
-       <property name="junit.output.dir" value="junit"/>
-       <property name="debuglevel" value="source,lines,vars"/>
-       <property name="target" value="1.8"/>
-       <property name="source" value="1.8"/>
-       <property name="mysqlconnector" value="mysql-connector-java-5.1.31-bin.jar"/>
-       <property name="juintexec" value="."/>
+<project basedir="." default="develop" name="cacert-gigi" xmlns:jacoco="antlib:org.jacoco.ant">
+
+       <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
+               <classpath path="/usr/share/java/jacocoant.jar" />
+       </taskdef>
+
+       <property environment="env" />
+       <property name="PACKAGE" value="cacert-gigi" />
+       <property name="junit.output.dir" value="junit" />
+       <property name="debuglevel" value="source,lines,vars" />
+       <property name="target" value="1.8" />
+       <property name="source" value="1.8" />
+       <property name="sqlconnector" value="/usr/share/java/postgresql-jdbc4.jar" />
+       <property name="juintexec" value="/usr/share/java" />
        <path id="JUnit 4.libraryclasspath">
-               <pathelement location="${juintexec}/junit.jar"/>
-               <pathelement location="${juintexec}/org.hamcrest.core.jar"/>
+               <pathelement location="${juintexec}/junit4.jar" />
+               <pathelement location="${juintexec}/hamcrest-core.jar" />
        </path>
        <path id="cacert-gigi.classpath">
-               <pathelement location="bin"/>
-               <pathelement location="${mysqlconnector}"/>
+               <pathelement location="bin" />
+               <pathelement location="binutil" />
+               <pathelement location="${sqlconnector}" />
        </path>
        <path id="cacert-gigi.test.classpath">
-               <pathelement location="bin"/>
-               <pathelement location="bintest"/>
-               <path refid="JUnit 4.libraryclasspath"/>
-               <pathelement location="${mysqlconnector}"/>
+               <pathelement location="bintest" />
+               <pathelement location="bin" />
+               <pathelement location="binutil" />
+               <pathelement location="binutil-testing" />
+               <path refid="JUnit 4.libraryclasspath" />
+               <pathelement location="${sqlconnector}" />
+       </path>
+       <path id="cacert-gigi.test.classpath.jdt">
+               <pathelement location="${jdt}" />
        </path>
        <target name="init">
-               <mkdir dir="bin"/>
-               <mkdir dir="bintest"/>
+               <mkdir dir="bin" />
+               <mkdir dir="binutil" />
+               <mkdir dir="binutil-testing" />
+               <mkdir dir="bintest" />
 
                <copy includeemptydirs="false" todir="bin">
                        <fileset dir="lib/servlet-api">
-                               <exclude name="**/*.launch"/>
-                               <exclude name="**/*.java"/>
+                               <exclude name="**/*.launch" />
+                               <exclude name="**/*.java" />
                        </fileset>
-               </copy>
-               <copy includeemptydirs="false" todir="bin">
                        <fileset dir="lib/jetty">
-                               <exclude name="**/*.launch"/>
-                               <exclude name="**/*.java"/>
+                               <exclude name="**/*.launch" />
+                               <exclude name="**/*.java" />
                        </fileset>
-               </copy>
-               <copy includeemptydirs="false" todir="bin">
                        <fileset dir="src">
-                               <exclude name="**/*.launch"/>
-                               <exclude name="**/*.java"/>
+                               <exclude name="**/*.launch" />
+                               <exclude name="**/*.java" />
                        </fileset>
-               </copy>
-               <copy includeemptydirs="false" todir="bin">
                        <fileset dir="util">
-                               <exclude name="**/*.launch"/>
-                               <exclude name="**/*.java"/>
+                               <exclude name="**/*.launch" />
+                               <exclude name="**/*.java" />
+                       </fileset>
+               </copy>
+               <copy includeemptydirs="false" todir="binutil-testing">
+                       <fileset dir="util-testing">
+                               <exclude name="**/*.launch" />
+                               <exclude name="**/*.java" />
                        </fileset>
                </copy>
                <copy includeemptydirs="false" todir="bintest">
                        <fileset dir="tests">
-                               <exclude name="**/*.launch"/>
-                               <exclude name="**/*.java"/>
+                               <exclude name="**/*.launch" />
+                               <exclude name="**/*.java" />
                        </fileset>
                </copy>
        </target>
        <target name="clean">
-               <delete dir="bin"/>
+               <delete dir="bin" />
+       </target>
+       <target name="clean-test">
+               <delete dir="bintest" />
+               <delete dir="cocoReport" failonerror="false"/>
+
        </target>
-       <target depends="clean" name="cleanall"/>
-       <target depends="build-project, native" name="build"/>
+       <target depends="clean,clean-test" name="cleanall" />
+       <target depends="build-project, build-testing, native" name="build" />
        <target depends="init" name="build-project">
-               <echo message="${ant.project.name}: ${ant.file}"/>
-               <javac debug="true" debuglevel="${debuglevel}" destdir="bin" includeantruntime="false" source="${source}" target="${target}">
-                       <src path="lib/servlet-api"/>
-                       <src path="lib/jetty"/>
-                       <src path="lib/jtar"/>
-                       <src path="src"/>
-                       <src path="util"/>
-                       <classpath refid="cacert-gigi.classpath"/>
+               <exec outputproperty="git-version" executable="git">
+                       <arg line="rev-parse"/>
+                       <arg line="HEAD"/>
+               </exec>
+               <manifest file="Gigi.MF">
+                       <attribute name="Main-Class" value="org.cacert.gigi.Launcher" />
+                       <attribute name="Implementation-Version" value="${git-version}" />
+               </manifest>
+               <echo message="${ant.project.name}: ${ant.file}" />
+               <javac encoding="UTF-8" debug="true" debuglevel="${debuglevel}" destdir="bin"
+                       includeantruntime="false" source="${source}" target="${target}">
+                       <compilerarg value="-XDignore.symbol.file"/>
+                       <src path="lib/servlet-api" />
+                       <src path="lib/jetty" />
+                       <src path="lib/jtar" />
+                       <src path="lib/scrypt" />
+                       <src path="src" />
+                       <classpath refid="cacert-gigi.classpath" />
+               </javac>
+               <javac encoding="UTF-8" debug="true" debuglevel="${debuglevel}" destdir="binutil"
+                       includeantruntime="false" source="${source}" target="${target}">
+                       <compilerarg value="-XDignore.symbol.file"/>
+                       <src path="util" />
+                       <classpath refid="cacert-gigi.classpath" />
+               </javac>
+       </target>
+       <target depends="init, build-project" name="build-testing">
+               <javac encoding="UTF-8" debug="true" debuglevel="${debuglevel}" destdir="binutil-testing"
+                       includeantruntime="false" source="${source}" target="${target}">
+                       <compilerarg value="-XDignore.symbol.file"/>
+                       <src path="util-testing" />
+                       <exclude name="org/cacert/gigi/localisation/**"/>
+                       <classpath refid="cacert-gigi.classpath" />
                </javac>
        </target>
+       <target depends="init, build-project" name="build-testing-l10n">
+               <javac encoding="UTF-8" debug="true" debuglevel="${debuglevel}" destdir="binutil-testing"
+                       includeantruntime="false" source="${source}" target="${target}">
+                       <compilerarg value="-XDignore.symbol.file"/>
+                       <src path="util-testing" />
+                       <include name="org/cacert/gigi/localisation/**"/>
+                       <classpath refid="cacert-gigi.classpath" />
+                       <classpath refid="cacert-gigi.test.classpath.jdt" />
+               </javac>
+               <java classname="org.cacert.gigi.localisation.TranslationCollector">
+                       <arg value="util-testing/org/cacert/gigi/localisation/conf.txt"/>
+                       <arg value="."/>
+                       <arg value="messages.po"/>
+                       <classpath refid="cacert-gigi.test.classpath" />
+                       <classpath refid="cacert-gigi.test.classpath.jdt" />
+               </java>
+       </target>
 
        <target name="native">
                <exec executable="make" dir="natives"/>
        </target>
 
-       <target depends="build" name="pack">
-               <jar destfile="gigi.jar" basedir="bin" manifest="Gigi.MF"/>
+       <target depends="build-project, update-effective-tlds" name="pack">
+               <jar destfile="gigi.jar" basedir="bin" manifest="Gigi.MF" update="false"/>
+               <jar destfile="gigi.jar" basedir="binutil" update="on"/>
+       </target>
+
+       <target depends="build-testing,update-effective-tlds" name="pack-testing">
+               <jar destfile="gigi-testing.jar" basedir="bin" manifest="Gigi.MF" update="false"/>
+               <jar destfile="gigi-testing.jar" basedir="binutil" update="on"/>
+               <jar destfile="gigi-testing.jar" basedir="binutil-testing" update="on"/>
        </target>
 
        <target depends="test,native" name="bundle">
-               <zip destfile="gigi-linux_amd64.zip" basedir="." includes="gigi.jar,native/*.so,doc/tableStructure.sql,static/**,templates/**"/>
+               <zip destfile="gigi-linux_amd64.zip" basedir="."
+                       includes="gigi.jar,native/*.so,src/org/cacert/gigi/database/tableStructure.sql,static/**,templates/**" />
        </target>
        <target name="static-bundle">
-               <tar destfile="static.tar.gz" compression="gzip" basedir="." includes="doc/tableStructure.sql,static/**,templates/**"/>
+               <mkdir dir="work"/>
+               <mkdir dir="work/static"/>
+               <copy todir="work/static">
+                       <fileset dir="static"/>
+               </copy>
+               <move file="work/static/static/images/cacert4-test.png" tofile="work/static/static/images/cacert4.png"/>
+               <delete file="work/static/static/image/cacert4-test.png"/>
+               <tar destfile="static.tar.gz" compression="gzip" basedir="work"
+                       includes="../src/org/cacert/gigi/database/tableStructure.sql,**,templates/**" />
+       </target>
+
+       <target name="static-bundle-release">
+               <mkdir dir="work"/>
+               <mkdir dir="work/static"/>
+               <copy todir="work/static">
+                       <fileset dir="static"/>
+               </copy>
+               <delete file="work/static/static/image/cacert4-test.png"/>
+               <tar destfile="static.tar.gz" compression="gzip" basedir="work"
+                       includes="../src/org/cacert/gigi/database/tableStructure.sql,**,templates/**" />
        </target>
 
-       <target name="all" depends="bundle,static-bundle"/>
+       <target name="develop" depends="bundle,static-bundle" />
 
+       <target name="release" depends="bundle,static-bundle-release" />
 
-       <target depends="init,build-project" name="build-project-test">
-               <echo message="${ant.project.name}: ${ant.file}"/>
-               <javac debug="true" debuglevel="${debuglevel}" destdir="bintest" includeantruntime="false" source="${source}" target="${target}">
-                       <src path="tests"/>
-                       <classpath refid="cacert-gigi.test.classpath"/>
+       <target depends="init,build-testing,update-effective-tlds" name="build-project-test">
+               <echo message="${ant.project.name}: ${ant.file}" />
+               <javac encoding="UTF-8" debug="true" debuglevel="${debuglevel}" destdir="bintest"
+                       includeantruntime="false" source="${source}" target="${target}">
+                       <compilerarg value="-XDignore.symbol.file"/>
+                       <src path="tests" />
+                       <classpath refid="cacert-gigi.test.classpath" />
                </javac>
+               <concat destfile="bintest/org/cacert/gigi/util/effective_tld_names.dat">
+                       <path path="bin/org/cacert/gigi/util/effective_tld_names.dat"/>
+                       <path path="publicSuffixFooter.dat"/>
+               </concat>
        </target>
-       <target name="FetchLocales">
-               <java classname="org.cacert.gigi.util.FetchLocales" failonerror="true" fork="yes">
-                       <classpath refid="cacert-gigi.classpath"/>
+       <target name="check-locale">
+               <available file="locale/de.xml" property="locale.present" />
+       </target>
+       <target name="FetchLocales" depends="check-locale" unless="locale.present">
+               <java classname="org.cacert.gigi.util.FetchLocales" failonerror="true"
+                       fork="yes">
+                       <classpath refid="cacert-gigi.classpath" />
+                       <arg value="${localePath}"/>
                </java>
        </target>
        <target name="check-generateKeys">
-               <available file="config/keystore.pkcs12" property="keystore.present"/>
+               <available file="config/keystore.pkcs12" property="keystore.present" />
        </target>
        <target name="generateKeys" depends="check-generateKeys" unless="keystore.present">
-               <exec executable="./generateKeys.sh" dir="doc/scripts"/>
-               <exec executable="./generateTruststore.sh" dir="doc/scripts">
-                       <arg value="-noprompt"/>
+               <exec executable="./generateKeys.sh" dir="keys" />
+               <exec executable="./generateTruststore.sh" dir="keys">
+                       <arg value="-noprompt" />
                </exec>
        </target>
-       <target name="test" depends="build-project-test,generateKeys,FetchLocales,pack">
-               <mkdir dir="${junit.output.dir}"/>
-               <junit fork="yes" printsummary="withOutAndErr">
-                       <formatter type="xml"/>
+       <target name="reset-db" depends="build-project">
+               <copy file="config/test.properties" tofile="config/gigi.properties"/>
+               <java classname="org.cacert.gigi.util.DatabaseManager">
+                       <arg value="--test"/>
+                       <classpath refid="cacert-gigi.test.classpath" />
+               </java>
+       </target>
+       <target name="test" depends="build-project-test,FetchLocales,pack-testing,pack,reset-db">
+               <delete failonerror="false">
+                       <fileset dir=".">
+                               <include name="jacoco.exec"/>
+                               <include name="tester.exec"/>
+                       </fileset>
+               </delete>
+               <mkdir dir="${junit.output.dir}" />
+               <junit maxmemory="2g" fork="yes" printsummary="withOutAndErr">
+                       <jvmarg value="-javaagent:/usr/share/java/jacocoagent.jar=destfile=tester.exec"/>
+                       <jvmarg value="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:50001"/>
+                       <formatter type="xml" />
                        <batchtest fork="yes" todir="${junit.output.dir}">
                                <fileset dir="tests">
-                                       <include name="**/*.java"/>
-                                       <exclude name="**/testUtils/**"/>
+                                       <include name="**/*.java" />
+                                       <exclude name="**/testUtils/**" />
+                                       <exclude name="**/com/lambdaworks/crypto/test/CryptoTestUtil.java"/>
                                </fileset>
                        </batchtest>
-                       <classpath refid="cacert-gigi.test.classpath"/>
+                       <classpath refid="cacert-gigi.test.classpath" />
                </junit>
        </target>
        <target name="junitreport">
                <junitreport todir="${junit.output.dir}">
                        <fileset dir="${junit.output.dir}">
-                               <include name="TEST-*.xml"/>
+                               <include name="TEST-*.xml" />
                        </fileset>
-                       <report format="frames" todir="${junit.output.dir}"/>
+                       <report format="frames" todir="${junit.output.dir}" />
                </junitreport>
        </target>
+       <target name="generatecoco">
+               <delete file="merged.exec"/>
+
+               <jacoco:merge destfile="merged.exec">
+                       <fileset dir="." includes="*.exec"/>
+               </jacoco:merge>
+               <jacoco:report>
+                       <executiondata>
+                               <file file="merged.exec" />
+                       </executiondata>
+
+                       <structure name="CAcert gigi">
+                               <group name="Server">
+                                       <classfiles>
+                                               <fileset dir="bin">
+                                                       <include name="org/cacert/gigi/**"/>
+                                               </fileset>
+                                       </classfiles>
+                                       <sourcefiles encoding="UTF-8">
+                                               <fileset dir="src">
+                                                       <include name="org/cacert/gigi/**"/>
+                                               </fileset>
+                                       </sourcefiles>
+                               </group>
+                               <group name="Testcases">
+                                       <classfiles>
+                                               <fileset dir="bintest">
+                                                       <include name="org/cacert/gigi/**"/>
+                                               </fileset>
+                                       </classfiles>
+                                       <sourcefiles encoding="UTF-8">
+                                               <fileset dir="tests">
+                                                       <include name="org/cacert/gigi/**"/>
+                                               </fileset>
+                                       </sourcefiles>
+                               </group>
+                       </structure>
+
+                       <html destdir="cocoReport"/>
+
+               </jacoco:report>
+       </target>
+       <target name="install-native" depends="native">
+               <mkdir dir="${env.DESTDIR}/usr/lib/jni"/>
+               <copy file="natives/libsetuid.so" todir="${env.DESTDIR}/usr/lib/jni"/>
+       </target>
+       <target name="install" depends="install-common">
+               <mkdir dir="${env.DESTDIR}/usr/share/cacert-gigi/static" />
+               <copy todir="${env.DESTDIR}/usr/share/cacert-gigi/static">
+                       <fileset dir="static" />
+               </copy>
+               <delete file="${env.DESTDIR}/usr/share/cacert-gigi/static/static/images/cacert4-test.png" />
+               <copy file="gigi.jar" tofile="${env.DESTDIR}/usr/share/java/gigi.jar"/>
+       </target>
+       <target name="install-testing" depends="install-common">
+               <mkdir dir="${env.DESTDIR}/usr/share/cacert-gigi/static" />
+               <copy todir="${env.DESTDIR}/usr/share/cacert-gigi/static">
+                       <fileset dir="static" />
+               </copy>
+               <move file="${env.DESTDIR}/usr/share/cacert-gigi/static/static/images/cacert4-test.png" tofile="${env.DESTDIR}/usr/share/cacert-gigi/static/static/images/cacert4.png" />
+               <delete file="${env.DESTDIR}/usr/share/cacert-gigi/static/static/images/cacert4-test.png" />
+               <copy file="gigi-testing.jar" tofile="${env.DESTDIR}/usr/share/java/gigi.jar"/>
+       </target>
+
+       <target name="install-common" depends="pack">
+               <mkdir dir="${env.DESTDIR}/usr/share/java" />
+
+               <mkdir dir="${env.DESTDIR}/usr/bin"/>
+               <copy file="doc/scripts/gigi" tofile="${env.DESTDIR}/usr/bin/gigi"/>
+
+               <chmod file="${env.DESTDIR}/usr/bin/gigi" perm="+x"/>
+               <mkdir dir="${env.DESTDIR}/usr/share/dbconfig-common/data/${PACKAGE}/install/"/>
+               <copy file="src/org/cacert/gigi/database/tableStructure.sql" tofile="${env.DESTDIR}/usr/share/dbconfig-common/data/${PACKAGE}/install/mysql.sql"/>
+
+               <mkdir dir="${env.DESTDIR}/var/lib/cacert-gigi/doc"/>
+               <exec executable="ln">
+                       <arg value="-s"/>
+                       <arg value="/usr/share/dbconfig-common/data/${PACKAGE}/install/mysql.sql"/>
+                       <arg value="${env.DESTDIR}/var/lib/cacert-gigi/doc/tableStructure.sql"/>
+               </exec>
+               <mkdir dir="${env.DESTDIR}/var/lib/cacert-gigi/natives"/>
+               <delete failonerror="false" file="${env.DESTDIR}/var/lib/cacert-gigi/static"/>
+               <exec executable="ln">
+                       <arg value="-s"/>
+                       <arg value="/usr/share/cacert-gigi/static"/>
+                       <arg value="${env.DESTDIR}/var/lib/cacert-gigi/static"/>
+               </exec>
+               <exec executable="ln">
+                       <arg value="-s"/>
+                       <arg value="/usr/lib/jni/libsetuid.so"/>
+                       <arg value="${env.DESTDIR}/var/lib/cacert-gigi/natives/libsetuid.so"/>
+               </exec>
+
+               <mkdir dir="${env.DESTDIR}/etc/cacert/gigi"/>
+               <!--<copy todir="${env.DESTDIR}/DEBIAN">
+                       <fileset dir="debian">
+                       </fileset>
+               </copy>-->
+       </target>
+       <target name="update-effective-tlds">
+               <mkdir dir="bin/org/cacert/gigi/util"/>
+               <exec executable="wget" dir="bin/org/cacert/gigi/util">
+                       <arg value="-N"/>
+                       <arg value="-q"/>
+                       <arg value="https://publicsuffix.org/list/effective_tld_names.dat"/>
+               </exec>
+       </target>
 </project>
index 59f2d874c85ff395ee0ac3fb4393ebe95add78f9..8bec870d457826870bc7e8b1754369c322353966 100644 (file)
@@ -3,3 +3,6 @@ keystore.pkcs12
 cacerts.jks
 gigi.properties
 test.properties
+/profiles
+/ca
+/keys
diff --git a/config/generateTruststoreNRE.sh b/config/generateTruststoreNRE.sh
new file mode 100755 (executable)
index 0000000..69a76be
--- /dev/null
@@ -0,0 +1,30 @@
+#!/bin/bash
+# this script imports the cacert root certs
+
+rm -f cacerts.jks
+
+function import(){
+  name=$1
+  keytool -importcert -keystore ../config/cacerts.jks -file "$1.crt" -alias own -storepass "changeit" -alias "$(basename $name)" $2
+}
+
+function importP(){
+ keytool -importkeystore -srckeystore "$1" -noprompt -destkeystore keystore.pkcs12 -srcstoretype pkcs12 -deststoretype pkcs12 -deststorepass changeit -srcstorepass changeit
+}
+
+import ca/root -noprompt
+import ca/assured
+import ca/unassured
+import ca/orga
+import ca/orgaSign
+import ca/codesign
+
+for i in ca/*_*_*; do
+  import ${i%.crt}
+done
+
+for i in ../keys/*.pkcs12; do
+  importP $i
+done
+
+keytool -list -keystore ../config/cacerts.jks -storepass "changeit"
index 474d6771e48551ee92608cc1dadc5631773d997a..6df75cb614b247d72e074bf935539e9276227381 100644 (file)
@@ -1,8 +1,16 @@
 host=127.0.0.1
-port=443
+name.static=static.cacert.local
+name.secure=secure.cacert.local
+name.www=www.cacert.local
+name.api=api.cacert.local
+
+https.port=443
+http.port=80
 #emailProvider=org.cacert.gigi.email.Sendmail
 emailProvider=org.cacert.gigi.email.CommandlineEmailProvider
-sql.driver=com.mysql.jdbc.Driver
-sql.url=jdbc:mysql://
+sql.driver=org.postgresql.Driver
+#sql.driver=com.mysql.jdbc.Driver
+sql.url=jdbc:postgresql://localhost/cacert
+#sql.url=jdbc:mysql://localhost:3306/cacert
 sql.user=
 sql.password=
index 1bd8a5845f01f229d14613b7e1448d4296a0606b..f18f895b3866d78ea678e4836b76fc71a9c6c6eb 100644 (file)
@@ -1,18 +1,40 @@
 type=local
-server=localhost:443
+serverPort.https=443
+serverPort.http=80
 mail=localhost:8474
 
 # ==== OR ===
 type=autonomous
-java=java -cp bin;/path/to/mysqlConnector.jar org.cacert.gigi.Launcher
-serverPort=4443
+java=java -cp bin;/path/to/mysqlConnector.jar org.cacert.gigi.TestLauncher
+serverPort.https=4443
+serverPort.http=8098
 mailPort=8473
 
 
 
 
 # ==== ALL ===
-sql.driver=com.mysql.jdbc.Driver
-sql.url=jdbc:mysql://localhost:3306/cacert
+name.static=static.cacert.local
+name.secure=secure.cacert.local
+name.www=www.cacert.local
+name.api=api.cacert.local
+sql.driver=org.postgresql.Driver
+#sql.driver=com.mysql.jdbc.Driver
+sql.url=jdbc:postgresql://localhost/cacert
+#sql.url=jdbc:mysql://localhost:3306/cacert
 sql.user=cacert
 sql.password=<password>
+
+
+domain.manage=http://you-installation-of-the/index.php
+domain.http=you-intstallation-for-the-textfiles
+domain.dnstest=the.dns.zone
+domain.testns=the.authorativ.ns.for.domain.dnstest
+domain.local=a.domain.that.resolves.to.localhost
+
+
+email.address=somemail@yourdomain.org
+email.password=somemails-imap-password
+email.imap=imap.yourdomain.org
+email.imap.user=somemail-imap-useraccount
+email.non-address=some-non-existent-domain@yourdomain.org
diff --git a/debian/.gitignore b/debian/.gitignore
new file mode 100644 (file)
index 0000000..5a7dbaa
--- /dev/null
@@ -0,0 +1,6 @@
+*.debhelper
+*.substvars
+*.log
+cacert-gigi-setuid
+files
+cacert-gigi
diff --git a/debian/cacert-gigi-testing.cacert-gigi-signer.default b/debian/cacert-gigi-testing.cacert-gigi-signer.default
new file mode 100644 (file)
index 0000000..436a32a
--- /dev/null
@@ -0,0 +1 @@
+START_DAEMON=0
diff --git a/debian/cacert-gigi-testing.cacert-gigi-signer.init b/debian/cacert-gigi-testing.cacert-gigi-signer.init
new file mode 100644 (file)
index 0000000..2519417
--- /dev/null
@@ -0,0 +1,175 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides:          cacert-gigi-signer
+# Required-Start:    $local_fs $network $remote_fs $syslog postgresql
+# Required-Stop:     $local_fs $network $remote_fs $syslog postgresql
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: <Enter a short description of the software>
+# Description:       <Enter a long description of the software>
+#                    <...>
+#                    <...>
+### END INIT INFO
+
+# Author: unknown <software@cacert.org>
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="cacert-gigi-signer"
+NAME=cacert-gigi-signer
+DAEMON=`which java`
+DAEMON_ARGS="-cp /usr/share/java/postgresql-jdbc4.jar:/usr/share/java/gigi.jar org.cacert.gigi.util.SimpleSigner"
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+DIR=/var/lib/cacert-gigi
+
+# Exit if the package is not installed
+[ -r "/usr/share/java/gigi.jar" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+if [ "$START_DAEMON" = "0" ]; then
+    echo "Not starting $NAME (as configured in /etc/default/$NAME)";
+    exit 0;
+fi
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+       if [ ! -f /var/lib/cacert-gigi/config/gigi.properties ]; then
+               echo Missing signer-configfile
+               return 2
+       fi
+       # Return
+       #   0 if daemon has been started
+       #   1 if daemon was already running
+       #   2 if daemon could not be started
+       start-stop-daemon --start --quiet --pidfile $PIDFILE -d $DIR --startas $DAEMON --test > /dev/null \
+               || return 1
+       start-stop-daemon -b --start --quiet --pidfile $PIDFILE --make-pidfile -d $DIR --startas $DAEMON -- \
+               $DAEMON_ARGS \
+               || return 2
+       # The above code will not work for interpreted scripts, use the next
+       # six lines below instead (Ref: #643337, start-stop-daemon(8) )
+       #start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME --test > /dev/null \
+       #       || return 1
+       #start-stop-daemon -b --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME -- $DAEMON_ARGS \
+       #       || return 2
+
+       # Add code here, if necessary, that waits for the process to be ready
+       # to handle requests from services started subsequently which depend
+       # on this one.  As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+       # Return
+       #   0 if daemon has been stopped
+       #   1 if daemon was already stopped
+       #   2 if daemon could not be stopped
+       #   other if a failure occurred
+       start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
+       RETVAL="$?"
+       [ "$RETVAL" = 2 ] && return 2
+       # Wait for children to finish too if this is a daemon that forks
+       # and if the daemon is only ever run from this initscript.
+       # If the above conditions are not satisfied then add some other code
+       # that waits for the process to drop all resources that could be
+       # needed by services started subsequently.  A last resort is to
+       # sleep for some time.
+       start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --pidfile $PIDFILE
+       [ "$?" = 2 ] && return 2
+       # Many daemons don't delete their pidfiles when they exit.
+       rm -f $PIDFILE
+       return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+       #
+       # If the daemon can reload its configuration without
+       # restarting (for example, when it is sent a SIGHUP),
+       # then implement that here.
+       #
+       start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+       return 0
+}
+
+case "$1" in
+  start)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+       do_start
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  stop)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+       do_stop
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  #reload|force-reload)
+       #
+       # If do_reload() is not implemented then leave this commented out
+       # and leave 'force-reload' as an alias for 'restart'.
+       #
+       #log_daemon_msg "Reloading $DESC" "$NAME"
+       #do_reload
+       #log_end_msg $?
+       #;;
+  restart|force-reload)
+       #
+       # If the "reload" option is implemented then remove the
+       # 'force-reload' alias
+       #
+       log_daemon_msg "Restarting $DESC" "$NAME"
+       do_stop
+       case "$?" in
+         0|1)
+               do_start
+               case "$?" in
+                       0) log_end_msg 0 ;;
+                       1) log_end_msg 1 ;; # Old process is still running
+                       *) log_end_msg 1 ;; # Failed to start
+               esac
+               ;;
+         *)
+               # Failed to stop
+               log_end_msg 1
+               ;;
+       esac
+       ;;
+  *)
+       #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+       echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+       exit 3
+       ;;
+esac
+
+:
diff --git a/debian/cacert-gigi-testing.cacert-gigi.default b/debian/cacert-gigi-testing.cacert-gigi.default
new file mode 100644 (file)
index 0000000..436a32a
--- /dev/null
@@ -0,0 +1 @@
+START_DAEMON=0
diff --git a/debian/cacert-gigi-testing.cacert-gigi.init b/debian/cacert-gigi-testing.cacert-gigi.init
new file mode 100644 (file)
index 0000000..d22dbbf
--- /dev/null
@@ -0,0 +1,175 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides:          cacert-gigi
+# Required-Start:    $local_fs $network $remote_fs $syslog postgresql
+# Required-Stop:     $local_fs $network $remote_fs $syslog postgresql
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: <Enter a short description of the software>
+# Description:       <Enter a long description of the software>
+#                    <...>
+#                    <...>
+### END INIT INFO
+
+# Author: unknown <software@cacert.org>
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="cacert-gigi"
+NAME=cacert-gigi
+DAEMON=`which java`
+DAEMON_ARGS="-cp /usr/share/java/postgresql-jdbc4.jar:/usr/share/java/gigi.jar org.cacert.gigi.Launcher"
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+DIR=/var/lib/cacert-gigi
+
+# Exit if the package is not installed
+[ -r "/usr/share/java/gigi.jar" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+if [ "$START_DAEMON" = "0" ]; then
+    echo "Not starting $NAME (as configured in /etc/default/$NAME)";
+    exit 0;
+fi
+
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+       if [ ! -f /etc/cacert/gigi/conf.tar ]; then
+               echo Missing gigi-configfile
+               exit 2
+       fi
+       # Return
+       #   0 if daemon has been started
+       #   1 if daemon was already running
+       #   2 if daemon could not be started
+       start-stop-daemon --start --quiet --pidfile $PIDFILE -d $DIR --exec $DAEMON --test > /dev/null \
+               || return 1
+       start-stop-daemon -b --start --quiet --pidfile $PIDFILE -d $DIR --exec /usr/bin/gigi -- start-daemon \
+               || return 2
+       # The above code will not work for interpreted scripts, use the next
+       # six lines below instead (Ref: #643337, start-stop-daemon(8) )
+       # start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME --test > /dev/null \
+       #       || return 1
+       # start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME -- $DAEMON_ARGS \
+       #       || return 2
+
+       # Add code here, if necessary, that waits for the process to be ready
+       # to handle requests from services started subsequently which depend
+       # on this one.  As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+       # Return
+       #   0 if daemon has been stopped
+       #   1 if daemon was already stopped
+       #   2 if daemon could not be stopped
+       #   other if a failure occurred
+       start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
+       RETVAL="$?"
+       [ "$RETVAL" = 2 ] && return 2
+       # Wait for children to finish too if this is a daemon that forks
+       # and if the daemon is only ever run from this initscript.
+       # If the above conditions are not satisfied then add some other code
+       # that waits for the process to drop all resources that could be
+       # needed by services started subsequently.  A last resort is to
+       # sleep for some time.
+       start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --pidfile $PIDFILE
+       [ "$?" = 2 ] && return 2
+       # Many daemons don't delete their pidfiles when they exit.
+       rm -f $PIDFILE
+       return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+       #
+       # If the daemon can reload its configuration without
+       # restarting (for example, when it is sent a SIGHUP),
+       # then implement that here.
+       #
+       start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+       return 0
+}
+
+case "$1" in
+  start)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+       do_start
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  stop)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+       do_stop
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  #reload|force-reload)
+       #
+       # If do_reload() is not implemented then leave this commented out
+       # and leave 'force-reload' as an alias for 'restart'.
+       #
+       #log_daemon_msg "Reloading $DESC" "$NAME"
+       #do_reload
+       #log_end_msg $?
+       #;;
+  restart|force-reload)
+       #
+       # If the "reload" option is implemented then remove the
+       # 'force-reload' alias
+       #
+       log_daemon_msg "Restarting $DESC" "$NAME"
+       do_stop
+       case "$?" in
+         0|1)
+               do_start
+               case "$?" in
+                       0) log_end_msg 0 ;;
+                       1) log_end_msg 1 ;; # Old process is still running
+                       *) log_end_msg 1 ;; # Failed to start
+               esac
+               ;;
+         *)
+               # Failed to stop
+               log_end_msg 1
+               ;;
+       esac
+       ;;
+  *)
+       #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+       echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+       exit 3
+       ;;
+esac
+
+:
diff --git a/debian/cacert-gigi-testing.docs b/debian/cacert-gigi-testing.docs
new file mode 100644 (file)
index 0000000..b43bf86
--- /dev/null
@@ -0,0 +1 @@
+README.md
diff --git a/debian/cacert-gigi-testing.manpages b/debian/cacert-gigi-testing.manpages
new file mode 100644 (file)
index 0000000..3de344b
--- /dev/null
@@ -0,0 +1 @@
+debian/gigi.1
diff --git a/debian/cacert-gigi-testing.postinst b/debian/cacert-gigi-testing.postinst
new file mode 100644 (file)
index 0000000..3a7ec74
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+# postinst script for cacert-gigi
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <postinst> `configure' <most-recently-configured-version>
+#        * <old-postinst> `abort-upgrade' <new version>
+#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+#          <new-version>
+#        * <postinst> `abort-remove'
+#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+#          <failed-install-package> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    configure)
+    ;;
+
+    abort-upgrade|abort-remove|abort-deconfigure)
+    ;;
+
+    *)
+        echo "postinst called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+if [ -f /usr/share/debconf/confmodule ]; then
+    . /usr/share/debconf/confmodule
+fi
+
+gigi fetch-locales
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/cacert-gigi.cacert-gigi-signer.init b/debian/cacert-gigi.cacert-gigi-signer.init
new file mode 100644 (file)
index 0000000..21c7296
--- /dev/null
@@ -0,0 +1,171 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides:          cacert-gigi-signer
+# Required-Start:    $local_fs $network $remote_fs $syslog postgresql
+# Required-Stop:     $local_fs $network $remote_fs $syslog postgresql
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: <Enter a short description of the software>
+# Description:       <Enter a long description of the software>
+#                    <...>
+#                    <...>
+### END INIT INFO
+
+# Author: unknown <software@cacert.org>
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="cacert-gigi-signer"
+NAME=cacert-gigi-signer
+DAEMON=`which java`
+DAEMON_ARGS="-cp /usr/share/java/postgresql-jdbc4.jar:/usr/share/java/gigi.jar org.cacert.gigi.util.SimpleSigner"
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+DIR=/var/lib/cacert-gigi
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+        if [ ! -f /var/lib/cacert-gigi/config/gigi.properties ]; then
+                echo Missing signer-configfile
+                return 0
+        fi
+       # Return
+       #   0 if daemon has been started
+       #   1 if daemon was already running
+       #   2 if daemon could not be started
+       start-stop-daemon --start --quiet --pidfile $PIDFILE -d $DIR --startas $DAEMON --test > /dev/null \
+               || return 1
+       start-stop-daemon -b --start --quiet --pidfile $PIDFILE --make-pidfile -d $DIR --startas $DAEMON -- \
+               $DAEMON_ARGS \
+               || return 2
+       # The above code will not work for interpreted scripts, use the next
+       # six lines below instead (Ref: #643337, start-stop-daemon(8) )
+       #start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME --test > /dev/null \
+       #       || return 1
+       #start-stop-daemon -b --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME -- $DAEMON_ARGS \
+       #       || return 2
+
+       # Add code here, if necessary, that waits for the process to be ready
+       # to handle requests from services started subsequently which depend
+       # on this one.  As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+       # Return
+       #   0 if daemon has been stopped
+       #   1 if daemon was already stopped
+       #   2 if daemon could not be stopped
+       #   other if a failure occurred
+       start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
+       RETVAL="$?"
+       [ "$RETVAL" = 2 ] && return 2
+       # Wait for children to finish too if this is a daemon that forks
+       # and if the daemon is only ever run from this initscript.
+       # If the above conditions are not satisfied then add some other code
+       # that waits for the process to drop all resources that could be
+       # needed by services started subsequently.  A last resort is to
+       # sleep for some time.
+       start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --pidfile $PIDFILE
+       [ "$?" = 2 ] && return 2
+       # Many daemons don't delete their pidfiles when they exit.
+       rm -f $PIDFILE
+       return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+       #
+       # If the daemon can reload its configuration without
+       # restarting (for example, when it is sent a SIGHUP),
+       # then implement that here.
+       #
+       start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+       return 0
+}
+
+case "$1" in
+  start)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+       do_start
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  stop)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+       do_stop
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  #reload|force-reload)
+       #
+       # If do_reload() is not implemented then leave this commented out
+       # and leave 'force-reload' as an alias for 'restart'.
+       #
+       #log_daemon_msg "Reloading $DESC" "$NAME"
+       #do_reload
+       #log_end_msg $?
+       #;;
+  restart|force-reload)
+       #
+       # If the "reload" option is implemented then remove the
+       # 'force-reload' alias
+       #
+       log_daemon_msg "Restarting $DESC" "$NAME"
+       do_stop
+       case "$?" in
+         0|1)
+               do_start
+               case "$?" in
+                       0) log_end_msg 0 ;;
+                       1) log_end_msg 1 ;; # Old process is still running
+                       *) log_end_msg 1 ;; # Failed to start
+               esac
+               ;;
+         *)
+               # Failed to stop
+               log_end_msg 1
+               ;;
+       esac
+       ;;
+  *)
+       #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+       echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+       exit 3
+       ;;
+esac
+
+:
diff --git a/debian/cacert-gigi.cacert-gigi.init b/debian/cacert-gigi.cacert-gigi.init
new file mode 100644 (file)
index 0000000..8d81a74
--- /dev/null
@@ -0,0 +1,170 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides:          cacert-gigi
+# Required-Start:    $local_fs $network $remote_fs $syslog postgresql
+# Required-Stop:     $local_fs $network $remote_fs $syslog postgresql
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: <Enter a short description of the software>
+# Description:       <Enter a long description of the software>
+#                    <...>
+#                    <...>
+### END INIT INFO
+
+# Author: unknown <software@cacert.org>
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="cacert-gigi"
+NAME=cacert-gigi
+DAEMON=`which java`
+DAEMON_ARGS="-cp /usr/share/java/postgresql-jdbc4.jar:/usr/share/java/gigi.jar org.cacert.gigi.Launcher"
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+DIR=/var/lib/cacert-gigi
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+       if [ ! -f /etc/cacert-gigi/conf.tar ]; then
+               echo Missing gigi-configfile
+                exit 0
+        fi
+       # Return
+       #   0 if daemon has been started
+       #   1 if daemon was already running
+       #   2 if daemon could not be started
+        start-stop-daemon --start --quiet --pidfile $PIDFILE -d $DIR --exec $DAEMON --test > /dev/null \
+               || return 1
+       start-stop-daemon -b --start --quiet --pidfile $PIDFILE -d $DIR --exec /usr/bin/gigi -- start-daemon \
+               || return 2
+       # The above code will not work for interpreted scripts, use the next
+       # six lines below instead (Ref: #643337, start-stop-daemon(8) )
+       # start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME --test > /dev/null \
+       #       || return 1
+       # start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \
+       #       --name $NAME -- $DAEMON_ARGS \
+       #       || return 2
+
+       # Add code here, if necessary, that waits for the process to be ready
+       # to handle requests from services started subsequently which depend
+       # on this one.  As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+       # Return
+       #   0 if daemon has been stopped
+       #   1 if daemon was already stopped
+       #   2 if daemon could not be stopped
+       #   other if a failure occurred
+       start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
+       RETVAL="$?"
+       [ "$RETVAL" = 2 ] && return 2
+       # Wait for children to finish too if this is a daemon that forks
+       # and if the daemon is only ever run from this initscript.
+       # If the above conditions are not satisfied then add some other code
+       # that waits for the process to drop all resources that could be
+       # needed by services started subsequently.  A last resort is to
+       # sleep for some time.
+       start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --pidfile $PIDFILE
+       [ "$?" = 2 ] && return 2
+       # Many daemons don't delete their pidfiles when they exit.
+       rm -f $PIDFILE
+       return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+       #
+       # If the daemon can reload its configuration without
+       # restarting (for example, when it is sent a SIGHUP),
+       # then implement that here.
+       #
+       start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+       return 0
+}
+
+case "$1" in
+  start)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+       do_start
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  stop)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+       do_stop
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  #reload|force-reload)
+       #
+       # If do_reload() is not implemented then leave this commented out
+       # and leave 'force-reload' as an alias for 'restart'.
+       #
+       #log_daemon_msg "Reloading $DESC" "$NAME"
+       #do_reload
+       #log_end_msg $?
+       #;;
+  restart|force-reload)
+       #
+       # If the "reload" option is implemented then remove the
+       # 'force-reload' alias
+       #
+       log_daemon_msg "Restarting $DESC" "$NAME"
+       do_stop
+       case "$?" in
+         0|1)
+               do_start
+               case "$?" in
+                       0) log_end_msg 0 ;;
+                       1) log_end_msg 1 ;; # Old process is still running
+                       *) log_end_msg 1 ;; # Failed to start
+               esac
+               ;;
+         *)
+               # Failed to stop
+               log_end_msg 1
+               ;;
+       esac
+       ;;
+  *)
+       #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+       echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+       exit 3
+       ;;
+esac
+
+:
diff --git a/debian/cacert-gigi.docs b/debian/cacert-gigi.docs
new file mode 100644 (file)
index 0000000..b43bf86
--- /dev/null
@@ -0,0 +1 @@
+README.md
diff --git a/debian/cacert-gigi.manpages b/debian/cacert-gigi.manpages
new file mode 100644 (file)
index 0000000..3de344b
--- /dev/null
@@ -0,0 +1 @@
+debian/gigi.1
diff --git a/debian/cacert-gigi.postinst b/debian/cacert-gigi.postinst
new file mode 100644 (file)
index 0000000..3a7ec74
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+# postinst script for cacert-gigi
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <postinst> `configure' <most-recently-configured-version>
+#        * <old-postinst> `abort-upgrade' <new version>
+#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+#          <new-version>
+#        * <postinst> `abort-remove'
+#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+#          <failed-install-package> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    configure)
+    ;;
+
+    abort-upgrade|abort-remove|abort-deconfigure)
+    ;;
+
+    *)
+        echo "postinst called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+if [ -f /usr/share/debconf/confmodule ]; then
+    . /usr/share/debconf/confmodule
+fi
+
+gigi fetch-locales
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..cc57cff
--- /dev/null
@@ -0,0 +1,5 @@
+cacert-gigi (0.1) unstable; urgency=low
+
+  * Initial Release
+
+ -- CAcert Software Team <cacert-devel@cacert.org>  Thu, 25 Sep 2014 03:19:20 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644 (file)
index 0000000..ec63514
--- /dev/null
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..c593712
--- /dev/null
@@ -0,0 +1,29 @@
+Source: cacert-gigi
+Section: java
+Priority: optional
+Maintainer: unknown <software@cacert.org>
+Build-Depends: debhelper (>= 8.0.0), openjdk-8-jdk-gigi
+Standards-Version: 3.9.4
+Homepage: http://cacert.org
+#Vcs-Git: git://git.debian.org/collab-maint/cacert-gigi.git
+#Vcs-Browser: http://git.debian.org/?p=collab-maint/cacert-gigi.git;a=summary
+
+Package: cacert-gigi
+Architecture: all
+Depends: openjdk-8-jdk-gigi, cacert-gigi-setuid, libpostgresql-jdbc-java, ${shlibs:Depends}, ${misc:Depends}
+Conflicts: cacert-gigi-testing
+Description: CAcert Web-DB software.
+ This program is used to manage accounts and certificates.
+
+Package: cacert-gigi-testing
+Architecture: all
+Depends:  openjdk-8-jdk-gigi, cacert-gigi-setuid, libpostgresql-jdbc-java, ${shlibs:Depends}, ${misc:Depends}
+Conflicts: cacert-gigi
+Description: CAcert Web-DB software testing version.
+ This program is the release to the testing server.
+
+Package: cacert-gigi-setuid
+Architecture: any
+Depends: openjdk-8-jdk-gigi, ${shlibs:Depends}, ${misc:Depends}
+Description: CAcert Web-DB software's setuid native library.
+ It is used to drop privilleges after allocating ports.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..c454948
--- /dev/null
@@ -0,0 +1,65 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: gigi
+Source: <https://github.com/CAcertOrg/cacert-gigi>
+
+Files: *
+Copyright: 2014 CAcert Software Team <cacert-devel@cacert.org>
+License: GPL-2.0
+
+Files: debian/*
+Copyright: 2014 CAcert Software Team <cacert-devel@cacert.org>
+License: GPL-2.0
+
+Files: src/org/cacert/gigi/output/template
+Copyright: 2015 CAcert Software Team <cacert-devel@cacert.org>
+License: GPL-2.0 or BSD
+
+Files: src/org/cacert/gigi/localisation
+Copyright: 2015 CAcert Software Team <cacert-devel@cacert.org>
+License: GPL-2.0 or BSD
+
+Files: src/org/cacert/gigi/database
+Copyright: 2015 CAcert Software Team <cacert-devel@cacert.org>
+License: GPL-2.0 or BSD
+
+License: BSD
+ Copyright (c) 2015, CAcert
+ All rights reserved.
+ .
+ Redistribution and use in source and binary forms, with or without 
+ modification, are permitted provided that the following conditions are met:
+ .
+ 1. Redistributions of source code must retain the above copyright notice, 
+ this list of conditions and the following disclaimer.
+ .
+ 2. Redistributions in binary form must reproduce the above copyright notice, 
+ this list of conditions and the following disclaimer in the documentation 
+ and/or other materials provided with the distribution.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
+ THE POSSIBILITY OF SUCH DAMAGE.
+
+License: GPL-2.0
+ This package is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; only version 2 of the License.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
diff --git a/debian/gigi.1 b/debian/gigi.1
new file mode 100644 (file)
index 0000000..61b41b6
--- /dev/null
@@ -0,0 +1,49 @@
+.\"                                      Hey, EMACS: -*- nroff -*-
+.\" (C) Copyright 2014 CAcert Software Team <software@cacert.org>,
+.\"
+.TH CACERT-GIGI 1 "September 25, 2014"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh        disable hyphenation
+.\" .hy        enable hyphenation
+.\" .ad l      left justify
+.\" .ad b      justify to both left and right margins
+.\" .nf        disable filling
+.\" .fi        enable filling
+.\" .br        insert line break
+.\" .sp <n>    insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.SH NAME
+cacert-gigi \- a starter for the CAcert-gigi system
+.SH SYNOPSIS
+.B cacert-gigi
+.RI {start|signer|reset-database|fetch-locales}
+.SH DESCRIPTION
+.B cacert-gigi
+is the starter for the CAcert-gigi system.
+.\" TeX users may be more comfortable with the \fB<whatever>\fP and
+.\" \fI<whatever>\fP escape sequences to invode bold face and italics,
+.\" respectively.
+.SH OPTIONS
+.TP
+.B debug
+Run the usual webdb (not forking) with opening jdwp on port 8000. You will need to pipe the config into this program.
+.TP
+.B fetch-locales
+Fetch all Translations from http://translations.cacert.org/
+.TP
+.B reset-database
+Delete the whole database contents, resetting it to default.
+.TP
+.B signer
+Run the test-replacement signer (not forking).
+.TP
+.B signer-conf
+Configure the (internal) signer and the "reset-database"-tool with the config from stdin.
+.TP
+.B start
+Run the usual webdb (not forking). You will need to pipe the config into this program.
+.TP
+.B start-daemon
+Run the usual webdb (forking). You will not need to pipe the config into this program. It reads the config from /etc/cacert/gigi/conf.tar
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..f0e56fb
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+%:
+       dh $@ 
+
+build:
+       ant pack
+
+override_dh_auto_clean:
+       echo i dont clean
+
+override_dh_installinit:
+       dh_installinit --name=cacert-gigi
+       dh_installinit --name=cacert-gigi-signer
+
+override_dh_auto_build: build
+
+override_dh_auto_install:
+       DESTDIR=debian/cacert-gigi ant install
+       DESTDIR=debian/cacert-gigi-testing ant install-testing
+       DESTDIR=debian/cacert-gigi-setuid ant install-native
+
diff --git a/debian/source/format b/debian/source/format
new file mode 100644 (file)
index 0000000..89ae9db
--- /dev/null
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/doc/TemplateSyntax.txt b/doc/TemplateSyntax.txt
new file mode 100644 (file)
index 0000000..ad8e09a
--- /dev/null
@@ -0,0 +1,29 @@
+A template is constructed from a charstream. Everything that is not in "<?" to "?>" will be outputted directly. Text in these markers will be interpreted is template scripting syntax. The following strings are valid:
+
+General remarks:
+- $variablename: a variablename matches the regex [a-zA-Z0-9_-]
+Syntax:
+- <?=$variablename?> will output "variablename".
+   if "variablename" is an Outputable output this thing recursively.
+   else turn it into a String (Object.toString()) and output it.
+
+- <?=$!variablename?> will output the variable "variablename" but not HTML-escaped
+   
+- <?=_This is free Text.?> will translate "This is free Text." into the users language, (escaped) and output it.
+       Text may not contain "?>".
+       If the text contains "$" or "!'" it is interpreted as "advanced replacement".
+          - ${variablename} is interpreted as "output this variable at this point"
+          - !'literal content' output "literal content" here and do not translate or escape. (literal content may not contain any of: {}'$   )
+       Then the whole text than also may not contain "{" and "}".
+
+- <? if($variable) { ?> ... <? } ?>
+  Output/execute the text until "<? } ?>" only if $variable is Boolean.TRUE (<=> !Boolean.FALSE) or not null. 
+- <? if(...) { ?> ... <? } else { ?> ... <? } ?>
+
+- <? foreach($variable) { ?> ... <? } ?>
+  If $variable is an "IterableDataset"
+  Output/execute the text until "<? } ?>" repeated as $variable suggests. 
+  Special variables that $variable defines can be used in the inner text.
+  
+  
\ No newline at end of file
index bfa3f39e591dc4b464f84593977c9a166c19ade2..809d70e7d2500d5385f4a681134b942e8ebb04b9 100644 (file)
@@ -1,10 +1,15 @@
 Before you start using you might want to:
 
-- create a keypair for the server (scripts/generateKeys.sh)
-- create a truststore for the server (scripts/generateTruststore.sh)
+- use cacert-nre to generate a roots structure
+- extract the gigi-*.tar.gz file into "config"
+- execute "generateTruststore.sh" there
+- extract the signer-server-*.tar.gz into "signer"
 
 - download locales (util/ org.cacert.gigi.util.FetchLocales)
 - write your sql connection properties: config/gigi.properties.template -> config/gigi.properties
+- install "hosts" entries for the hosts you entered in "gigi.properties"
+   (be aware if you change the default ones you need to change the CN given in the certificates)
+
 - add the corresponding jdbc connector to your path.
 
 - on unix-like systems: to securely run on privileged ports <= 1024 build the native setuid library (run the makefile in natives/).
diff --git a/doc/exoticKeys.sh b/doc/exoticKeys.sh
new file mode 100644 (file)
index 0000000..caa4c76
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/bash
+openssl ecparam -out ec.pem -name secp521r1 -genkey
+openssl req -new -key ec.pem -out ec.csr -subj "/CN=bla"
+
+openssl dsaparam -genkey 1024 -out dsa.pem
+openssl req -new -key dsa.pem -out dsa.csr -subj "/CN=bla"
+
index c5651bf0f7dd44b67d7e9f21179eef86edf47b47..ec8e3372b453b78d2578b49472d49baef3c601f0 100644 (file)
@@ -1,5 +1,4 @@
-/path/to/mysql-connector.jar
-a Path to the mysql-jdbc-connector
+you need the debian mysql-connector package (jar under /usr/share/java/mysql-connector.jar)
 
 <yourSqlPassword>
 a Password to the sql database to test in.
@@ -8,3 +7,6 @@ a Password to the sql database to test in.
 folder Containing:
 - junit.jar
 - org.hamcrest.core.jar
+
+
+Fill also all other variables that are marked with "$$$$"
diff --git a/doc/jenkinsJob/ci-tests-setup.txt b/doc/jenkinsJob/ci-tests-setup.txt
new file mode 100644 (file)
index 0000000..e51c280
--- /dev/null
@@ -0,0 +1,16 @@
+-you need 4 domains resolving to the ci server (or localhost)
+preferably
+static.DOMAIN, secure.DOMAIN, www.DOMAIN and api.DOMAIN.
+enter them in the jenkins job to write them to "keys/config" and "config/test.properties"
+
+-you need credentials to an acessabible mysql database.
+make jenkins write them to "config/test.properties"
+
+-you need a dynamically managable dns zone.
+Write the zone name to "domain.dnstest" in "test.properties"
+and a manage script (see dyn-txt.php). 
+- Put the url with password in "domain.manage"
+- Put the host with password in "domain.http"
+
+Setup with bind9:
+dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST example.org.
index 61a2e0cb8ed2afa6ebee981858923735c3156860..505a7f132232b3c8dd9825a4847f72548919371f 100644 (file)
@@ -2,33 +2,44 @@
 <project>
   <actions/>
   <description></description>
+  <logRotator class="hudson.tasks.LogRotator">
+    <daysToKeep>-1</daysToKeep>
+    <numToKeep>100</numToKeep>
+    <artifactDaysToKeep>-1</artifactDaysToKeep>
+    <artifactNumToKeep>-1</artifactNumToKeep>
+  </logRotator>
   <keepDependencies>false</keepDependencies>
   <properties>
-    <hudson.security.AuthorizationMatrixProperty>
-      <permission>hudson.model.Item.Read:anonymous</permission>
-    </hudson.security.AuthorizationMatrixProperty>
     <hudson.model.ParametersDefinitionProperty>
       <parameterDefinitions>
         <hudson.model.TextParameterDefinition>
           <name>JAVA_HOME</name>
           <description></description>
-          <defaultValue>/usr/lib/jvm/java-8-openjdk/</defaultValue>
+          <defaultValue>/usr/lib/jvm/openjdk-8-jdk-gigi</defaultValue>
         </hudson.model.TextParameterDefinition>
         <hudson.model.TextParameterDefinition>
           <name>BRANCH</name>
           <description>The branch to build from.</description>
           <defaultValue>master</defaultValue>
         </hudson.model.TextParameterDefinition>
+        <hudson.model.ChoiceParameterDefinition>
+          <name>TARGET</name>
+          <description>The target.</description>
+          <choices class="java.util.Arrays$ArrayList">
+            <a class="string-array">
+              <string>develop</string>
+              <string>release</string>
+            </a>
+          </choices>
+        </hudson.model.ChoiceParameterDefinition>
       </parameterDefinitions>
     </hudson.model.ParametersDefinitionProperty>
   </properties>
-  <scm class="hudson.plugins.git.GitSCM" plugin="git@1.5.0">
+  <scm class="hudson.plugins.git.GitSCM" plugin="git@2.2.5">
     <configVersion>2</configVersion>
     <userRemoteConfigs>
       <hudson.plugins.git.UserRemoteConfig>
-        <name></name>
-        <refspec></refspec>
-        <url>https://github.com/yellowant/cacert-gigi.git</url>
+        <url>$$$$YOUR_REFERENCE_GIT_REPO$$$$</url>
       </hudson.plugins.git.UserRemoteConfig>
     </userRemoteConfigs>
     <branches>
         <name>$BRANCH</name>
       </hudson.plugins.git.BranchSpec>
     </branches>
-    <disableSubmodules>false</disableSubmodules>
-    <recursiveSubmodules>false</recursiveSubmodules>
     <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
-    <authorOrCommitter>false</authorOrCommitter>
-    <clean>false</clean>
-    <wipeOutWorkspace>false</wipeOutWorkspace>
-    <pruneBranches>false</pruneBranches>
-    <remotePoll>false</remotePoll>
-    <ignoreNotifyCommit>false</ignoreNotifyCommit>
-    <useShallowClone>false</useShallowClone>
-    <buildChooser class="hudson.plugins.git.util.DefaultBuildChooser"/>
-    <gitTool>Default</gitTool>
     <submoduleCfg class="list"/>
-    <relativeTargetDir></relativeTargetDir>
-    <reference></reference>
-    <excludedRegions></excludedRegions>
-    <excludedUsers></excludedUsers>
-    <gitConfigName></gitConfigName>
-    <gitConfigEmail></gitConfigEmail>
-    <skipTag>false</skipTag>
-    <includedRegions></includedRegions>
-    <scmName></scmName>
+    <extensions>
+      <hudson.plugins.git.extensions.impl.RelativeTargetDirectory>
+        <relativeTargetDir>cacert-gigi</relativeTargetDir>
+      </hudson.plugins.git.extensions.impl.RelativeTargetDirectory>
+    </extensions>
   </scm>
   <canRoam>true</canRoam>
   <disabled>false</disabled>
   <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
   <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
   <jdk>Java 8 OpenJDK</jdk>
-  <triggers class="vector">
+  <triggers>
     <hudson.triggers.SCMTrigger>
       <spec>@midnight</spec>
       <ignorePostCommitHooks>false</ignorePostCommitHooks>
   <concurrentBuild>false</concurrentBuild>
   <builders>
     <hudson.tasks.Shell>
-      <command>cat &lt;&lt;EOT &gt;config/test.properties
+      <command>rm -f *.deb
+cd cacert-gigi
+cat &lt;&lt;EOT &gt;keys/config
+DOMAIN=$$$$YOUR_LOOKUP_DOMAIN$$$$
+KEYSIZE=4096
+EOT
+cat &lt;&lt;EOT &gt;config/test.properties
 type=autonomous
-java=java -cp gigi.jar:/path/to/mysql-connector.jar org.cacert.gigi.Launcher
-serverPort=4448
+java=/usr/lib/jvm/openjdk-8-jdk-gigi/bin/java -cp bintest:gigi-testing.jar:/usr/share/java/mysql-connector-java.jar -javaagent:/usr/share/java/jacocoagent.jar org.cacert.gigi.Launcher
+serverPort.https=4448
+serverPort.http=8098
 mailPort=8473
 sql.driver=com.mysql.jdbc.Driver
 sql.url=jdbc:mysql://localhost:3306/cacert
 sql.user=cacert
-sql.password=<yourSqlPassword>
+sql.password=$$$$sql password$$$$
+name.static=static.$$$$YOUR_LOOKUP_DOMAIN$$$$
+name.secure=secure.$$$$YOUR_LOOKUP_DOMAIN$$$$
+name.www=www.$$$$YOUR_LOOKUP_DOMAIN$$$$
+name.api=api.$$$$YOUR_LOOKUP_DOMAIN$$$$
+
+domain.manage=http://$$$$YOUR_TESTSERVICE$$$$/dyn-txt.php?token=$$$$managementToken$$$$&amp;
+domain.http=$$$$YOUR_TESTSERVICE_HTTP$$$$
+domain.dnstest=$$$$YOUR_TESTSERVICE_ZONE$$$$
+domain.testns=$$$$YOUR_TESTSERVICE_AUTH_NAMESERVER$$$$
+domain.local=test.$$$$YOUR_LOOKUP_DOMAIN$$$$
+
+email.address=$$$$YOUR_IMAP_EMAIL$$$$
+email.password=$$$$YOUR_IMAP_PASSWORD$$$$
+email.imap=$$$$YOUR_IMAP_SERVER$$$$
+email.imap.user=$$$$YOUR_IMAP_USERNAME$$$$
+email.non-address=$$$$IMAP_NON_EXISTENT_ADDRESS$$$$
+
 EOT
+
 </command>
     </hudson.tasks.Shell>
     <hudson.tasks.Ant plugin="ant@1.2">
-      <targets></targets>
+      <targets>$TARGET generatecoco</targets>
       <antOpts>-Dfile.encoding=UTF-8</antOpts>
-      <buildFile>build.xml</buildFile>
-      <properties>juintexec=/path/to/folder/with/junit/
-mysqlconnector=/path/to/mysql-connector.jar</properties>
+      <buildFile>cacert-gigi/build.xml</buildFile>
+      <properties>juintexec=$$$$JUNIT_PATH$$$$
+test_nic=$$$$YOUR_TESTSERVICE_NIC$$$$\n$$$$YOUR_LOOKUP_DOMAIN$$$$</properties>
     </hudson.tasks.Ant>
+    <hudson.tasks.Shell>
+      <command>cd cacert-gigi
+dpkg-buildpackage -b -us -uc</command>
+    </hudson.tasks.Shell>
   </builders>
   <publishers>
     <hudson.tasks.junit.JUnitResultArchiver>
-      <testResults>junit/*.xml</testResults>
+      <testResults>cacert-gigi/junit/*.xml</testResults>
       <keepLongStdio>false</keepLongStdio>
       <testDataPublishers/>
     </hudson.tasks.junit.JUnitResultArchiver>
     <hudson.tasks.ArtifactArchiver>
-      <artifacts>natives/*.so,gigi.jar,gigi-linux_amd64.zip,static.tar.gz</artifacts>
+      <artifacts>cacert-gigi/natives/*.so,cacert-gigi/gigi*.jar,cacert-gigi/gigi-linux_amd64.zip,*.deb</artifacts>
       <latestOnly>false</latestOnly>
       <allowEmptyArchive>false</allowEmptyArchive>
     </hudson.tasks.ArtifactArchiver>
diff --git a/doc/jenkinsJob/dyn-txt.php b/doc/jenkinsJob/dyn-txt.php
new file mode 100644 (file)
index 0000000..c7b6cfe
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+header("Content-type: text/plain");
+
+define("ZONENAME", "cacert.dyn.dogcraft.de");
+define("KEYNAME", "keys/Kcacert.dyn.dogcraft.de.+165+54687.key");
+
+if(!isset($_GET['token']) || !isset($_GET['t1']) || !isset($_GET['t2']) || !isset($_GET['action'])){
+  die("Error");
+}
+if($_GET['token'] != "rD1m3A9ew6Hs4DIv7lnTxNbR6dr"){
+  die ();
+}
+$t1 = $_GET['t1'];
+$t2 = $_GET['t2'];
+if(!preg_match("/^[a-zA-Z0-9]+$/", $t1) || !preg_match("/^[a-zA-Z0-9]+$/", $t2)){
+  die("Error");
+}
+$todelete = array();
+
+if(file_exists("data.php")){
+  include ("data.php");
+}
+
+$time = time()/60;
+if(!isset($todelete[$time])){
+  $todelete[$time] = array();
+}
+
+$dnscalls = "";
+
+if($_GET['action'] == "http"){
+  $todelete[$time][] = array("http", $t1);
+  file_put_contents("cacert-$t1.txt", $t2);
+} else if($_GET['action'] == "dns") {
+  $todelete[$time][] = array("dns", $t1);
+  $dnscalls .= "update delete {$t1}._cacert._auth." . ZONENAME . " TXT\n"
+    ."update add {$t1}._cacert._auth." . ZONENAME . " 60 TXT {$t2}\n";
+}
+$copy = $todelete;
+foreach($copy as $nt => $ar){
+  if($nt < $time - 2){
+    unset($todelete[$nt]);
+    foreach($ar as $act){
+      if($act[0] == "http"){
+        unlink("cacert-{$act[1]}.txt");
+      } else if($act[0] == "dns") {
+        $dnscalls .= "update delete {$act[1]}._cacert._auth." . ZONENAME . " TXT\n";
+      }
+    }
+  }
+}
+file_put_contents("data.php", "<?php \$todelete = ".var_export($todelete,true).";\n?>");
+
+if($dnscalls != ""){
+  dnsAction($dnscalls);
+}
+
+function dnsAction($command) {
+  $call = "server localhost\n$command\nsend\nquit\n";
+
+  $nsupdate = popen("/usr/bin/nsupdate -k " . KEYNAME, 'w');
+  fwrite($nsupdate, $call);
+  $retval = pclose($nsupdate); // nsupdate doesn't return anything useful when called this way
+}
+
index 3574a98acc77837ca270f5615e6ad2b0af6d3131..ea4975ec40a131e880e34a518807c1c35d1ff824 100644 (file)
@@ -1,3 +1 @@
-*.crt
-jetty.csr
-jetty.key
+/*.csr
diff --git a/doc/scripts/generateKeys.sh b/doc/scripts/generateKeys.sh
deleted file mode 100755 (executable)
index 26a01ce..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-# this script generates a simple self-signed keypair
-
-openssl genrsa -out jetty.key 4096
-openssl req -new -key jetty.key -out jetty.csr -subj "/CN=localhost" -config selfsign.config
-openssl x509 -req -days 365 -in jetty.csr -signkey jetty.key -out jetty.crt
-openssl pkcs12 -inkey jetty.key -in jetty.crt -export -passout pass: -out ../../config/keystore.pkcs12
diff --git a/doc/scripts/generateSomeCsrs.sh b/doc/scripts/generateSomeCsrs.sh
new file mode 100755 (executable)
index 0000000..758342a
--- /dev/null
@@ -0,0 +1,7 @@
+cd `dirname $0`
+
+for i in {4..100}; do
+openssl req -newkey rsa:1024 -nodes -keyout /dev/null \
+       -out $i.csr -subj "/CN=tmp.cacert.local" \
+       -config ../../keys/selfsign.config;
+done
diff --git a/doc/scripts/generateTruststore.sh b/doc/scripts/generateTruststore.sh
deleted file mode 100755 (executable)
index 1295294..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-# this script imports the cacert root certs
-
-wget -N http://www.cacert.org/certs/root.crt
-wget -N http://www.cacert.org/certs/class3.crt
-
-keytool -importcert -keystore ../../config/cacerts.jks -file root.crt -alias root -storepass "changeit" $1
-keytool -importcert -keystore ../../config/cacerts.jks -file class3.crt -alias class3 -storepass "changeit" $1
-keytool -importcert -keystore ../../config/cacerts.jks -file jetty.crt -alias own -storepass "changeit" $1
-
-keytool -list -keystore ../../config/cacerts.jks -storepass "changeit"
diff --git a/doc/scripts/getScrypt.sh b/doc/scripts/getScrypt.sh
new file mode 100755 (executable)
index 0000000..02f1fea
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh
+git clone https://github.com/wg/scrypt.git
+BASE=$(dirname $0)/../../..
+
+cd scrypt
+[ "$(git rev-parse refs/tags/1.4.0)" != "0675236370458e819ee21e4427c5f7f3f9485d33" ] && echo "SHA-Hash failed" && exit 1
+
+git checkout 0675236370458e819ee21e4427c5f7f3f9485d33
+mkdir -p $BASE/lib/scrypt/com/lambdaworks
+cp -R src/main/java/com/lambdaworks/crypto $BASE/lib/scrypt/com/lambdaworks
+
+mkdir -p $BASE/tests/com/lambdaworks/crypto/test
+cp -R src/test/java/com/lambdaworks/crypto/test $BASE/tests/com/lambdaworks/crypto
+
+cd ..
+rm -Rf scrypt
diff --git a/doc/scripts/gigi b/doc/scripts/gigi
new file mode 100755 (executable)
index 0000000..14bccc4
--- /dev/null
@@ -0,0 +1,57 @@
+#!/bin/bash
+if [ "$JDBC_DRIVER" == "" ]
+then
+JDBC_DRIVER=/usr/share/java/postgresql-jdbc4.jar
+#echo "JDBC_DRIVER environment variable not set. Assumed path: $JDBC_DRIVER"
+fi
+if [ "$GIGI_EXEC" == "" ]
+then
+GIGI_EXEC=/usr/share/java/gigi.jar
+#echo "GIGI_EXEC environment variable not set. Assumed path: $GIGI_EXEC" 
+fi
+
+cd /var/lib/cacert-gigi
+
+if [ "$1" == "start" ]
+then
+       java -cp $JDBC_DRIVER:$GIGI_EXEC org.cacert.gigi.Launcher
+elif [ "$1" == "debug" ]
+then
+       java -cp $JDBC_DRIVER:$GIGI_EXEC -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 org.cacert.gigi.Launcher
+elif [ "$1" == "start-daemon" ]
+then
+       if [ ! -e /etc/cacert/gigi/conf.tar ]; then
+               echo "Config missing."
+               exit 1;
+       fi
+       java -cp $JDBC_DRIVER:$GIGI_EXEC org.cacert.gigi.Launcher < /etc/cacert/gigi/conf.tar >> /var/log/cacert-gigi.log 2>&1 &
+       echo $! > /var/run/cacert-gigi.pid
+elif [ "$1" == "signer" ]
+then
+       java -cp $JDBC_DRIVER:$GIGI_EXEC org.cacert.gigi.util.SimpleSigner
+elif [ "$1" == "reset-database" ]
+then
+       java -cp $JDBC_DRIVER:$GIGI_EXEC org.cacert.gigi.util.DatabaseManager
+elif [ "$1" == "fetch-locales" ]
+then
+    if [ "$2" == "" ]; then
+               java -cp $JDBC_DRIVER:$GIGI_EXEC org.cacert.gigi.util.FetchLocales
+       else
+               java -cp $JDBC_DRIVER:$GIGI_EXEC org.cacert.gigi.util.FetchLocales "$2"
+       fi
+elif [ "$1" == "signer-conf" ]
+then
+       mkdir /var/lib/cacert-gigi/config
+       cd /var/lib/cacert-gigi/config
+       tar x gigi.properties
+else
+       echo "Usage: gigi <option>"
+       echo "debug - starts gigi in debug mode (on port 8000, with config from stdin)"
+       echo "fetch-locales - (re)fetch the localisation"
+       echo "reset-database - resets the database"
+       echo "signer - starts the simple signer"
+       echo "signer-conf - extract config for simple signer (and reset-database) from the tar from stdin"
+       echo "start - starts gigi"
+       echo "start-daemon - starts gigi in background (using config from /etc/cacert/gigi/conf.tar)"
+
+fi
diff --git a/doc/scripts/selfsign.config b/doc/scripts/selfsign.config
deleted file mode 100644 (file)
index 4962f72..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-[req]
-distinguished_name=dn
-#req_extensions=ext
-
-[dn]
-commonName = cn
-
-[ext]
-subjectAltName=
diff --git a/doc/tableStructure.sql b/doc/tableStructure.sql
deleted file mode 100644 (file)
index 88ba34e..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-DROP TABLE IF EXISTS `users`;
-CREATE TABLE `users` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `email` varchar(255) NOT NULL DEFAULT '',
-  `password` varchar(255) NOT NULL DEFAULT '',
-  `fname` varchar(255) NOT NULL DEFAULT '',
-  `mname` varchar(255) NOT NULL DEFAULT '',
-  `lname` varchar(255) NOT NULL DEFAULT '',
-  `suffix` varchar(50) NOT NULL DEFAULT '',
-  `dob` date NOT NULL DEFAULT '0000-00-00',
-  `verified` int(1) NOT NULL DEFAULT '0',
-  `ccid` int(3) NOT NULL DEFAULT '0',
-  `regid` int(5) NOT NULL DEFAULT '0',
-  `locid` int(7) NOT NULL DEFAULT '0',
-  `listme` int(1) NOT NULL DEFAULT '0',
-  `admin` tinyint(1) NOT NULL DEFAULT '0',
-  `language` varchar(5) NOT NULL DEFAULT '',
-  `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `deleted` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `locked` tinyint(1) NOT NULL DEFAULT '0',
-  `assurer_blocked` tinyint(1) NOT NULL DEFAULT '0',
-  PRIMARY KEY (`id`),
-  KEY `ccid` (`ccid`),
-  KEY `regid` (`regid`),
-  KEY `locid` (`locid`),
-  KEY `email` (`email`),
-  KEY `stats_users_created` (`created`),
-  KEY `stats_users_verified` (`verified`),
-  KEY `userverified` (`verified`)
-) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
-
-
-DROP TABLE IF EXISTS `email`;
-CREATE TABLE `email` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `memid` int(11) NOT NULL DEFAULT '0',
-  `email` varchar(255) NOT NULL DEFAULT '',
-  `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `deleted` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `hash` varchar(50) NOT NULL DEFAULT '',
-  `attempts` int(1) NOT NULL DEFAULT '0',
-  PRIMARY KEY (`id`),
-  KEY `memid` (`memid`),
-  KEY `stats_email_hash` (`hash`),
-  KEY `stats_email_deleted` (`deleted`),
-  KEY `email` (`email`)
-) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
-
-DROP TABLE IF EXISTS `pinglog`;
-CREATE TABLE `pinglog` (
-  `when` datetime NOT NULL,
-  `uid` int(11) NOT NULL,
-  `email` varchar(255) NOT NULL,
-  `result` varchar(255) NOT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-
-DROP TABLE IF EXISTS `baddomains`;
-CREATE TABLE `baddomains` (
-  `domain` varchar(255) NOT NULL DEFAULT ''
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-
-
-DROP TABLE IF EXISTS `alerts`;
-CREATE TABLE `alerts` (
-  `memid` int(11) NOT NULL DEFAULT '0',
-  `general` tinyint(1) NOT NULL DEFAULT '0',
-  `country` tinyint(1) NOT NULL DEFAULT '0',
-  `regional` tinyint(1) NOT NULL DEFAULT '0',
-  `radius` tinyint(1) NOT NULL DEFAULT '0',
-  PRIMARY KEY (`memid`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-
-DROP TABLE IF EXISTS `user_agreements`;
-CREATE TABLE `user_agreements` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `memid` int(11) NOT NULL,
-  `secmemid` int(11) DEFAULT NULL,
-  `document` varchar(50) DEFAULT NULL,
-  `date` datetime DEFAULT NULL,
-  `active` int(1) NOT NULL,
-  `method` varchar(100) NOT NULL,
-  `comment` varchar(100) DEFAULT NULL,
-  PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
-
-DROP TABLE IF EXISTS `emailcerts`;
-CREATE TABLE `emailcerts` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `memid` int(11) NOT NULL DEFAULT '0',
-  `serial` varchar(50) NOT NULL DEFAULT '',
-  `CN` varchar(255) NOT NULL DEFAULT '',
-  `subject` text NOT NULL,
-  `keytype` char(2) NOT NULL DEFAULT 'NS',
-  `codesign` tinyint(1) NOT NULL DEFAULT '0',
-  `csr_name` varchar(255) NOT NULL DEFAULT '',
-  `crt_name` varchar(255) NOT NULL DEFAULT '',
-  `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `revoked` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `expire` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `warning` tinyint(1) NOT NULL DEFAULT '0',
-  `renewed` tinyint(1) NOT NULL DEFAULT '0',
-  `rootcert` int(2) NOT NULL DEFAULT '1',
-  `md` enum('md5','sha1','sha256','sha512') NOT NULL DEFAULT 'sha512',
-  `type` tinyint(4) DEFAULT NULL,
-  `disablelogin` int(1) NOT NULL DEFAULT '0',
-  `pkhash` char(40) DEFAULT NULL,
-  `certhash` char(40) DEFAULT NULL,
-  `coll_found` tinyint(1) NOT NULL,
-  `description` varchar(100) NOT NULL DEFAULT '',
-  PRIMARY KEY (`id`),
-  KEY `emailcerts_pkhash` (`pkhash`),
-  KEY `revoked` (`revoked`),
-  KEY `created` (`created`),
-  KEY `memid` (`memid`),
-  KEY `serial` (`serial`),
-  KEY `stats_emailcerts_expire` (`expire`),
-  KEY `emailcrt` (`crt_name`)
-) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
-
-DROP TABLE IF EXISTS `notary`;
-CREATE TABLE `notary` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `from` int(11) NOT NULL DEFAULT '0',
-  `to` int(11) NOT NULL DEFAULT '0',
-  `awarded` int(3) NOT NULL DEFAULT '0',
-  `points` int(3) NOT NULL DEFAULT '0',
-  `method` enum('Face to Face Meeting','Trusted Third Parties','Thawte Points Transfer','Administrative Increase','CT Magazine - Germany','Temporary Increase','Unknown','TOPUP','TTP-Assisted') NOT NULL DEFAULT 'Face to Face Meeting',
-  `location` varchar(255) NOT NULL DEFAULT '',
-  `date` varchar(255) NOT NULL DEFAULT '',
-  `when` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `expire` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `sponsor` int(11) NOT NULL DEFAULT '0',
-  `deleted` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-  PRIMARY KEY (`id`),
-  KEY `from` (`from`),
-  KEY `to` (`to`),
-  KEY `from_2` (`from`),
-  KEY `to_2` (`to`),
-  KEY `stats_notary_when` (`when`),
-  KEY `stats_notary_method` (`method`)
-) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
-
-
-DROP TABLE IF EXISTS `cats_passed`;
-CREATE TABLE `cats_passed` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `user_id` int(11) NOT NULL,
-  `variant_id` int(11) NOT NULL,
-  `pass_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `test_passed` (`user_id`,`variant_id`,`pass_date`)
-) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
-
-# --------------------------------------------------------
-
-#
-# Table structure for table `cats_type`
-#
-
-DROP TABLE IF EXISTS `cats_type`;
-CREATE TABLE `cats_type` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `type_text` varchar(255) NOT NULL,
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `type_text` (`type_text`)
-) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
diff --git a/doc/wishList.md b/doc/wishList.md
new file mode 100644 (file)
index 0000000..2060314
--- /dev/null
@@ -0,0 +1,724 @@
+== Glossary / Definitions ==
+
+ASN.1: A horrible way to encode data. Usually used together with X.509
+
+BER: Basic Encoding Rules for ASN.1
+
+CER: Canonical Encoding Rules for ASN.1
+
+CSR: Certificate Signing Request, request to get some public key signed
+
+CSRF: Cross Site Request Forgery, attach technique breaching causality
+of requests
+
+DER: Distinguished Encoding Rules for ASN.1
+
+ECMA: European Computer Manufacturers Association
+
+ETSI: European Telecommunications Standards Institute
+
+GnuPG: GNU Privacy Guard, Some implementation using the OpenPGP standard
+
+HSTS: Hypertext Strict Transport Security, Protection Mechanism against
+casual MitM in networks and SSL Stripping, governed by RFC 6797
+
+ITU: International Telecommunication Union, standards body responsible
+for most standards with a dot in their names
+
+JS: JavaScript, an ECMA-Standard
+
+JSON: JavaScript Object Notation, standardized way to encode data for
+easy parsing
+
+MIME: Multipurpose Internet Mail Extensions, some way to stuff multiple
+messages into one message
+
+OAuth: OpenAuthentication standard for SSO
+
+OpenPGP: Signature and Encryption format governed by RFC 4880 et. al.
+
+OTP: One-Time-Password
+
+PKI: Public Key Infrastructure
+
+PKIX: PKI using X.509
+
+SPKAC: Signed Public Key and Challenge, interactive variant of a CSR
+
+SSL: Secure Socket Layer, predecessor of TLS, cf. TLS
+
+SSO: Single Sign On, mechanism for authentication accross different
+domains/systems using a central identity
+
+TLS: Transport Layer Security, Protocol for secure communication between
+a client and a server, governed by various RFCs
+
+X.509: An ITU standard describing contents of things (usually abused for
+PKIX certificates)
+
+XSS: Cross-Site Scripting, attack technique breaching same-origin boundaries
+
+= Todo list =
+== "Todo" (work in active code) ==
+
+* Benchmarking (long time + load)
+
+* review all date/time uses in order to watch over the timezone.
+
+== Missing ==
+
+* add email/domain ping
+  * check for domains allowed in policy
+  * black list for special domains not be use by user but through org
+admins (e.g. adobe.com, 3m.com)
+    * Maybe use Alexa Top 1M as one source
+    * Use copies of DNS zones to identify new domains
+    * Use DNSRBLs to identify young domains
+  * List of well-known top level domains and Second-level sub domains
+(cf. Public Suffix List below in references)
+* OpenPGP integration
+* think about org - certificates
+* think about names. What are names? how many names? assure names?
+
+== Makeup ==
+
+** Extract menu from template
+* Separate content and design
+
+
+= Software requirements / wishes =
+== Generic ==
+* Useable without Javascript enabled
+* Features implemented in Javascript may only enhance features already
+accessible without Javascript
+* All pages presented to the user must be HTML5-compliant
+* All stylesheets must be valid CSS3
+* All used scripts should be JavaScript/ECMAScript 1.6 or compatible
+* For security reasons enforced by HSTS/CSP, the following restrictions
+for included content apply
+  * No inline Javascript
+  * No inline CSS
+  * Stylesheets, images, scripts are only allowed from a special
+"static" subdomain
+
+== Notifications ==
+* Mail notifications from the system should use OpenPGP or S/MIME signatures
+
+== Registration ==
+ * set names (tbd)
+ * set DoB (default: not set) (has to be before current date - not older
+than 150 years, check for 29.2.?) (if underage: special ack?) INO: Why
+not 120 year, which should be sufficent? (People are getting older, not
+younger ... You know?)
+ * set email(s) - at least one has to be set, primary needs to be set (tbd)
+ * selection of allowed login methods (default: yes for PW and cert)
+  * if PW login is allowed: set PW (tbd)  (always initially required as
+it is hard to issue certificates directly on signup?) ES: If you could
+do the email-check in the same session that is not mandatory. Even IF it
+is, this could be done with a 1-time-PW from the mail. - Maybe the
+complete step could be moved to AFTER the first email-check. It would
+make more sense, and one could add more explanation and a link to
+generate said certificate or whatever
+  * selection if pw-recovery per questions should be possible
+(explanatory text!)
+   * if yes: pw-recovery q&a have to be set
+ * confirmation that data was entered truthfully and recap of RLO at
+this stage
+ * CCA acceptance
+
+== Login/authentication ==
+* Separate Session scope for "www", "secure" and "api"
+* Authenticate with email and password ("www" only)
+* Authenticate with client certificate ("secure" + "api")
+* Authenticate (temporary) with one-time access token (special
+operations like revocation) (new feature)
+* Option to switch off/on authentication methods (new feature)
+* Option to selectively grant permissions to certain types of
+credentials only (e.g. pw login may only assure someone, while cert XY
+may do anything, SE interface with cert only) (new feature) ES: I think
+Benny was speaking about a user setting, while INO was speaking about a
+fixed global setting - this are two different things!
+* Reset password by security questions and mail (?? probably not
+[recommended in all situations])
+* Option to switch off/on pw recovery per security questions (new feature)
+* Reset password by assurance (how is this going to be implemented
+automatically?) Cannot be done automatically at the moment, as this is
+covered by a process clearly defined by an arbitration precedents-case.
+- At least not with a new precedents ruling. - Only part is the pw-reset
+field on the SE side
+* CCA acceptance at login, if current version of CCA was not accepted by
+user before
+
+=== Account Flags (tbd: collect further (new) flags) ===
+most should contain dates when set/removed - needed for account log and
+audit
+* Activated (Initial Account Activation Mail received)
+* CATs passed (FD: virtual = live calculation)
+* assurer (FD: virtual = live calculation)
+* blocked assurer
+* CCA accepted (virtual)
+* blocked account
+* deleted account
+* PW login allowed (Global for Account)
+* Cert login allowed (Global for Account, per Cert flags may set actual
+permissions)
+* PW Recovery function disabled
+* PW Reset Required
+* code signing allowed (Maybe better suited as a Permission for audit of
+Dates)
+* official nick name allowed (maybe needed for another new feature)
+* nick name allowed (maybe needed for another new feature)
+* announcements (general, country, regional, 200km) set
+* involved in arbitration case (probably virtual, as additional
+information has to be stored for those entries)
+
+=== Special Account Permissions and Groups ===
+TBL: Group [ID{AI/PK}, Name{S}]
+TBL: GroupMembership [ID{AI/PK}, User{Ref(User)}, Group{Ref(Group)}]
+TBL: Permission [ID{AI/PK}, Name{S}, ReportTo{S}]
+TBL: UserPermissions [ID{AI/PK}, User{Ref(User)},
+Permission{Ref(Permission)}, Action{E(Grant,Revoke)}, Date{Date}]
+TBL: GroupPermissions [ID{AI/PK}, Group{Ref(Group)},
+Permission{Ref(Permission)}, Action{E(Grant,Revoke)}, Date{Date}]
+
+* Used for special permissions e.g. in support interface, mass mailings
+* Additionally groups members with additional obligations (e.g. PP-CATS)
+
+* manual setting of permissions has to appear in personal and admin-log
+
+== personal details (names, DoB) ==
+* Change personal details (names, DoB) (only if not assured)
+ * after each change, the user has to state, to have those names entered
+in official documents
+* maybe adding/removing of non-official nick-name allowed at any time
+(new feature)
+* Allow multiple names that can be assured independently (new) -
+something like this should be clarified with the AO at least, better to
+get it fixed clearly in an updated AP (in general AP does allow for this
+at the moment, but people do not know about this)
+* Allow to enter
+ * 1 name - with a confirmation that one only has exactly one name part
+ * Title? FN+ LN+ Suffix? marked as non-assurable Nick? (could be used
+for internal processes) - with a confirmation that those are covered by
+at least one official document owned by the member/potential member
+* GUI should (until further notice) allow only exactly ONE name (with
+multiple name-parts) to be entered for an account
+* Names containing a Nickname component MUST NOT be primary names for an
+account (BB: Who did the striking? and Why?)
+* official nicknames may be used, if switched on based on an assurance
+by support
+* else nicknames may only be displayed as an addition and MAY NOT be assured
+TBL: Name: [ID{AI/PK}, UID{Ref(User}},
+TBL: NameComponent [ID{AI/PK}, Name{Ref(Name)}, Order{I},
+Type{E[One,Firstname,Lastname,Title,Suffix,Nick,OfficialNick]},
+Assureable{B}, Value{S}, Context{S}, Points{I,Cached}]
+
+
+=== Domain/email pinging ===
+* re-ping domains regularly
+  * Domains by automated variants like (see MoPad devDomainPing)
+    * text files at certain locations
+    * DNS TXT records
+    * opening ssl connections
+
+* configure types of pinging
+* show previous pings with results. -> for support all, for member only
+during their ownership (new feature)
+* show status of the various pings and next scheduled events (new feature)
+* Domain Pings record account ownership at time of ping (new feature)
+* domain dispute (transfer of domains)
+  * New owner files "Domain Dispute"
+  * Old Owner receives mail with options to Accept, Decline or report
+(to support) this claim
+  * If Accepted domain is transferred (and existing certificates
+referencing this domain revoked) [New feature: Old owner may specify
+which ones, confirmed by new owner]
+  * If any certificates should be kept, new owner is asked to confirm
+(within certain amount of time) INO: There should be NO handover of cert
+when moving domains (Needs discussion, new feature, maybe Policy Change
+required)
+  * Only after all certificates have either been revoked or been
+confirmed by the new owner, the domain is finally transferred over.
+  * If the transfer is Declined, no transfer is performed.
+  * If the transfer is reported, information on both parties is sent to
+support, including a description of the case (entered by the old owner
+when reporting)
+* email ping
+  * For emails on a domain within the account
+    * Reachability of the mail accounts is tried within random intervals
+(silent)
+    * If an email by our system is sent and properly accepted by the
+receiving system, next try for explicit ping is postponed. (maximum
+postponing of explicit check for about 1 year using this method) ES: has
+big issues with a) grace period as it removes a big part of security
+that we want to provide and b) automated contend-less mails as we
+declare to not address members without reason - a ping would not be
+considered a reason by our members BB: Outside the grace period the
+email is considered unconfirmed, and no explicit ping is triggered, but
+an active operation using this email address will cause a ping mail to
+be sent. The main reason for this is to reduce the number of ping mails
+sent when multiple certificates are issued in a given time span (UNDER
+HEAVY DISCUSSION)
+    * If any email address is failing for a certain time, schedule it to
+be explicitly checked by a ping mail on next use in a certificate
+    * a member should be able to initiate a ping mail
+    * support should be able to initiate a ping (admin-log!)
+    * If this explicit ping mail (with its included token/link) is not
+confirmed in a reasonable time, a revalidation of the accompanying
+domain is triggered, including a note to explicitly revalidate the
+failing address)
+    * permanent failing pings should be reported to primary email
+address once
+    * permanent failing pings should be displayed after a login (as
+recent notifications)
+  * For separate mail accounts (outside of domains that are part of an
+account):
+    * If regular mails from the system are properly received, postpone
+automatic checking by at most one year (see ES's and BB's comments above)
+  * send information/ping email before each certificate creation to
+affected email(s),
+    * this mail may be repeated if unsuccessful, but
+    * HAS TO be successful eventually to complete the issuing of the
+certificate for the affected email
+    * selection per general setting if such mails must be confirmed
+before the completion of the certificate issuing
+    * failures have to be displayed directly and in logs
+    * permanent failures have to be reported to primary email address once
+    * permanent failures should be displayed after a login (as recent
+notifications)
+  * option to inform primary email, also (general setting + selectable
+at certificate issue time)
+
+== Certificate Management ==
+* List all issued certificates per kind
+* Revoke issued certificates
+* renew certificates (re-issue a previous CSR) INO: what is if the old
+CSR does not meet the requiremnts any more eg. weak keys -> Prefill of
+the CreateCert-Dialog with the existing data for the cert/csr
+* select md (sha2-512, sha2-384, sha2-256, sha3-512, whirlpool?, ...)
+* select duration up to maximum allowed for member
+* select (assured) name components / acronyms thereof
+* select (ping-approved) domains / email-addresses
+* select class (up to what is allowed for member)
+* select if allowed for login as described above @ login
+* change comment of certificate
+
+MAYBE: * schedule to issue certificates for a day in the next two weeks.
+(Should require someone who knows what he's doing, requires experienced
+assurer)
+
+Notification Mail about new certificate may include link for revocation
+(new feature)
+
+Process for issuing a certificate:
+1. Upload CSR / Generate key in Browser
+   (That's actually step 2)1a.for browser generation: selection for
+names, emails, domains, certificate root
+2. Confirm values and if necessary modify them
+3. Select additional settings (login, comments, ...) and validity interval
+4. confirm CCA + confirm the issuing of the certificate as shown
+(do 1a to 4 in one form? - Could be done with JS enabled)
+5. send information (ping) mail to affected address (& primary if
+selected), display failures + potential abort
+6. Show Progress Page with JS hinting when it is done / otherwise using
+redirects in HTML
+7. Display a page with the final Certificate and details about it.
+
+Process of re-issuing certificate:
+1. click certificate (this creates the form-process)
+2. review all previous-entered settings. (inputs will be re-checked when
+sent: is name still valid/etc. still have all rights.)
+3. rest as above, formulations adapted to re-issuing
+
+=== Certificate types ===
+* Email certificates (include email (+ real name if points>=50)
+(+codesigning if enabled && Assurer)
+* Domain certificates (include domain name + SANs (+real name if
+points>=50))
+* Organisation Email certificates
+* Organisation Domain certificates
+* Split more templates (e.g. Email from Code Signing)
+
+=== WoT management ===
+* Assure a member
+  * enter email address + DoB
+    * if correct:
+      * if member was assured before by this assurer:
+        * currently: abort process with according information - but:
+this may be changed later
+      * if there was no previous assurance by the assurer over the member:
+        * names shown [has to be defined in more detail]
+        * DoB shown
+        * primary email shown
+        * confirmation that names + DoB could be verified were official
+documents
+        * select if TTP [further details TBD]
+        * free-text-field: location of assurance - default: empty -
+needs to be filled
+        * date of assurance selection, default: non selected (not before
+DoB, not after current date+13h) - needs to be set INO: Current Date UTC
++ 12 h BB:(13h, cause of timezones with +13h)
+        * confirmation that assuree has accepted CCA
+        * confirmation box, that AP was followed
+        * CCA acceptance box for assurer
+        * display if member was underage at assurance date, when set
+[TBD] and force a check-box-selection to confirm that PoJam process was
+followed
+    * if not correct independent of which was wrong:
+      * option to write an automated email to the member that the
+assurer tried to assure the member
+* View all assurances received/done
+  * points, locations, dates of assurance, dates of assurance entered,
+name of assurer/assuree, revocations
+* show how my points are calculated
+  * as assuree, as assurer
+* search for assurers/etc (what exactly is this community-thing)
+  * Search for assurer is referred to Portal (cacert.eu) ES: I really
+would like to have this feature again in the main software- in both
+directions, with the option to enter multiple locations at least
+temporary (permanent location, current location), we should make it easy
+for travelling assurer and assurees to spread assurances around! BB: I
+agree to ES: Having it in the software makes many things much more easy.
+* What is about multiple assurances for the same assuree, only last
+Assurance should count. (New Feature) - ES: currently this would need
+more details in policies - we do not know how this would resolve, we may
+only guess that it would be the last one
+* Experience Points only for the last of such re-assurances (New
+Features) - ES: again, this should wait for a policy decision
+
+=== Organisation management ===
+
+rethink? what needs to be done? what needs to be possible? how much is
+this used?
+* The Org area can be shifted back for now.
+
+
+Roles:
+* OrgAssurer: can create and administer the OrgAccounts, administer: add
+and delete OrgAdmin, add and delete Org Domains, does not have access to
+the certs
+* OrgMaster: can see the Org account data; add/remove other OrgMasters
+and OrgAdmins
+* OrgAdmin: can see the org account data; is able to administer the certs
+* SE: is able to see account data, revoke certs (new feature)
+
+Cert Creation:
+* Client Cert with form
+* Client Cert with CSR (New feature)
+* Client Cert with multiple email addresses
+* Multiple Client Certs via file upload and result download (create e.g.
+10 certs with one upload) (New Feature)
+* revoke cert via file upload, e.g. file with to email addresses all
+certs to these addresses will be revoked. (New Feature)
+
+* Filter on client cert page (new feature)
+
+* Domain Cert with CSR
+
+* Org Account History
+  * Org account full history visible by OrgAdmins, SE via Ticket Number
+  * Org Account history only account information without cert history by
+OrgAssurer
+
+* OrgAssurer should be able to perform an IsAssuer check.
+
+
+
+=== OpenPGP key signing ===
+* if points >= 50 allow signing of OpenPGP key with real name, no
+comment field, matching email in account.
+* UserIDs on key to be signed user-selectable.
+* Expiry date: selectable up to maximum allowed
+* re-signing without need to uploade key again
+* CCA acceptance needed for all signatures
+* lookup of keys on keyserver (if present)
+* Ensure key has basic cryptographic security:
+  * conformant to normal certificate's requirements
+  * valid key material
+
+=== Signer interface ===
+* database based
+* allow multiple signer instances
+* job table
+
+=== SE management (not finished) ===
+* nothing happens without support ticket id (that is verified?) <- ES:
+currently the basic account can be seen without a ticket number
+  * Support Ticket, Arbitration; see existing software on this <- ES:
+currently other tickets are allowed, too, but I see no need, as one of
+both should be needed (there will always be a support ticket number, if
+support is addressed as such - there should be no need for anybody to
+neither go this way nor through arbitration (arbitration may have
+reasons to go directly to a supporter but has the authority to do so)
+* every action on a member will cause an email to be sent to him and the
+action is being logged. <- the mail should be optional (to primary
+address) and only once in a given time frame/login, as a supporter may
+need to do more than one thing at a time
+* View/edit users personal details (even if assured)
+* set account flags (code signing, assurer status, block of account,
+ttp, org-admin, support, team member)
+* delete/invalidate assurances
+* revoke certificates, pgp-key signatures, both should be possible for
+single certificates/keys or for all certificates/keys
+* "delete" account
+
+=== Arbitration interface (new feature) ===
+ * everything requires: arbitration number, role in arbitration case
+(iCM, CM, Arbitrator)
+ * everything has to be shown in admin log for support and in account log
+ * show CCA acceptance for user identified by email (does not have to be
+primary) (only latest version of accepted CCA)
+ * show primary email for email address
+ * show arbitration case number of arbitration cases, a user identified
+per email address is involved in
+ * mark/unmark user identified per email (does not have to be primary)
+as involved in an arbitration case (by numbers)
+ * set CCA acceptance for user identified by email (date, "before
+arbitrator")
+ * running arbitration case should be visible to SE - ES: That is NOT
+part of the arbitration interface! And currently it would be enough, if
+delete account process would be stopped by the member being involved in
+an arb case, telling that there is an arb case (not which and how many)
+- currently this information is not needed in any other support case and
+involvement in arbitration cases is anonymous.
+
+=== automated (outside of a current process) information mails to
+members ===
+ * certificate(s)/signature expires within the next 14 days (combined or
+separate)
+
+=== "mass mails" interface (new feature) ===
+  * access only for critical team (or other restricted personal
+controlled by arbitration)
+  * arbitration case number needed (maybe announcement flags could be
+allowed for support case numbers or motions, as well)
+  * requester has to be set (identified by email)
+  * text-field for subject
+  * text-field for mail-content
+  * one of the following three should be possible for selection of
+recipients:
+    * sql-query
+    * list of email addresses
+    * list of account-ids
+    * selection field if this should be noted in account-logs of recipients
+    * announcement flag (general, ...)
+
+=== Statisics ===
+
+* running total for current year
+  * assurances
+  * new member
+  * new assurers
+  * lost members
+  * active assurers (at least one assurance according to the assurance
+date of the current year)
+  * active assurees (who got the latest assurance in the current year)
+
+* running total for last 12 months per month
+  * assurances
+  * new member
+  * new assurers
+  * lost members
+  * new certs
+  * revoked certs
+  * active assurer / assurees
+
+* Grand Total
+  * members
+  * assurances
+  * granted AP
+  * user >= 50 and <99 AP
+  * assurer candidates
+  * assurer
+  * issued client certs
+  * active client certs
+  * issued server certs
+  * active server certs
+  * issued org client certs
+  * active org client certs
+  * issued org server certs
+  * active org server certs
+  * issued gpg certs
+  * active gpg certs
+
+* yearly statistics
+  * assurances
+  * new member
+  * new assuer
+  * lost members
+  * new certs
+  * revoked certs
+  * active assurers (at least one assurance according to the assurance
+date of the current year)
+  * active assurees
+
+access statistic data via API e.g. assurance per year is needed for
+coaudit database
+
+the active assurees may be relevant for any decision in the regard how
+long assurances should be valid and if they need to be renewed. It also
+gives
+
+=== Premission Review mails (monthly?) ===
+* Regular check and report of permissions for various groups of people
+INO: currently quarterly
+
+=== Tools ===
+* csr/crt inspector
+* + lookup OCSP/CRL status.
+* OpenPGP key inspector (Yup, we need one)
+* Database Editor CLI tool for crit to update records (and their
+checksums/timestamping)
+
+==References==
+* Our Policies
+  * CAcert Community Agreement (CCA)
+  * Dispute Resolution Policy (DRP)
+  * Certificate Policy on Signing (CPS) + Certification Policy (per
+Root) (CP)
+  * Security Policy (SP) / Security Manual (SM)
+  * Assurance Handbook (AH) + Practice on Names (PoN)
+    * Policy on Junior Assurers and Members (PoJAM)
+    * Trusted Third Party (TTP) and Sub-Policies
+    * Organisation Assurance Policy (OAP)
+  * Policy on Policy (PoP)
+  * CAcert Website Data Privacy Policy (currently WIP) WDPP
+* Internet Standards
+  * RFC documents
+    * IPv4/IPv6
+      * RFC 0791 (IPv4: Internet Protocol, 1981)
+      * RFC 2460 (IPv6: Internet Protocol, Version 6 (IPv6)
+Specification, 1998)
+    * HTTP
+      * RFC 1945 (HTTP/1.0, 1996)
+      * RFC 2616 (HTTP/1.1, 1999)
+      * RFC 7230 (HTTP/1.1: Message Syntax and Routing, 2014)
+      * RFC 7231 (HTTP/1.1: Semantics and Content, 2014)
+      * RFC 7232 (HTTP/1.1: Conditional Requests, 2014)
+      * RFC 7233 (HTTP/1.1: Range Requests, 2014)
+      * RFC 7234 (HTTP/1.1: Caching, 2014)
+      * RFC 7235 (HTTP/1.1: Authentication, 2014)
+    * SSL/TLS
+      * RFC 3268 (TLS 1.0+: Advanced Encryption Standard (AES)
+Ciphersuites for Transport Layer Security (TLS), 2002)
+      * RFC 4347 (DTLS 1.0: Datagram Transport Layer Security, 2006)
+      * RFC 4492 (TLS 1.0+: Elliptic Curve Cryptography (ECC) Cipher
+Suites for Transport Layer Security (TLS), 2006)
+      * RFC 4346 (TLS 1.1: The Transport Layer Security (TLS) Protocol
+Version 1.1, 2006)
+      * RFC 4366 (TLS 1.0+: Transport Layer Security (TLS) Extensions, 2006)
+      * RFC 5246 (TLS 1.2: The Transport Layer Security (TLS) Protocol
+Version 1.2, 2008)
+      * RFC 5764 (TLS 1.0+: Transport Layer Security (TLS) Renegotiation
+Indication Extension, 2010)
+      * RFC 5878 (TLS 1.0+: Transport Layer Security (TLS) Authorization
+Extensions, 2010)
+      * RFC 6176 (SSL 2.0: Prohibiting Secure Sockets Layer (SSL)
+Version 2.0, 2011)
+      * RFC 7027 (TLS 1.0+: Elliptic Curve Cryptography (ECC) Brainpool
+Curves for Transport Layer Security (TLS), 2013)
+    * Web Security
+      * RFC 6797 (HSTS: HTTP Strict Transport Security, 2012)
+    * PKIX
+      * RFC 2459 (X.509: Internet X.509 Public Key Infrastructure
+Certificate and CRL Profile, 1999)
+      * RFC 3280 (X.509: Internet X.509 Public Key Infrastructure
+Certificate and Certificate Revocation List (CRL) Profile, 2002)
+      * RFC 4325 (X.509: Internet X.509 Public Key Infrastructure
+Authority Information, Access Certificate Revocation List (CRL)
+Extension, 2005)
+      * RFC 4630 (X.509: Update to DirectoryString Processing in the
+Internet X.509 Public Key Infrastructure Certificate and Certificate
+Revocation List (CRL) Profile, 2006)
+      * RFC 5280 (X.509: Internet X.509 Public Key Infrastructure
+Certificate and Certificate Revocation List (CRL) Profile, 2008)
+    * OpenPGP
+      * RFC 1991 (OpenPGP: PGP Message Exchange Formats, 1996)
+      * RFC 2440 (OpenPGP: OpenPGP Message Format, 1998)
+      * RFC 4880 (OpenPGP: OpenPGP Message Format, 2007)
+      * RFC 5581 (OpenPGP: The Camellia Cipher in OpenPGP, 2009)
+      * RFC 6637 (OpenPGP: Elliptic Curve Cryptography (ECC) in OpenPGP,
+2012)
+    * JSON
+      * RFC 4627 (JSON: The application/json Media Type for JavaScript
+Object Notation (JSON), 2006)
+      * RFC 7158 (JSON: The JavaScript Object Notation (JSON) Data
+Interchange Format, 2013)
+      * RFC 7159 (JSON: The JavaScript Object Notation (JSON) Data
+Interchange Format, 2013)
+      * ECMA 404 (JSON: The JSON Data Interchange Format, 2013)
+
+http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
+    * MIME
+      * RFC 2045 (MIME: Multipurpose Internet Mail Extensions (MIME)
+Part One: Format of Internet Message Bodies, 1996)
+      * RFC 2046 (MIME: Multipurpose Internet Mail Extensions (MIME)
+Part Two: Media Types, 1996)
+      * RFC 2047 (MIME: MIME (Multipurpose Internet Mail Extensions)
+Part Three: Message Header Extensions for Non-ASCII Text, 1996)
+      * RFC 2048 (MIME: Multipurpose Internet Mail Extensions (MIME)
+Part Four: Registration Procedures, 1996)
+      * RFC 2049 (MIME: Multipurpose Internet Mail Extensions (MIME)
+Part Five: Conformance Criteria and Examples, 1996)
+      * RFC 2183 (MIME: Communicating Presentation Information in
+Internet Messages: The Content-Disposition Header Field, 1997)
+      * RFC 2184 (MIME: MIME Parameter Value and Encoded Word
+Extensions: Character Sets, Languages, and Continuations, 1997)
+      * RFC 2231 (MIME: MIME Parameter Value and Encoded Word
+Extensions: Character Sets, Languages, and Continuations, 1997)
+      * RFC 5335 (MIME: Internationalized Email Headers, 2008)
+      * RFC 6532 (MIME: Internationalized Email Headers, 2012)
+    * S/MIME
+      * RFC 1847 (S/MIME: Security Multiparts for MIME: Multipart/Signed
+and Multipart/Encrypted, 1995)
+      * RFC 2633 (S/MIME: S/MIME Version 3 Message Specification, 1999)
+      * RFC 3851 (S/MIME: Secure/Multipurpose Internet Mail Extensions
+(S/MIME) Version 3.1 Message Specification, 2004)
+      * RFC 5751 (S/MIME: Secure/Multipurpose Internet Mail Extensions
+(S/MIME) Version 3.2 Message Specification, 2010)
+  * W3C documents
+    * HTML 5
+      * http://www.w3.org/TR/html-markup/
+      * http://dev.w3.org/html5/html4-differences
+    * CSS 3
+      * http://www.w3.org/Style/CSS/
+    * JavaScript / ECMAScript
+      *
+http://standards.iso.org/ittf/PubliclyAvailableStandards/c055755_ISO_IEC_16262_2011(E).zip
+      * http://ecma-international.org/ecma-262/5.1/
+    * XML
+      * XML 1.0: http://www.w3.org/TR/2008/REC-xml-20081126/
+      * XML 1.1: http://www.w3.org/TR/2006/REC-xml11-20060816/
+    * Content Security Policy
+      * Version 1.0: http://www.w3.org/TR/CSP/
+      * Version 1.1 (WIP):
+https://w3c.github.io/webappsec/specs/content-security-policy/
+  * Miscellaneous
+    * Public Suffix List of
+      http://publicsuffix.org/
+  * Unicode Standard
+    * Unicode Technical Report 39: http://www.unicode.org/reports/tr39/
+* ITU Standards
+  * X.690 / ASN.1
+    * Information technology � ASN.1 encoding rules: Specification of
+Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and
+Distinguished Encoding Rules (DER)
+      http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
+  * X.509
+* CA/Browser Forum
+  * Baseline Requirements
+    https://cabforum.org/baseline-requirements-documents/
+
+* Miscellanious
+  * Passwords
+    * Research on Password strength
+      Carnegie Mellon University: Guessing again (and again and again)
+      https://www.ece.cmu.edu/~lbauer/papers/2012/oakland2012-guessing.pdf
+      Presentation on the findings of the paper:
+      https://www.youtube.com/watch?v=USMd3swFZp4
+    * SCrypt Key Dervation Function
+      http://www.tarsnap.com/scrypt.html
+      Colin Percival, Stronger Key Derivation via Sequential Memory-Hard
+Functions, presented at BSDCan'09, May 2009:
+      * http://www.tarsnap.com/scrypt/scrypt.pdf
+      * http://www.tarsnap.com/scrypt/scrypt-slides.pdf
diff --git a/keys/.dirinfo b/keys/.dirinfo
new file mode 100644 (file)
index 0000000..c9087a3
--- /dev/null
@@ -0,0 +1,9 @@
+This directoy will contain keys created to test CAcert-gigi.
+generate them with doc/scripts/generateKeys.sh
+
+It may contain:
+
+testca/*
+testca.crt
+testca.key
+{api,secure,static,www}.{crt,key,csr,pkcs12}
diff --git a/keys/.gitignore b/keys/.gitignore
new file mode 100644 (file)
index 0000000..83df620
--- /dev/null
@@ -0,0 +1,15 @@
+#generated keys
+*.crt
+*.csr
+*.key
+*.pkcs12
+*.ca
+*.crl
+csr
+crt
+signer_bundle.tar
+
+
+# user specific generation config
+config
+
diff --git a/keys/generateKeys.sh b/keys/generateKeys.sh
new file mode 100755 (executable)
index 0000000..e9f75a7
--- /dev/null
@@ -0,0 +1,136 @@
+#!/bin/sh
+# this script generates a set of sample keys
+DOMAIN="cacert.local"
+KEYSIZE=4096
+PRIVATEPW="changeit"
+
+[ -f config ] && . ./config
+
+
+rm -Rf *.csr *.crt *.key *.pkcs12 *.ca *.crl
+
+
+####### create various extensions files for the various certificate types ######
+cat <<TESTCA > test_ca.cnf
+subjectKeyIdentifier = hash
+#extendedKeyUsage = critical
+basicConstraints = CA:true
+keyUsage = digitalSignature, nonRepudiation, keyCertSign, cRLSign
+TESTCA
+
+cat <<TESTCA > test_subca.cnf
+subjectKeyIdentifier = hash
+#extendedKeyUsage = critical,
+basicConstraints = CA:true
+keyUsage = digitalSignature, nonRepudiation, keyCertSign, cRLSign
+TESTCA
+
+cat <<TESTCA > test_req.cnf
+basicConstraints = critical,CA:false
+keyUsage = keyEncipherment, digitalSignature
+extendedKeyUsage=serverAuth
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer:always
+#crlDistributionPoints=URI:http://www.my.host/ca.crl
+#authorityInfoAccess = OCSP;URI:http://ocsp.my.host/
+TESTCA
+
+cat <<TESTCA > test_reqClient.cnf
+basicConstraints = critical,CA:false
+keyUsage = keyEncipherment, digitalSignature
+extendedKeyUsage=clientAuth
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer:always
+#crlDistributionPoints=URI:http://www.my.host/ca.crl
+#authorityInfoAccess = OCSP;URI:http://ocsp.my.host/
+TESTCA
+
+cat <<TESTCA > test_reqMail.cnf
+basicConstraints = critical,CA:false
+keyUsage = keyEncipherment, digitalSignature
+extendedKeyUsage=emailProtection
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer:always
+#crlDistributionPoints=URI:http://www.my.host/ca.crl
+#authorityInfoAccess = OCSP;URI:http://ocsp.my.host/
+TESTCA
+
+
+genca(){ #subj, internalName
+
+    openssl genrsa -out $2.key ${KEYSIZE}
+    openssl req -new -key $2.key -out $2.csr -subj "$1/O=Test Environment CA Ltd./OU=Test Environment CAs"
+    
+    mkdir $2.ca
+    mkdir $2.ca/newcerts
+    echo 01 > $2.ca/serial
+    touch $2.ca/db
+    echo unique_subject = no >$2.ca/db.attr
+
+}
+
+caSign(){ # key,ca,config
+    cd $2.ca
+    openssl ca -cert ../$2.crt -keyfile ../$2.key -in ../$1.csr -out ../$1.crt -days 365 -batch -config ../selfsign.config -extfile ../$3
+    cd ..
+}
+
+rootSign(){ # key
+    caSign $1 root test_subca.cnf
+}
+
+genserver(){ #key, subject, config
+    openssl genrsa -out $1.key ${KEYSIZE}
+    openssl req -new -key $1.key -out $1.csr -subj "$2" -config selfsign.config
+    caSign $1 env "$3"
+    
+    openssl pkcs12 -inkey $1.key -in $1.crt -CAfile env.chain.crt -chain -name $1 -export -passout pass:changeit -out $1.pkcs12
+    
+    keytool -importkeystore -noprompt -srckeystore $1.pkcs12 -destkeystore ../config/keystore.pkcs12 -srcstoretype pkcs12 -deststoretype pkcs12 -srcstorepass "changeit" -deststorepass "$PRIVATEPW"
+}
+
+
+# Generate the super Root CA
+genca "/CN=Cacert-gigi testCA" root
+openssl x509 -req -days 365 -in root.csr -signkey root.key -out root.crt -extfile test_ca.cnf
+
+# generate the various sub-CAs
+genca "/CN=Environment" env
+rootSign env
+genca "/CN=Unassured" unassured
+rootSign unassured
+genca "/CN=Assured" assured
+rootSign assured
+genca "/CN=Codesigning" codesign
+rootSign codesign
+genca "/CN=Timestamping" timestamp
+rootSign timestamp
+genca "/CN=Orga" orga
+rootSign orga
+genca "/CN=Orga sign" orgaSign
+rootSign orgaSign
+
+
+cat env.crt root.crt > env.chain.crt
+
+# generate orga-keys specific to gigi.
+# first the server keys
+genserver www "/CN=www.${DOMAIN}" test_req.cnf
+genserver secure "/CN=secure.${DOMAIN}" test_req.cnf
+genserver static "/CN=static.${DOMAIN}" test_req.cnf
+genserver api "/CN=api.${DOMAIN}" test_req.cnf
+
+genserver signer_client "/CN=CAcert signer handler 1" test_reqClient.cnf
+genserver signer_server "/CN=CAcert signer 1" test_req.cnf
+
+# then the email signing key
+genserver mail "/emailAddress=support@${DOMAIN}" test_reqMail.cnf
+
+keytool -list -keystore ../config/keystore.pkcs12 -storetype pkcs12 -storepass "$PRIVATEPW"
+
+rm test_ca.cnf test_subca.cnf test_req.cnf test_reqMail.cnf test_reqClient.cnf
+rm env.chain.crt
+
+cat root.crt env.crt > ca.crt
+tar cf signer_bundle.tar root.crt env.crt signer_client.crt signer_client.key signer_server.crt signer_server.key ca.crt
+rm ca.crt
diff --git a/keys/generateTruststore.sh b/keys/generateTruststore.sh
new file mode 100755 (executable)
index 0000000..0c5aedc
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/bash
+# this script imports the cacert root certs
+
+rm -f ../config/cacerts.jks
+
+#wget -N http://www.cacert.org/certs/root.crt
+#wget -N http://www.cacert.org/certs/class3.crt
+
+#keytool -importcert -keystore ../config/cacerts.jks -file root.crt -alias root -storepass "changeit" $1
+#keytool -importcert -keystore ../config/cacerts.jks -file class3.crt -alias class3 -storepass "changeit" $1
+
+function import(){
+  keytool -importcert -keystore ../config/cacerts.jks -file "$1.crt" -alias own -storepass "changeit" -alias "$1" $2
+}
+
+import root -noprompt
+import assured
+import unassured
+
+keytool -list -keystore ../config/cacerts.jks -storepass "changeit"
diff --git a/keys/selfsign.config b/keys/selfsign.config
new file mode 100644 (file)
index 0000000..2b0f5a7
--- /dev/null
@@ -0,0 +1,39 @@
+[req]
+distinguished_name=dn
+#req_extensions=ext
+
+[dn]
+[ext]
+subjectAltName=
+
+[ca]
+default_ca=ca1
+
+[ca1]
+new_certs_dir=newcerts
+database=db
+serial=serial
+default_md=sha256
+email_in_dn=salat
+policy=ca1_pol
+#default_days=365
+x509_extensions = v3_ca 
+
+
+
+[ v3_ca ]
+
+basicConstraints        = critical, CA:FALSE
+keyUsage                = critical, digitalSignature, keyEncipherment, keyAgreement
+extendedKeyUsage        = clientAuth, serverAuth, nsSGC, msSGC
+
+
+[ca1_pol]
+commonName              = optional
+subjectAltName          = optional
+organizationName       = optional
+organizationalUnitName = optional
+emailAddress           = optional
+countryName            = optional
+stateOrProvinceName    = optional
+localityName           = optional
diff --git a/lib/scrypt/com/lambdaworks/crypto/PBKDF.java b/lib/scrypt/com/lambdaworks/crypto/PBKDF.java
new file mode 100644 (file)
index 0000000..43dc2f4
--- /dev/null
@@ -0,0 +1,87 @@
+// Copyright (C) 2011 - Will Glozer.  All rights reserved.
+
+package com.lambdaworks.crypto;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.GeneralSecurityException;
+import static java.lang.System.arraycopy;
+
+/**
+ * An implementation of the Password-Based Key Derivation Function as specified
+ * in RFC 2898.
+ *
+ * @author  Will Glozer
+ */
+public class PBKDF {
+    /**
+     * Implementation of PBKDF2 (RFC2898).
+     *
+     * @param   alg     HMAC algorithm to use.
+     * @param   P       Password.
+     * @param   S       Salt.
+     * @param   c       Iteration count.
+     * @param   dkLen   Intended length, in octets, of the derived key.
+     *
+     * @return  The derived key.
+     *
+     * @throws  GeneralSecurityException
+     */
+    public static byte[] pbkdf2(String alg, byte[] P, byte[] S, int c, int dkLen) throws GeneralSecurityException {
+        Mac mac = Mac.getInstance(alg);
+        mac.init(new SecretKeySpec(P, alg));
+        byte[] DK = new byte[dkLen];
+        pbkdf2(mac, S, c, DK, dkLen);
+        return DK;
+    }
+
+    /**
+     * Implementation of PBKDF2 (RFC2898).
+     *
+     * @param   mac     Pre-initialized {@link Mac} instance to use.
+     * @param   S       Salt.
+     * @param   c       Iteration count.
+     * @param   DK      Byte array that derived key will be placed in.
+     * @param   dkLen   Intended length, in octets, of the derived key.
+     *
+     * @throws  GeneralSecurityException
+     */
+    public static void pbkdf2(Mac mac, byte[] S, int c, byte[] DK, int dkLen) throws GeneralSecurityException {
+        int hLen = mac.getMacLength();
+
+        if (dkLen > (Math.pow(2, 32) - 1) * hLen) {
+            throw new GeneralSecurityException("Requested key length too long");
+        }
+
+        byte[] U      = new byte[hLen];
+        byte[] T      = new byte[hLen];
+        byte[] block1 = new byte[S.length + 4];
+
+        int l = (int) Math.ceil((double) dkLen / hLen);
+        int r = dkLen - (l - 1) * hLen;
+
+        arraycopy(S, 0, block1, 0, S.length);
+
+        for (int i = 1; i <= l; i++) {
+            block1[S.length + 0] = (byte) (i >> 24 & 0xff);
+            block1[S.length + 1] = (byte) (i >> 16 & 0xff);
+            block1[S.length + 2] = (byte) (i >> 8  & 0xff);
+            block1[S.length + 3] = (byte) (i >> 0  & 0xff);
+
+            mac.update(block1);
+            mac.doFinal(U, 0);
+            arraycopy(U, 0, T, 0, hLen);
+
+            for (int j = 1; j < c; j++) {
+                mac.update(U);
+                mac.doFinal(U, 0);
+
+                for (int k = 0; k < hLen; k++) {
+                    T[k] ^= U[k];
+                }
+            }
+
+            arraycopy(T, 0, DK, (i - 1) * hLen, (i == l ? r : hLen));
+        }
+    }
+}
diff --git a/lib/scrypt/com/lambdaworks/crypto/SCrypt.java b/lib/scrypt/com/lambdaworks/crypto/SCrypt.java
new file mode 100644 (file)
index 0000000..f2e9789
--- /dev/null
@@ -0,0 +1,266 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.crypto;
+
+import static java.lang.Integer.*;
+import static java.lang.System.*;
+
+import java.security.GeneralSecurityException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An implementation of the <a
+ * href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt</a> key derivation
+ * function. This class will attempt to load a native library containing the
+ * optimized C implementation from <a
+ * href="http://www.tarsnap.com/scrypt.html">http
+ * ://www.tarsnap.com/scrypt.html<a> and fall back to the pure Java version if
+ * that fails.
+ *
+ * @author Will Glozer
+ */
+public class SCrypt {
+
+    private static final boolean native_library_loaded;
+
+    static {
+        // do not load native library
+        native_library_loaded = false;
+    }
+
+    /**
+     * Implementation of the <a
+     * href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt KDF</a>. Calls
+     * the native implementation {@link #scryptN} when the native library was
+     * successfully loaded, otherwise calls {@link #scryptJ}.
+     *
+     * @param passwd
+     *            Password.
+     * @param salt
+     *            Salt.
+     * @param N
+     *            CPU cost parameter.
+     * @param r
+     *            Memory cost parameter.
+     * @param p
+     *            Parallelization parameter.
+     * @param dkLen
+     *            Intended length of the derived key.
+     * @return The derived key.
+     * @throws GeneralSecurityException
+     *             when HMAC_SHA256 is not available.
+     */
+    public static byte[] scrypt(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException {
+        return native_library_loaded ? scryptN(passwd, salt, N, r, p, dkLen) : scryptJ(passwd, salt, N, r, p, dkLen);
+    }
+
+    /**
+     * Native C implementation of the <a
+     * href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt KDF</a> using the
+     * code from <a
+     * href="http://www.tarsnap.com/scrypt.html">http://www.tarsnap.
+     * com/scrypt.html<a>.
+     *
+     * @param passwd
+     *            Password.
+     * @param salt
+     *            Salt.
+     * @param N
+     *            CPU cost parameter.
+     * @param r
+     *            Memory cost parameter.
+     * @param p
+     *            Parallelization parameter.
+     * @param dkLen
+     *            Intended length of the derived key.
+     * @return The derived key.
+     */
+    public static native byte[] scryptN(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen);
+
+    /**
+     * Pure Java implementation of the <a
+     * href="http://www.tarsnap.com/scrypt/scrypt.pdf"/>scrypt KDF</a>.
+     *
+     * @param passwd
+     *            Password.
+     * @param salt
+     *            Salt.
+     * @param N
+     *            CPU cost parameter.
+     * @param r
+     *            Memory cost parameter.
+     * @param p
+     *            Parallelization parameter.
+     * @param dkLen
+     *            Intended length of the derived key.
+     * @return The derived key.
+     * @throws GeneralSecurityException
+     *             when HMAC_SHA256 is not available.
+     */
+    public static byte[] scryptJ(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException {
+        if (N < 2 || (N & (N - 1)) != 0) {
+            throw new IllegalArgumentException("N must be a power of 2 greater than 1");
+        }
+        if (r <= 0) {
+            throw new IllegalArgumentException("Parameter r zero or negative");
+        }
+        if (p <= 0) {
+            throw new IllegalArgumentException("Parameter p zero or negative");
+        }
+
+        if (N > MAX_VALUE / 128 / r) {
+            throw new IllegalArgumentException("Parameter N is too large");
+        }
+        if (r > MAX_VALUE / 128 / p) {
+            throw new IllegalArgumentException("Parameter r is too large");
+        }
+
+        Mac mac = Mac.getInstance("HmacSHA256");
+        mac.init(new SecretKeySpec(passwd, "HmacSHA256"));
+
+        byte[] DK = new byte[dkLen];
+
+        byte[] B = new byte[128 * r * p];
+        byte[] XY = new byte[256 * r];
+        byte[] V = new byte[128 * r * N];
+        int i;
+
+        PBKDF.pbkdf2(mac, salt, 1, B, p * 128 * r);
+
+        for (i = 0; i < p; i++) {
+            smix(B, i * 128 * r, r, N, V, XY);
+        }
+
+        PBKDF.pbkdf2(mac, B, 1, DK, dkLen);
+
+        return DK;
+    }
+
+    public static void smix(byte[] B, int Bi, int r, int N, byte[] V, byte[] XY) {
+        int Xi = 0;
+        int Yi = 128 * r;
+        int i;
+
+        arraycopy(B, Bi, XY, Xi, 128 * r);
+
+        for (i = 0; i < N; i++) {
+            arraycopy(XY, Xi, V, i * (128 * r), 128 * r);
+            blockmix_salsa8(XY, Xi, Yi, r);
+        }
+
+        for (i = 0; i < N; i++) {
+            int j = integerify(XY, Xi, r) & (N - 1);
+            blockxor(V, j * (128 * r), XY, Xi, 128 * r);
+            blockmix_salsa8(XY, Xi, Yi, r);
+        }
+
+        arraycopy(XY, Xi, B, Bi, 128 * r);
+    }
+
+    public static void blockmix_salsa8(byte[] BY, int Bi, int Yi, int r) {
+        byte[] X = new byte[64];
+        int i;
+
+        arraycopy(BY, Bi + (2 * r - 1) * 64, X, 0, 64);
+
+        for (i = 0; i < 2 * r; i++) {
+            blockxor(BY, i * 64, X, 0, 64);
+            salsa20_8(X);
+            arraycopy(X, 0, BY, Yi + (i * 64), 64);
+        }
+
+        for (i = 0; i < r; i++) {
+            arraycopy(BY, Yi + (i * 2) * 64, BY, Bi + (i * 64), 64);
+        }
+
+        for (i = 0; i < r; i++) {
+            arraycopy(BY, Yi + (i * 2 + 1) * 64, BY, Bi + (i + r) * 64, 64);
+        }
+    }
+
+    public static int R(int a, int b) {
+        return (a << b) | (a >>> (32 - b));
+    }
+
+    public static void salsa20_8(byte[] B) {
+        int[] B32 = new int[16];
+        int[] x = new int[16];
+        int i;
+
+        for (i = 0; i < 16; i++) {
+            B32[i] = (B[i * 4 + 0] & 0xff) << 0;
+            B32[i] |= (B[i * 4 + 1] & 0xff) << 8;
+            B32[i] |= (B[i * 4 + 2] & 0xff) << 16;
+            B32[i] |= (B[i * 4 + 3] & 0xff) << 24;
+        }
+
+        arraycopy(B32, 0, x, 0, 16);
+
+        for (i = 8; i > 0; i -= 2) {
+            x[4] ^= R(x[0] + x[12], 7);
+            x[8] ^= R(x[4] + x[0], 9);
+            x[12] ^= R(x[8] + x[4], 13);
+            x[0] ^= R(x[12] + x[8], 18);
+            x[9] ^= R(x[5] + x[1], 7);
+            x[13] ^= R(x[9] + x[5], 9);
+            x[1] ^= R(x[13] + x[9], 13);
+            x[5] ^= R(x[1] + x[13], 18);
+            x[14] ^= R(x[10] + x[6], 7);
+            x[2] ^= R(x[14] + x[10], 9);
+            x[6] ^= R(x[2] + x[14], 13);
+            x[10] ^= R(x[6] + x[2], 18);
+            x[3] ^= R(x[15] + x[11], 7);
+            x[7] ^= R(x[3] + x[15], 9);
+            x[11] ^= R(x[7] + x[3], 13);
+            x[15] ^= R(x[11] + x[7], 18);
+            x[1] ^= R(x[0] + x[3], 7);
+            x[2] ^= R(x[1] + x[0], 9);
+            x[3] ^= R(x[2] + x[1], 13);
+            x[0] ^= R(x[3] + x[2], 18);
+            x[6] ^= R(x[5] + x[4], 7);
+            x[7] ^= R(x[6] + x[5], 9);
+            x[4] ^= R(x[7] + x[6], 13);
+            x[5] ^= R(x[4] + x[7], 18);
+            x[11] ^= R(x[10] + x[9], 7);
+            x[8] ^= R(x[11] + x[10], 9);
+            x[9] ^= R(x[8] + x[11], 13);
+            x[10] ^= R(x[9] + x[8], 18);
+            x[12] ^= R(x[15] + x[14], 7);
+            x[13] ^= R(x[12] + x[15], 9);
+            x[14] ^= R(x[13] + x[12], 13);
+            x[15] ^= R(x[14] + x[13], 18);
+        }
+
+        for (i = 0; i < 16; ++i) {
+            B32[i] = x[i] + B32[i];
+        }
+
+        for (i = 0; i < 16; i++) {
+            B[i * 4 + 0] = (byte) (B32[i] >> 0 & 0xff);
+            B[i * 4 + 1] = (byte) (B32[i] >> 8 & 0xff);
+            B[i * 4 + 2] = (byte) (B32[i] >> 16 & 0xff);
+            B[i * 4 + 3] = (byte) (B32[i] >> 24 & 0xff);
+        }
+    }
+
+    public static void blockxor(byte[] S, int Si, byte[] D, int Di, int len) {
+        for (int i = 0; i < len; i++) {
+            D[Di + i] ^= S[Si + i];
+        }
+    }
+
+    public static int integerify(byte[] B, int Bi, int r) {
+        int n;
+
+        Bi += (2 * r - 1) * 64;
+
+        n = (B[Bi + 0] & 0xff) << 0;
+        n |= (B[Bi + 1] & 0xff) << 8;
+        n |= (B[Bi + 2] & 0xff) << 16;
+        n |= (B[Bi + 3] & 0xff) << 24;
+
+        return n;
+    }
+}
diff --git a/lib/scrypt/com/lambdaworks/crypto/SCryptUtil.java b/lib/scrypt/com/lambdaworks/crypto/SCryptUtil.java
new file mode 100644 (file)
index 0000000..808d69f
--- /dev/null
@@ -0,0 +1,134 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.crypto;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+/**
+ * Simple {@link SCrypt} interface for hashing passwords using the <a
+ * href="http://www.tarsnap.com/scrypt.html">scrypt</a> key derivation function
+ * and comparing a plain text password to a hashed one. The hashed output is an
+ * extended implementation of the Modular Crypt Format that also includes the
+ * scrypt algorithm parameters. Format: <code>$s0$PARAMS$SALT$KEY</code>.
+ * <dl>
+ * <dd>PARAMS</dd>
+ * <dt>32-bit hex integer containing log2(N) (16 bits), r (8 bits), and p (8
+ * bits)</dt>
+ * <dd>SALT</dd>
+ * <dt>base64-encoded salt</dt>
+ * <dd>KEY</dd>
+ * <dt>base64-encoded derived key</dt>
+ * </dl>
+ * <code>s0</code> identifies version 0 of the scrypt format, using a 128-bit
+ * salt and 256-bit derived key.
+ *
+ * @author Will Glozer
+ */
+public class SCryptUtil {
+
+    /**
+     * Hash the supplied plaintext password and generate output in the format
+     * described in {@link SCryptUtil}.
+     *
+     * @param passwd
+     *            Password.
+     * @param N
+     *            CPU cost parameter.
+     * @param r
+     *            Memory cost parameter.
+     * @param p
+     *            Parallelization parameter.
+     * @return The hashed password.
+     */
+    public static String scrypt(String passwd, int N, int r, int p) {
+        try {
+            byte[] salt = new byte[16];
+            SecureRandom.getInstance("SHA1PRNG").nextBytes(salt);
+
+            byte[] derived = SCrypt.scrypt(passwd.getBytes("UTF-8"), salt, N, r, p, 32);
+
+            String params = Long.toString(log2(N) << 16L | r << 8 | p, 16);
+
+            StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2);
+            sb.append("$s0$").append(params).append('$');
+            sb.append(Base64.getEncoder().encodeToString(salt)).append('$');
+            sb.append(Base64.getEncoder().encodeToString(derived));
+
+            return sb.toString();
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalStateException("JVM doesn't support UTF-8?");
+        } catch (GeneralSecurityException e) {
+            throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
+        }
+    }
+
+    /**
+     * Compare the supplied plaintext password to a hashed password.
+     *
+     * @param passwd
+     *            Plaintext password.
+     * @param hashed
+     *            scrypt hashed password.
+     * @return true if passwd matches hashed value.
+     */
+    public static boolean check(String passwd, String hashed) {
+        try {
+            String[] parts = hashed.split("\\$");
+
+            if (parts.length != 5 || !parts[1].equals("s0")) {
+                throw new IllegalArgumentException("Invalid hashed value");
+            }
+
+            long params = Long.parseLong(parts[2], 16);
+            byte[] salt = Base64.getDecoder().decode(parts[3]);
+            byte[] derived0 = Base64.getDecoder().decode(parts[4]);
+
+            int N = (int) Math.pow(2, params >> 16 & 0xffff);
+            int r = (int) params >> 8 & 0xff;
+            int p = (int) params & 0xff;
+            if (r == 0 || p == 0) {
+                return false;
+            }
+
+            byte[] derived1 = SCrypt.scrypt(passwd.getBytes("UTF-8"), salt, N, r, p, 32);
+
+            if (derived0.length != derived1.length) {
+                return false;
+            }
+
+            int result = 0;
+            for (int i = 0; i < derived0.length; i++) {
+                result |= derived0[i] ^ derived1[i];
+            }
+            return result == 0;
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalStateException("JVM doesn't support UTF-8?");
+        } catch (GeneralSecurityException e) {
+            throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
+        }
+    }
+
+    private static int log2(int n) {
+        int log = 0;
+        if ((n & 0xffff0000) != 0) {
+            n >>>= 16;
+            log = 16;
+        }
+        if (n >= 256) {
+            n >>>= 8;
+            log += 8;
+        }
+        if (n >= 16) {
+            n >>>= 4;
+            log += 4;
+        }
+        if (n >= 4) {
+            n >>>= 2;
+            log += 2;
+        }
+        return log + (n >>> 1);
+    }
+}
index 917eade9d33f47ed1376747e86d82e5f4a1528b9..6fe7289d67e3042692c783eda7604da3ab10bde3 100644 (file)
@@ -1,3 +1,3 @@
 *
 !.gitignore
-a
\ No newline at end of file
+a
index b58e400672c0e222c313ab4583d07fb7b872ee46..3fbf922b64918965c626c480189f48f060c50e14 100644 (file)
@@ -1,11 +1,17 @@
+JAVA_HOME=/usr/lib/jvm/default-java
 SYSTEM= $(shell uname | awk '{print tolower($$0)}')
+JAVAH=javah
+CC=gcc
+CFLAGS=-O3 -g -flto -Wall -Werror -Wextra -pedantic -fPIC
+CFLAGS+=-I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/$(SYSTEM)
+LDFLAGS=-shared
 
 
 all: libsetuid.so
 
-libsetuid.so:
-       javah -classpath ../bin/ -jni org.cacert.gigi.natives.SetUID    
-       gcc -fPIC -o libsetuid.so -shared -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/$(SYSTEM) org_cacert_gigi_natives_SetUID.c
+libsetuid.so: org_cacert_gigi_natives_SetUID.c
+       $(JAVAH) -classpath ../bin/ -jni org.cacert.gigi.natives.SetUID && \
+       $(CC) $(CFLAGS) $(LDFLAGS) -o libsetuid.so org_cacert_gigi_natives_SetUID.c
 
 clean:
        rm -f *.so
index f0ae7cb1d5548daeb8435f58880971599e6c8e58..6c94d619a397dfdf891d25136753febe390be827 100644 (file)
@@ -1,36 +1,40 @@
-#include <jni.h>  
+#include <jni.h>
 #include <sys/types.h>
-#include <unistd.h> 
-  
-#ifndef _Included_org_cacert_natives_SetUID  
-#define _Included_org_cacert_natives_SetUID  
-#ifdef __cplusplus  
-extern "C" {  
-#endif  
-  
-jobject getStatus(JNIEnv *env, int successCode, const char * message) {  
-  
-   jstring message_str = (*env)->NewStringUTF(env, message);
-   jboolean success = successCode;  
-   jclass cls = (*env)->FindClass(env, "Lorg/cacert/gigi/natives/SetUID$Status;");  
-   jmethodID constructor = (*env)->GetMethodID(env, cls, "<init>", "(ZLjava/lang/String;)V");  
-   return (*env)->NewObject(env, cls, constructor, success, message_str);  
-}  
-  
-JNIEXPORT jobject JNICALL Java_org_cacert_gigi_natives_SetUID_setUid  
-  (JNIEnv *env, jobject obj, jint uid, jint gid) {  
-         if(setgid((int)gid)) {  
-         return (jobject)getStatus(env, 0, "Error while setting GID.");  
-      } 
-  
-      if(setuid((int)uid)) {
-         return (jobject)getStatus(env, 0, "Error while setting UID.");  
-      }  
-  
-      return (jobject)getStatus(env, 1, "Successfully set uid/gid.");  
-}  
-  
-#ifdef __cplusplus  
-}  
-#endif  
-#endif  
+
+#include <unistd.h>
+
+#ifndef _Included_org_cacert_natives_SetUID
+#define _Included_org_cacert_natives_SetUID
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static jobject getStatus(JNIEnv *env, int successCode, const char * message) {
+    jstring message_str = (*env)->NewStringUTF(env, message);
+    jboolean success = successCode;
+    jclass cls = (*env)->FindClass(env, "Lorg/cacert/gigi/natives/SetUID$Status;");
+    jmethodID constructor = (*env)->GetMethodID(env, cls, "<init>", "(ZLjava/lang/String;)V");
+    return (*env)->NewObject(env, cls, constructor, success, message_str);
+}
+
+JNIEXPORT jobject JNICALL Java_org_cacert_gigi_natives_SetUID_setUid
+        (JNIEnv *env, jobject obj, jint uid, jint gid) {
+
+    /* We don't need the reference for the object/class we are working on */
+    (void)obj;
+
+    if(setgid((int)gid)) {
+        return (jobject)getStatus(env, 0, "Error while setting GID.");
+    }
+
+    if(setuid((int)uid)) {
+        return (jobject)getStatus(env, 0, "Error while setting UID.");
+    }
+
+    return (jobject)getStatus(env, 1, "Successfully set uid/gid.");
+}
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/src/org/cacert/gigi/DevelLauncher.java b/src/org/cacert/gigi/DevelLauncher.java
deleted file mode 100644 (file)
index 74a4ae6..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-package org.cacert.gigi;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.Properties;
-
-import org.kamranzafar.jtar.TarEntry;
-import org.kamranzafar.jtar.TarHeader;
-import org.kamranzafar.jtar.TarOutputStream;
-
-public class DevelLauncher {
-       public static void main(String[] args) throws Exception {
-               Properties mainProps = new Properties();
-               mainProps.load(new FileInputStream("config/gigi.properties"));
-               for (int i = 0; i < args.length; i++) {
-                       if (args[i].equals("--port")) {
-                               mainProps.setProperty("port", args[i + 1]);
-                       }
-                       i++;
-               }
-
-               ByteArrayOutputStream chunkConfig = new ByteArrayOutputStream();
-               DataOutputStream dos = new DataOutputStream(chunkConfig);
-               byte[] cacerts = Files.readAllBytes(Paths.get("config/cacerts.jks"));
-               byte[] keystore = Files.readAllBytes(Paths
-                               .get("config/keystore.pkcs12"));
-
-               DevelLauncher.writeGigiConfig(dos, new byte[]{}, "changeit".getBytes(),
-                               mainProps, cacerts, keystore);
-               dos.flush();
-               InputStream oldin = System.in;
-               System.setIn(new ByteArrayInputStream(chunkConfig.toByteArray()));
-               Launcher.main(args);
-               System.setIn(oldin);
-       }
-       public static void writeGigiConfig(OutputStream target, byte[] keystorepw,
-                       byte[] truststorepw, Properties mainprop, byte[] cacerts,
-                       byte[] keystore) throws IOException {
-               TarOutputStream tos = new TarOutputStream(target);
-               ByteArrayOutputStream baos = new ByteArrayOutputStream();
-               mainprop.store(baos, "");
-
-               putTarEntry(baos.toByteArray(), tos, "gigi.properties");
-               putTarEntry(keystorepw, tos, "keystorepw");
-               putTarEntry(truststorepw, tos, "truststorepw");
-               putTarEntry(keystore, tos, "keystore.pkcs12");
-               putTarEntry(cacerts, tos, "cacerts.jks");
-               tos.close();
-
-       }
-       private static void putTarEntry(byte[] data, TarOutputStream tos,
-                       String name) throws IOException {
-               TarHeader th = new TarHeader();
-               th.name = new StringBuffer(name);
-               th.size = data.length;
-               tos.putNextEntry(new TarEntry(th));
-               tos.write(data);
-       }
-       public static void writeChunk(DataOutputStream dos, byte[] chunk)
-                       throws IOException {
-               dos.writeInt(chunk.length);
-               dos.write(chunk);
-       }
-       public static void launch(Properties props, File cacerts, File keystore)
-                       throws IOException {
-               ByteArrayOutputStream config = new ByteArrayOutputStream();
-               props.store(config, "");
-       }
-}
index cef183495315a748cff19d96c8433d99679cb6ef..b6cb3d7fe5f161b24d9bbd4b05c73fd3b080581f 100644 (file)
@@ -1,13 +1,18 @@
 package org.cacert.gigi;
 
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Properties;
+import java.util.regex.Pattern;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
@@ -16,142 +21,466 @@ import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
 import org.cacert.gigi.database.DatabaseConnection;
-import org.cacert.gigi.email.EmailProvider;
+import org.cacert.gigi.database.DatabaseConnection.Link;
+import org.cacert.gigi.dbObjects.CACertificate;
+import org.cacert.gigi.dbObjects.CATS;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.DomainPingConfiguration;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.Menu;
+import org.cacert.gigi.output.MenuCollector;
+import org.cacert.gigi.output.PageMenuItem;
+import org.cacert.gigi.output.SimpleMenuItem;
+import org.cacert.gigi.output.template.Form.CSRFException;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.AboutPage;
+import org.cacert.gigi.pages.HandlesMixedRequest;
 import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.LogoutPage;
 import org.cacert.gigi.pages.MainPage;
 import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.pages.PasswordResetPage;
+import org.cacert.gigi.pages.RootCertPage;
+import org.cacert.gigi.pages.StaticPage;
 import org.cacert.gigi.pages.TestSecure;
 import org.cacert.gigi.pages.Verify;
-import org.cacert.gigi.pages.account.MailAdd;
-import org.cacert.gigi.pages.account.MailCertificates;
-import org.cacert.gigi.pages.account.MailOverview;
+import org.cacert.gigi.pages.account.ChangePasswordPage;
+import org.cacert.gigi.pages.account.History;
 import org.cacert.gigi.pages.account.MyDetails;
+import org.cacert.gigi.pages.account.UserTrainings;
+import org.cacert.gigi.pages.account.certs.CertificateAdd;
+import org.cacert.gigi.pages.account.certs.Certificates;
+import org.cacert.gigi.pages.account.domain.DomainOverview;
+import org.cacert.gigi.pages.account.mail.MailOverview;
+import org.cacert.gigi.pages.admin.TTPAdminPage;
+import org.cacert.gigi.pages.admin.support.FindDomainPage;
+import org.cacert.gigi.pages.admin.support.FindUserPage;
+import org.cacert.gigi.pages.admin.support.SupportEnterTicketPage;
+import org.cacert.gigi.pages.admin.support.SupportUserDetailsPage;
+import org.cacert.gigi.pages.error.AccessDenied;
+import org.cacert.gigi.pages.error.PageNotFound;
 import org.cacert.gigi.pages.main.RegisterPage;
+import org.cacert.gigi.pages.orga.CreateOrgPage;
+import org.cacert.gigi.pages.orga.ViewOrgPage;
 import org.cacert.gigi.pages.wot.AssurePage;
-import org.eclipse.jetty.util.log.Log;
-
-public class Gigi extends HttpServlet {
-       public static final String LOGGEDIN = "loggedin";
-       public static final String USER = "user";
-       private static final long serialVersionUID = -6386785421902852904L;
-       private String[] baseTemplate;
-       private HashMap<String, Page> pages = new HashMap<String, Page>();
-
-       public Gigi(Properties conf) {
-               EmailProvider.init(conf);
-               DatabaseConnection.init(conf);
-       }
-       @Override
-       public void init() throws ServletException {
-               pages.put("/login", new LoginPage("CACert - Login"));
-               pages.put("/", new MainPage("CACert - Home"));
-               pages.put("/secure", new TestSecure());
-               pages.put(Verify.PATH, new Verify());
-               pages.put(AssurePage.PATH + "/*", new AssurePage());
-               pages.put(MailCertificates.PATH, new MailCertificates());
-               pages.put(MyDetails.PATH, new MyDetails());
-               pages.put(RegisterPage.PATH, new RegisterPage());
-               pages.put(MailOverview.DEFAULT_PATH, new MailOverview(
-                               "My email addresses"));
-               pages.put(MailAdd.DEFAULT_PATH, new MailAdd("Add new email"));
-               String templ = "";
-               try (BufferedReader reader = new BufferedReader(new InputStreamReader(
-                               new FileInputStream(new File("templates/base.html"))))) {
-                       String tmp;
-                       while ((tmp = reader.readLine()) != null) {
-                               templ += tmp + "\n";
-                       }
-                       baseTemplate = templ.split("\\$content\\$");
-               } catch (Exception e) {
-                       Log.getLogger(Gigi.class).warn("Error loading template!", e);
-               }
-               super.init();
-
-       }
-       @Override
-       protected void service(HttpServletRequest req, HttpServletResponse resp)
-                       throws ServletException, IOException {
-               addXSSHeaders(resp);
-               if (req.getHeader("Origin") != null) {
-                       resp.getWriter().println("No cross domain access allowed.");
-                       return;
-               }
-               HttpSession hs = req.getSession();
-               if (req.getPathInfo() != null && req.getPathInfo().equals("/logout")) {
-                       if (hs != null) {
-                               hs.setAttribute(LOGGEDIN, null);
-                               hs.invalidate();
-                       }
-                       resp.sendRedirect("/");
-                       return;
-               }
-
-               Page p = getPage(req.getPathInfo());
-               if (p != null) {
-
-                       if (p.needsLogin() && hs.getAttribute("loggedin") == null) {
-                               String request = req.getPathInfo();
-                               request = request.split("\\?")[0];
-                               hs.setAttribute(LoginPage.LOGIN_RETURNPATH, request);
-                               resp.sendRedirect("/login");
-                               return;
-                       }
-                       if (p.beforeTemplate(req, resp)) {
-                               return;
-                       }
-
-                       String b0 = baseTemplate[0];
-                       b0 = makeDynTempl(b0, p);
-                       resp.setContentType("text/html; charset=utf-8");
-                       resp.getWriter().print(b0);
-                       if (req.getMethod().equals("POST")) {
-                               p.doPost(req, resp);
-                       } else {
-                               p.doGet(req, resp);
-                       }
-                       String b1 = baseTemplate[1];
-                       b1 = makeDynTempl(b1, p);
-                       resp.getWriter().print(b1);
-               } else {
-                       resp.sendError(404, "Page not found.");
-               }
-
-       }
-       private Page getPage(String pathInfo) {
-               if (pathInfo.endsWith("/") && !pathInfo.equals("/")) {
-                       pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
-               }
-               Page page = pages.get(pathInfo);
-               if (page != null) {
-                       return page;
-               }
-               page = pages.get(pathInfo + "/*");
-               if (page != null) {
-                       return page;
-               }
-               int idx = pathInfo.lastIndexOf('/');
-               pathInfo = pathInfo.substring(0, idx);
-
-               page = pages.get(pathInfo + "/*");
-               if (page != null) {
-                       return page;
-               }
-               return null;
-
-       }
-       private String makeDynTempl(String in, Page p) {
-               int year = Calendar.getInstance().get(Calendar.YEAR);
-               in = in.replaceAll("\\$title\\$", p.getTitle());
-               in = in.replaceAll("\\$year\\$", year + "");
-               return in;
-       }
-       public static void addXSSHeaders(HttpServletResponse hsr) {
-               hsr.addHeader("Access-Control-Allow-Origin",
-                               "http://cacert.org https://localhost");
-               hsr.addHeader("Access-Control-Max-Age", "60");
-               hsr.addHeader("Content-Security-Policy",
-                               "default-src 'self' https://www.cacert.org/*;frame-ancestors 'none'");
-               // ;report-uri https://felix.dogcraft.de/report.php
-
-       }
+import org.cacert.gigi.pages.wot.MyListingPage;
+import org.cacert.gigi.pages.wot.MyPoints;
+import org.cacert.gigi.pages.wot.RequestTTPPage;
+import org.cacert.gigi.ping.PingerDaemon;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.ServerConstants;
+
+public final class Gigi extends HttpServlet {
+
+    private class MenuBuilder {
+
+        private LinkedList<Menu> categories = new LinkedList<Menu>();
+
+        private HashMap<String, Page> pages = new HashMap<String, Page>();
+
+        private MenuCollector rootMenu;
+
+        public MenuBuilder() {}
+
+        private void putPage(String path, Page p, String category) {
+            pages.put(path, p);
+            if (category == null) {
+                return;
+            }
+            Menu m = getMenu(category);
+            m.addItem(new PageMenuItem(p, path.replaceFirst("/?\\*$", "")));
+
+        }
+
+        private Menu getMenu(String category) {
+            Menu m = null;
+            for (Menu menu : categories) {
+                if (menu.getMenuName().equals(category)) {
+                    m = menu;
+                    break;
+                }
+            }
+            if (m == null) {
+                m = new Menu(category);
+                categories.add(m);
+            }
+            return m;
+        }
+
+        public MenuCollector generateMenu() throws ServletException {
+            putPage("/denied", new AccessDenied(), null);
+            putPage("/error", new PageNotFound(), null);
+            putPage("/login", new LoginPage(), null);
+            getMenu("SomeCA.org").addItem(new SimpleMenuItem("https://" + ServerConstants.getWwwHostNamePort() + "/login", "Password Login") {
+
+                @Override
+                public boolean isPermitted(AuthorizationContext ac) {
+                    return ac == null;
+                }
+            });
+            getMenu("SomeCA.org").addItem(new SimpleMenuItem("https://" + ServerConstants.getSecureHostNamePort() + "/login", "Certificate Login") {
+
+                @Override
+                public boolean isPermitted(AuthorizationContext ac) {
+                    return ac == null;
+                }
+            });
+            putPage("/", new MainPage(), null);
+            putPage("/roots", new RootCertPage(truststore), "SomeCA.org");
+            putPage("/about", new AboutPage(), "SomeCA.org");
+
+            putPage("/secure", new TestSecure(), null);
+            putPage(Verify.PATH, new Verify(), null);
+            putPage(Certificates.PATH + "/*", new Certificates(), "Certificates");
+            putPage(RegisterPage.PATH, new RegisterPage(), "SomeCA.org");
+            putPage(CertificateAdd.PATH, new CertificateAdd(), "Certificates");
+            putPage(MailOverview.DEFAULT_PATH, new MailOverview(), "Certificates");
+            putPage(DomainOverview.PATH + "*", new DomainOverview(), "Certificates");
+
+            putPage(AssurePage.PATH + "/*", new AssurePage(), "Web of Trust");
+            putPage(MyPoints.PATH, new MyPoints(), "Web of Trust");
+            putPage(MyListingPage.PATH, new MyListingPage(), "Web of Trust");
+            putPage(RequestTTPPage.PATH, new RequestTTPPage(), "Web of Trust");
+
+            putPage(TTPAdminPage.PATH + "/*", new TTPAdminPage(), "Admin");
+            putPage(CreateOrgPage.DEFAULT_PATH, new CreateOrgPage(), "Organisation Admin");
+            putPage(ViewOrgPage.DEFAULT_PATH + "/*", new ViewOrgPage(), "Organisation Admin");
+
+            putPage(SupportEnterTicketPage.PATH, new SupportEnterTicketPage(), "Support Console");
+            putPage(FindUserPage.PATH, new FindUserPage(), "Support Console");
+            putPage(FindDomainPage.PATH, new FindDomainPage(), "Support Console");
+
+            putPage(SupportUserDetailsPage.PATH + "*", new SupportUserDetailsPage(), null);
+            putPage(ChangePasswordPage.PATH, new ChangePasswordPage(), "My Account");
+            putPage(LogoutPage.PATH, new LogoutPage(), "My Account");
+            putPage(History.PATH, new History(false), "My Account");
+            putPage(History.SUPPORT_PATH, new History(true), null);
+            putPage(UserTrainings.PATH, new UserTrainings(false), "My Account");
+            putPage(MyDetails.PATH, new MyDetails(), "My Account");
+            putPage(UserTrainings.SUPPORT_PATH, new UserTrainings(true), null);
+
+            putPage(PasswordResetPage.PATH, new PasswordResetPage(), null);
+
+            if (testing) {
+                try {
+                    Class<?> manager = Class.forName("org.cacert.gigi.pages.Manager");
+                    Page p = (Page) manager.getMethod("getInstance").invoke(null);
+                    String pa = (String) manager.getField("PATH").get(null);
+                    putPage(pa + "/*", p, "Gigi test server");
+                } catch (ReflectiveOperationException e) {
+                    e.printStackTrace();
+                }
+            }
+
+            try {
+                putPage("/wot/rules", new StaticPage("Web of Trust Rules", AssurePage.class.getResourceAsStream("Rules.templ")), "Web of Trust");
+            } catch (UnsupportedEncodingException e) {
+                throw new ServletException(e);
+            }
+            baseTemplate = new Template(Gigi.class.getResource("Gigi.templ"));
+            rootMenu = new MenuCollector();
+
+            Menu languages = new Menu("Language");
+            for (Locale l : Language.getSupportedLocales()) {
+                languages.addItem(new SimpleMenuItem("?lang=" + l.toString(), l.getDisplayName(l)));
+            }
+            categories.add(languages);
+            for (Menu menu : categories) {
+                menu.prepare();
+                rootMenu.put(menu);
+            }
+
+            // rootMenu.prepare();
+            return rootMenu;
+        }
+
+        public Map<String, Page> getPages() {
+            return Collections.unmodifiableMap(pages);
+        }
+    }
+
+    public static final String LOGGEDIN = "loggedin";
+
+    public static final String CERT_SERIAL = "org.cacert.gigi.serial";
+
+    public static final String CERT_ISSUER = "org.cacert.gigi.issuer";
+
+    public static final String AUTH_CONTEXT = "auth";
+
+    public static final String LOGIN_METHOD = "org.cacert.gigi.loginMethod";
+
+    private static final long serialVersionUID = -6386785421902852904L;
+
+    private static Gigi instance;
+
+    private Template baseTemplate;
+
+    private PingerDaemon pinger;
+
+    private KeyStore truststore;
+
+    private boolean testing;
+
+    private MenuCollector rootMenu;
+
+    private Map<String, Page> pages;
+
+    private boolean firstInstanceInited = false;
+
+    public Gigi(Properties conf, KeyStore truststore) {
+        synchronized (Gigi.class) {
+            if (instance != null) {
+                throw new IllegalStateException("Multiple Gigi instances!");
+            }
+            testing = conf.getProperty("testing") != null;
+            instance = this;
+            DatabaseConnection.init(conf);
+            this.truststore = truststore;
+            pinger = new PingerDaemon(truststore);
+            pinger.start();
+        }
+    }
+
+    @Override
+    public synchronized void init() throws ServletException {
+        if (firstInstanceInited) {
+            super.init();
+            return;
+        }
+        // ensure those static initializers are finished
+        try (Link l = DatabaseConnection.newLink(false)) {
+            CACertificate.getById(1);
+            CertificateProfile.getById(1);
+            CATS.getID(CATS.ASSURER_CHALLENGE_NAME);
+        } catch (InterruptedException e) {
+            throw new Error(e);
+        }
+
+        MenuBuilder mb = new MenuBuilder();
+        rootMenu = mb.generateMenu();
+        pages = mb.getPages();
+
+        firstInstanceInited = true;
+        super.init();
+    }
+
+    private Page getPage(String pathInfo) {
+        if (pathInfo.endsWith("/") && !pathInfo.equals("/")) {
+            pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
+        }
+        Page page = pages.get(pathInfo);
+        if (page != null) {
+            return page;
+        }
+        page = pages.get(pathInfo + "/*");
+        if (page != null) {
+            return page;
+        }
+        int idx = pathInfo.lastIndexOf('/');
+        if (idx == -1 || idx == 0) {
+            return null;
+        }
+
+        page = pages.get(pathInfo.substring(0, idx) + "/*");
+        if (page != null) {
+            return page;
+        }
+        int lIdx = pathInfo.lastIndexOf('/', idx - 1);
+        if (lIdx == -1) {
+            return null;
+        }
+        String lastResort = pathInfo.substring(0, lIdx) + "/*" + pathInfo.substring(idx);
+        page = pages.get(lastResort);
+        return page;
+
+    }
+
+    private static String staticTemplateVarHttp = "http://" + ServerConstants.getStaticHostNamePort();
+
+    private static String staticTemplateVarHttps = "https://" + ServerConstants.getStaticHostNamePortSecure();
+
+    private static String getStaticTemplateVar(boolean https) {
+        if (https) {
+            return staticTemplateVarHttps;
+        } else {
+            return staticTemplateVarHttp;
+        }
+    }
+
+    @Override
+    protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        if ("/error".equals(req.getPathInfo()) || "/denied".equals(req.getPathInfo())) {
+            if (DatabaseConnection.hasInstance()) {
+                serviceWithConnection(req, resp);
+                return;
+            }
+        }
+        try (DatabaseConnection.Link l = DatabaseConnection.newLink( !req.getMethod().equals("POST"))) {
+            serviceWithConnection(req, resp);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    protected void serviceWithConnection(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        boolean isSecure = req.isSecure();
+        addXSSHeaders(resp, isSecure);
+        // Firefox only sends this, if it's a cross domain access; safari sends
+        // it always
+        String originHeader = req.getHeader("Origin");
+        if (originHeader != null //
+                && !(originHeader.matches("^" + Pattern.quote("https://" + ServerConstants.getWwwHostNamePortSecure()) + "(/.*|)") || //
+                        originHeader.matches("^" + Pattern.quote("http://" + ServerConstants.getWwwHostNamePort()) + "(/.*|)") || //
+                        originHeader.matches("^" + Pattern.quote("https://" + ServerConstants.getSecureHostNamePort()) + "(/.*|)"))) {
+            resp.setContentType("text/html; charset=utf-8");
+            resp.getWriter().println("<html><head><title>Alert</title></head><body>No cross domain access allowed.<br/><b>If you don't know why you're seeing this you may have been fished! Please change your password immediately!</b></body></html>");
+            return;
+        }
+        HttpSession hs = req.getSession();
+        String clientSerial = (String) hs.getAttribute(CERT_SERIAL);
+        if (clientSerial != null) {
+            X509Certificate[] cert = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
+            if (cert == null || cert[0] == null//
+                    || !cert[0].getSerialNumber().toString(16).toUpperCase().equals(clientSerial) //
+                    || !cert[0].getIssuerDN().equals(hs.getAttribute(CERT_ISSUER))) {
+                hs.invalidate();
+                resp.sendError(403, "Certificate mismatch.");
+                return;
+            }
+
+        }
+        if (req.getParameter("lang") != null) {
+            Locale l = Language.getLocaleFromString(req.getParameter("lang"));
+            Language lu = Language.getInstance(l);
+            req.getSession().setAttribute(Language.SESSION_ATTRIB_NAME, lu.getLocale());
+        }
+        final Page p = getPage(req.getPathInfo());
+
+        if (p != null) {
+            if ( !isSecure && (p.needsLogin() || p instanceof LoginPage || p instanceof RegisterPage)) {
+                resp.sendRedirect("https://" + ServerConstants.getWwwHostNamePortSecure() + req.getPathInfo());
+                return;
+            }
+            AuthorizationContext currentAuthContext = LoginPage.getAuthorizationContext(req);
+            if ( !p.isPermitted(currentAuthContext)) {
+                if (hs.getAttribute("loggedin") == null) {
+                    String request = req.getPathInfo();
+                    request = request.split("\\?")[0];
+                    hs.setAttribute(LoginPage.LOGIN_RETURNPATH, request);
+                    resp.sendRedirect("/login");
+                    return;
+                }
+                resp.sendError(403);
+                return;
+            }
+            if (p.beforeTemplate(req, resp)) {
+                return;
+            }
+            HashMap<String, Object> vars = new HashMap<String, Object>();
+            // System.out.println(req.getMethod() + ": " + req.getPathInfo() +
+            // " -> " + p);
+            Outputable content = new Outputable() {
+
+                @Override
+                public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+                    try {
+                        if (req.getMethod().equals("POST")) {
+                            if (req.getQueryString() != null && !(p instanceof HandlesMixedRequest)) {
+                                return;
+                            }
+                            p.doPost(req, resp);
+                        } else {
+                            p.doGet(req, resp);
+                        }
+                    } catch (CSRFException err) {
+                        try {
+                            resp.sendError(500, "CSRF invalid");
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                        }
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+
+                }
+            };
+            Language lang = Page.getLanguage(req);
+
+            vars.put(Menu.AUTH_VALUE, currentAuthContext);
+            vars.put("menu", rootMenu);
+            vars.put("title", lang.getTranslation(p.getTitle()));
+            vars.put("static", getStaticTemplateVar(isSecure));
+            vars.put("year", Calendar.getInstance().get(Calendar.YEAR));
+            vars.put("content", content);
+            if (currentAuthContext != null) {
+                // TODO maybe move this information into the AuthContext object
+                vars.put("loginMethod", req.getSession().getAttribute(LOGIN_METHOD));
+                vars.put("authContext", currentAuthContext);
+
+            }
+            resp.setContentType("text/html; charset=utf-8");
+            baseTemplate.output(resp.getWriter(), lang, vars);
+        } else {
+            resp.sendError(404, "Page not found.");
+        }
+
+    }
+
+    public static void addXSSHeaders(HttpServletResponse hsr, boolean doHttps) {
+        hsr.addHeader("Access-Control-Allow-Origin", "https://" + ServerConstants.getWwwHostNamePortSecure() + " https://" + ServerConstants.getSecureHostNamePort());
+        hsr.addHeader("Access-Control-Max-Age", "60");
+        if (doHttps) {
+            hsr.addHeader("Content-Security-Policy", httpsCSP);
+        } else {
+            hsr.addHeader("Content-Security-Policy", httpCSP);
+        }
+        hsr.addHeader("Strict-Transport-Security", "max-age=31536000");
+
+    }
+
+    private static String httpsCSP = genHttpsCSP();
+
+    private static String httpCSP = genHttpCSP();
+
+    private static String genHttpsCSP() {
+        StringBuffer csp = new StringBuffer();
+        csp.append("default-src 'none'");
+        csp.append(";font-src https://" + ServerConstants.getStaticHostNamePortSecure());
+        csp.append(";img-src https://" + ServerConstants.getStaticHostNamePortSecure());
+        csp.append(";media-src 'none'; object-src 'none'");
+        csp.append(";script-src https://" + ServerConstants.getStaticHostNamePortSecure());
+        csp.append(";style-src https://" + ServerConstants.getStaticHostNamePortSecure());
+        csp.append(";form-action https://" + ServerConstants.getSecureHostNamePort() + " https://" + ServerConstants.getWwwHostNamePortSecure());
+        // csp.append(";report-url https://api.cacert.org/security/csp/report");
+        return csp.toString();
+    }
+
+    private static String genHttpCSP() {
+        StringBuffer csp = new StringBuffer();
+        csp.append("default-src 'none'");
+        csp.append(";font-src http://" + ServerConstants.getStaticHostNamePort());
+        csp.append(";img-src http://" + ServerConstants.getStaticHostNamePort());
+        csp.append(";media-src 'none'; object-src 'none'");
+        csp.append(";script-src http://" + ServerConstants.getStaticHostNamePort());
+        csp.append(";style-src http://" + ServerConstants.getStaticHostNamePort());
+        csp.append(";form-action https://" + ServerConstants.getSecureHostNamePort() + " https://" + ServerConstants.getWwwHostNamePort());
+        // csp.append(";report-url http://api.cacert.org/security/csp/report");
+        return csp.toString();
+    }
+
+    /**
+     * Requests Pinging of domains.
+     * 
+     * @param toReping
+     *            if not null, the {@link DomainPingConfiguration} to test, if
+     *            null, just re-check if there is something to do.
+     */
+    public static void notifyPinger(DomainPingConfiguration toReping) {
+        if (toReping != null) {
+            instance.pinger.queue(toReping);
+        }
+        instance.pinger.interrupt();
+    }
+
 }
diff --git a/src/org/cacert/gigi/Gigi.templ b/src/org/cacert/gigi/Gigi.templ
new file mode 100644 (file)
index 0000000..a163c7d
--- /dev/null
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <!-- <link rel="alternate" type="application/rss+xml" title="Newsfeed" href="//blog.SomeCA.org/feed"> -->
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+        <title><?=_SomeCA?> - <?=$title?></title>
+        <meta name="description" content="">
+        <link rel="stylesheet" href="<?=$static?>/css/jquery-ui-timepicker-addon.css">
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+
+        <link rel="stylesheet" href="<?=$static?>/css/bootstrap.min.css">
+        <link rel="stylesheet" href="<?=$static?>/css/cacert.css">
+
+        <script src="<?=$static?>/js/jquery.min.js"></script>
+        <script src="<?=$static?>/js/bootstrap.min.js"></script>
+        <script src="<?=$static?>/js/expert.js"></script>
+        <script src="<?=$static?>/js/localDate.js"></script>
+    </head>
+<body>
+<nav class="navbar navbar-default">
+  <div class="container-fluid">
+    <!-- Brand and toggle get grouped for better mobile display -->
+    <div class="navbar-header">
+      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#main-nav" aria-expanded="false">
+        <span class="sr-only"><?=_Toggle navigation?></span>
+        <span class="icon-bar"></span>
+        <span class="icon-bar"></span>
+        <span class="icon-bar"></span>
+      </button>
+      <a class="navbar-brand" href="/"><?=_SomeCA Frontend?></a>
+    </div>
+
+    <!-- Collect the nav links, forms, and other content for toggling -->
+    <div class="collapse navbar-collapse" id="main-nav">
+      <ul class="nav navbar-nav">
+        <?=$menu?>
+      </ul>
+      <ul class="nav navbra-nav navbar-right">
+      <li><p class="navbar-text"><? if($authContext) { ?><?=$authContext?><? } ?></p></li>
+      </ul>
+    </div><!-- /.navbar-collapse -->
+  </div><!-- /.container-fluid -->
+</nav>
+       <div class="container">
+               
+                       <h1 class="page-header"><?=$title?></h1>
+                       <div class="content"><?=$content?></div>
+       </div>
+       <hr/>
+               <div id="siteInfo">
+            Page info
+                       <!-- <a href="//wiki.cacert.org/FAQ/AboutUs"><?=_About Us?></a> |
+                       <a href="/index.php?id=13"><?=_Donations?></a> |
+                       <a href="//wiki.cacert.org/wiki/CAcertIncorporated"><?=_Association Membership?></a> |
+                       <a href="/policy/PrivacyPolicy.html"><?=_Privacy Policy?></a> |
+                       <a href="/index.php?id=51"><?=_Mission Statement?></a> |
+                       <a href="/index.php?id=11"><?=_Contact Us?></a> --> |
+                       ©2016-<?=$year?> SomeCA
+               </div>
+
+</body>
+</html>
diff --git a/src/org/cacert/gigi/GigiApiException.java b/src/org/cacert/gigi/GigiApiException.java
new file mode 100644 (file)
index 0000000..0d600b9
--- /dev/null
@@ -0,0 +1,113 @@
+package org.cacert.gigi;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Locale;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.TranslateCommand;
+
+public class GigiApiException extends Exception {
+
+    private static final Language PLAIN_LANGUAGE = Language.getInstance(Locale.ENGLISH);
+
+    private static final long serialVersionUID = -164928670180852588L;
+
+    private SQLException e;
+
+    private LinkedList<Outputable> messages = new LinkedList<>();
+
+    public GigiApiException(SQLException e) {
+        super(e);
+        this.e = e;
+    }
+
+    public GigiApiException(String message) {
+        super(message);
+        messages.add(new TranslateCommand(message));
+    }
+
+    public GigiApiException() {
+
+    }
+
+    public GigiApiException(Outputable out) {
+        messages.add(out);
+    }
+
+    public void mergeInto(GigiApiException e2) {
+        messages.addAll(e2.messages);
+        if (e == null) {
+            e = e2.e;
+        }
+    }
+
+    public boolean isInternalError() {
+        return e != null;
+    }
+
+    public void format(PrintWriter out, Language language) {
+        out.println("<div class='formError'>");
+        if (isInternalError()) {
+            e.printStackTrace();
+            out.print("<div>");
+            out.println(language.getTranslation("An internal error occurred."));
+            out.println("</div>");
+        }
+        HashMap<String, Object> map = new HashMap<>();
+        for (Outputable message : messages) {
+            map.clear();
+
+            out.print("<div>");
+            message.output(out, language, map);
+            out.println("</div>");
+        }
+        out.println("</div>");
+
+    }
+
+    public void formatPlain(PrintWriter out) {
+        if (isInternalError()) {
+            out.println(PLAIN_LANGUAGE.getTranslation("An internal error occurred."));
+        }
+        HashMap<String, Object> map = new HashMap<>();
+        for (Outputable message : messages) {
+            if (message instanceof TranslateCommand) {
+                String m = ((TranslateCommand) message).getRaw();
+                // Skip HTML Entities
+                out.println(PLAIN_LANGUAGE.getTranslation(m));
+            } else {
+                map.clear();
+                message.output(out, PLAIN_LANGUAGE, map);
+                out.println();
+            }
+        }
+    }
+
+    public boolean isEmpty() {
+        return e == null && messages.size() == 0;
+    }
+
+    @Override
+    public String getMessage() {
+        if (messages.size() != 0) {
+            StringWriter sw = new StringWriter();
+            PrintWriter pw = new PrintWriter(sw);
+
+            HashMap<String, Object> map = new HashMap<>();
+            for (Outputable message : messages) {
+                map.clear();
+                message.output(pw, PLAIN_LANGUAGE, map);
+            }
+            pw.flush();
+
+            return sw.toString();
+        }
+        return "";
+    }
+
+}
index 69c95bbdea18f54e817f7a2ec2062aa46187f4d2..8b7c220202f83e8b3b07724fb57824c9fb49f368 100644 (file)
@@ -12,76 +12,92 @@ import org.kamranzafar.jtar.TarEntry;
 import org.kamranzafar.jtar.TarInputStream;
 
 public class GigiConfig {
-       public static final String GIGI_CONFIG_VERSION = "GigiConfigV1.0";
-       byte[] cacerts;
-       byte[] keystore;
-       Properties mainProps = new Properties();
-       private char[] keystorpw;
-       private char[] truststorepw;
-
-       private GigiConfig() {
-       }
-       public byte[] getCacerts() {
-               return cacerts;
-       }
-       public byte[] getKeystore() {
-               return keystore;
-       }
-       public Properties getMainProps() {
-               return mainProps;
-       }
-
-       public static GigiConfig parse(InputStream input) throws IOException {
-               TarInputStream tis = new TarInputStream(input);
-               TarEntry t;
-               GigiConfig gc = new GigiConfig();
-               while ((t = tis.getNextEntry()) != null) {
-                       if (t.getName().equals("gigi.properties")) {
-                               gc.mainProps.load(tis);
-                       } else if (t.getName().equals("cacerts.jks")) {
-                               gc.cacerts = readFully(tis);
-                       } else if (t.getName().equals("keystore.pkcs12")) {
-                               gc.keystore = readFully(tis);
-                       } else if (t.getName().equals("keystorepw")) {
-                               gc.keystorpw = transformSafe(readFully(tis));
-                       } else if (t.getName().equals("truststorepw")) {
-                               gc.truststorepw = transformSafe(readFully(tis));
-                       } else {
-                               System.out.println("Unknown config: " + t.getName());
-                       }
-               }
-               tis.close();
-               return gc;
-       }
-       public static byte[] readFully(InputStream is) throws IOException {
-               ByteArrayOutputStream baos = new ByteArrayOutputStream();
-               byte[] buffer = new byte[1024];
-               int len = 0;
-               while ((len = is.read(buffer)) > 0) {
-                       baos.write(buffer, 0, len);
-               }
-               baos.close();
-               return baos.toByteArray();
-       }
-       private static char[] transformSafe(byte[] readChunk) {
-               char[] res = new char[readChunk.length];
-               for (int i = 0; i < res.length; i++) {
-                       res[i] = (char) readChunk[i];
-                       readChunk[i] = 0;
-               }
-               return res;
-       }
-
-       public KeyStore getPrivateStore() throws GeneralSecurityException,
-                       IOException {
-               KeyStore ks1 = KeyStore.getInstance("pkcs12");
-               ks1.load(new ByteArrayInputStream(keystore), keystorpw);
-               return ks1;
-       }
-       public KeyStore getTrustStore() throws GeneralSecurityException,
-                       IOException {
-               KeyStore ks1 = KeyStore.getInstance("jks");
-               ks1.load(new ByteArrayInputStream(cacerts), truststorepw);
-               return ks1;
-       }
+
+    public static final String GIGI_CONFIG_VERSION = "GigiConfigV1.0";
+
+    private byte[] cacerts;
+
+    private byte[] keystore;
+
+    private Properties mainProps = new Properties();
+
+    private char[] keystorpw;
+
+    private char[] truststorepw;
+
+    private GigiConfig() {}
+
+    public byte[] getCacerts() {
+        return cacerts;
+    }
+
+    public byte[] getKeystore() {
+        return keystore;
+    }
+
+    public Properties getMainProps() {
+        return mainProps;
+    }
+
+    public static GigiConfig parse(InputStream input) throws IOException {
+        TarInputStream tis = new TarInputStream(input);
+        TarEntry t;
+        GigiConfig gc = new GigiConfig();
+        while ((t = tis.getNextEntry()) != null) {
+            if (t.getName().equals("gigi.properties")) {
+                gc.mainProps.load(tis);
+            } else if (t.getName().equals("cacerts.jks")) {
+                gc.cacerts = readFully(tis);
+            } else if (t.getName().equals("keystore.pkcs12")) {
+                gc.keystore = readFully(tis);
+            } else if (t.getName().equals("keystorepw")) {
+                gc.keystorpw = transformSafe(readFully(tis));
+            } else if (t.getName().equals("truststorepw")) {
+                gc.truststorepw = transformSafe(readFully(tis));
+            } else {
+                System.out.println("Unknown config: " + t.getName());
+            }
+        }
+        tis.close();
+        return gc;
+    }
+
+    public static byte[] readFully(InputStream is) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int len = 0;
+        while ((len = is.read(buffer)) > 0) {
+            baos.write(buffer, 0, len);
+        }
+        baos.close();
+        return baos.toByteArray();
+    }
+
+    private static char[] transformSafe(byte[] readChunk) {
+        char[] res = new char[readChunk.length];
+        for (int i = 0; i < res.length; i++) {
+            res[i] = (char) readChunk[i];
+            readChunk[i] = 0;
+        }
+        return res;
+    }
+
+    public KeyStore getPrivateStore() throws GeneralSecurityException, IOException {
+        if (keystore == null || keystorpw == null) {
+            return null;
+        }
+        KeyStore ks1 = KeyStore.getInstance("pkcs12");
+        ks1.load(new ByteArrayInputStream(keystore), keystorpw);
+        return ks1;
+    }
+
+    public KeyStore getTrustStore() throws GeneralSecurityException, IOException {
+        KeyStore ks1 = KeyStore.getInstance("jks");
+        ks1.load(new ByteArrayInputStream(cacerts), truststorepw);
+        return ks1;
+    }
+
+    public String getPrivateStorePw() {
+        return new String(keystorpw);
+    }
 }
diff --git a/src/org/cacert/gigi/Language.java b/src/org/cacert/gigi/Language.java
deleted file mode 100644 (file)
index 5841c3c..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-package org.cacert.gigi;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Locale;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
-
-public class Language {
-       private static HashMap<String, Language> langs = new HashMap<String, Language>();
-       HashMap<String, String> translations = new HashMap<String, String>();
-       Locale l;
-       private Language(String language) throws ParserConfigurationException,
-                       IOException, SAXException {
-               if (language.contains("_")) {
-                       String[] parts = language.split("_");
-                       l = new Locale(parts[0], parts[1]);
-               } else {
-                       l = new Locale(language);
-               }
-
-               DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-               DocumentBuilder db = dbf.newDocumentBuilder();
-               Document d = db.parse(new FileInputStream(new File("locale", language
-                               + ".xml")));
-               NodeList nl = d.getDocumentElement().getChildNodes();
-               for (int i = 0; i < nl.getLength(); i++) {
-                       if (!(nl.item(i) instanceof Element)) {
-                               continue;
-                       }
-                       Element e = (Element) nl.item(i);
-                       Element id = (Element) e.getElementsByTagName("id").item(0);
-                       Element msg = (Element) e.getElementsByTagName("msg").item(0);
-                       translations.put(id.getTextContent(), msg.getTextContent());
-               }
-               System.out.println(translations.size() + " strings loaded.");
-       }
-       public String getTranslation(String text) {
-               String string = translations.get(text);
-               if (string == null || string.equals("")) {
-                       return text;
-               }
-               return string;
-       }
-       public static Language getInstance(String language) {
-               Language l = langs.get(language);
-               if (l == null) {
-                       try {
-                               l = new Language(language);
-                               langs.put(language, l);
-                       } catch (ParserConfigurationException e) {
-                               e.printStackTrace();
-                       } catch (IOException e) {
-                               e.printStackTrace();
-                       } catch (SAXException e) {
-                               e.printStackTrace();
-                       }
-               }
-               return l;
-       }
-       public Locale getLocale() {
-               return l;
-       }
-
-}
index 6c234909f5714b3fe55df18cf6bac43afcf44c21..7bf5bd119554387739d8b07a2647342cde3f82df 100644 (file)
@@ -1,18 +1,46 @@
 package org.cacert.gigi;
+
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.net.InetSocketAddress;
 import java.security.GeneralSecurityException;
+import java.security.Key;
 import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Locale;
 import java.util.Properties;
+import java.util.TimeZone;
 
+import javax.net.ssl.ExtendedSSLSession;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLParameters;
-import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.SSLSession;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.api.GigiAPI;
+import org.cacert.gigi.email.EmailProvider;
 import org.cacert.gigi.natives.SetUID;
 import org.cacert.gigi.util.CipherInfo;
+import org.cacert.gigi.util.PEM;
+import org.cacert.gigi.util.ServerConstants;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpVersion;
 import org.eclipse.jetty.server.Connector;
 import org.eclipse.jetty.server.Handler;
 import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConfiguration.Customizer;
 import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.SecureRequestCustomizer;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
@@ -22,100 +50,317 @@ import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.handler.HandlerList;
 import org.eclipse.jetty.server.handler.HandlerWrapper;
 import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.resource.Resource;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 
 public class Launcher {
-       public static void main(String[] args) throws Exception {
-               GigiConfig conf = GigiConfig.parse(System.in);
-
-               Server s = new Server();
-               // === SSL HTTP Configuration ===
-               HttpConfiguration https_config = new HttpConfiguration();
-               https_config.setSendServerVersion(false);
-               https_config.setSendXPoweredBy(false);
-
-               // for client-cert auth
-               https_config.addCustomizer(new SecureRequestCustomizer());
-
-               ServerConnector connector = new ServerConnector(s,
-                               new SslConnectionFactory(generateSSLContextFactory(conf),
-                                               "http/1.1"), new HttpConnectionFactory(https_config));
-               connector.setHost(conf.getMainProps().getProperty("host"));
-               connector.setPort(Integer.parseInt(conf.getMainProps().getProperty(
-                               "port")));
-               s.setConnectors(new Connector[]{connector});
-
-               HandlerList hl = new HandlerList();
-               hl.setHandlers(new Handler[]{generateStaticContext(),
-                               generateGigiContext(conf.getMainProps())});
-               s.setHandler(hl);
-               s.start();
-               if (connector.getPort() <= 1024
-                               && !System.getProperty("os.name").toLowerCase().contains("win")) {
-                       SetUID uid = new SetUID();
-                       if (!uid.setUid(65536 - 2, 65536 - 2).getSuccess()) {
-                               Log.getLogger(Launcher.class).warn("Couldn't set uid!");
-                       }
-               }
-       }
-
-       private static ServletContextHandler generateGigiContext(Properties conf) {
-               ServletContextHandler servlet = new ServletContextHandler(
-                               ServletContextHandler.SESSIONS);
-               servlet.setInitParameter(SessionManager.__SessionCookieProperty,
-                               "CACert-Session");
-               servlet.addServlet(new ServletHolder(new Gigi(conf)), "/*");
-               return servlet;
-       }
-
-       private static Handler generateStaticContext() {
-               final ResourceHandler rh = new ResourceHandler();
-               rh.setResourceBase("static");
-               HandlerWrapper hw = new PolicyRedirector();
-               hw.setHandler(rh);
-
-               ContextHandler ch = new ContextHandler();
-               ch.setContextPath("/static");
-               ch.setHandler(hw);
-
-               return ch;
-       }
-
-       private static SslContextFactory generateSSLContextFactory(GigiConfig conf)
-                       throws GeneralSecurityException, IOException {
-               TrustManagerFactory tmFactory = TrustManagerFactory.getInstance("PKIX");
-               tmFactory.init((KeyStore) null);
-
-               SslContextFactory scf = new SslContextFactory() {
-
-                       String[] ciphers = null;
-
-                       @Override
-                       public void customize(SSLEngine sslEngine) {
-                               super.customize(sslEngine);
-
-                               SSLParameters ssl = sslEngine.getSSLParameters();
-                               ssl.setUseCipherSuitesOrder(true);
-                               if (ciphers == null) {
-                                       ciphers = CipherInfo.filter(sslEngine
-                                                       .getSupportedCipherSuites());
-                               }
-
-                               ssl.setCipherSuites(ciphers);
-                               sslEngine.setSSLParameters(ssl);
-
-                       }
-
-               };
-               scf.setRenegotiationAllowed(false);
-               scf.setWantClientAuth(true);
-
-               scf.setProtocol("TLS");
-               scf.setTrustStore(conf.getTrustStore());
-               scf.setKeyStore(conf.getPrivateStore());
-               return scf;
-       }
+
+    class ExtendedForwarded implements Customizer {
+
+        @Override
+        public void customize(Connector connector, HttpConfiguration config, Request request) {
+            HttpFields httpFields = request.getHttpFields();
+
+            String ip = httpFields.getStringField("X-Real-IP");
+            String proto = httpFields.getStringField("X-Real-Proto");
+            String cert = httpFields.getStringField("X-Client-Cert");
+            request.setSecure("https".equals(proto));
+            request.setScheme(proto);
+            if ( !"https".equals(proto)) {
+                cert = null;
+
+            }
+            if (cert != null) {
+                X509Certificate[] certs = new X509Certificate[1];
+                try {
+                    certs[0] = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(PEM.decode("CERTIFICATE", cert)));
+                    request.setAttribute("javax.servlet.request.X509Certificate", certs);
+                } catch (CertificateException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (ip != null) {
+                String[] parts = ip.split(":");
+                if (parts.length == 2) {
+                    request.setRemoteAddr(InetSocketAddress.createUnresolved(parts[0], Integer.parseInt(parts[1])));
+                }
+            }
+
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        System.setProperty("jdk.tls.ephemeralDHKeySize", "4096");
+        new Launcher().boot();
+    }
+
+    Server s;
+
+    GigiConfig conf;
+
+    public synchronized void boot() throws Exception {
+        Locale.setDefault(Locale.ENGLISH);
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+
+        conf = GigiConfig.parse(System.in);
+        ServerConstants.init(conf.getMainProps());
+        initEmails(conf);
+
+        s = new Server();
+
+        initConnectors();
+        initHandlers();
+
+        s.start();
+        if ((ServerConstants.getSecurePort() <= 1024 || ServerConstants.getPort() <= 1024) && !System.getProperty("os.name").toLowerCase().contains("win")) {
+            SetUID uid = new SetUID();
+            if ( !uid.setUid(65536 - 2, 65536 - 2).getSuccess()) {
+                Log.getLogger(Launcher.class).warn("Couldn't set uid!");
+            }
+        }
+    }
+
+    private HttpConfiguration createHttpConfiguration() {
+        // SSL HTTP Configuration
+        HttpConfiguration httpsConfig = new HttpConfiguration();
+        httpsConfig.setSendServerVersion(false);
+        httpsConfig.setSendXPoweredBy(false);
+        return httpsConfig;
+    }
+
+    private void initConnectors() throws GeneralSecurityException, IOException {
+        HttpConfiguration httpConfig = createHttpConfiguration();
+        if (conf.getMainProps().getProperty("proxy", "false").equals("true")) {
+            httpConfig.addCustomizer(new ExtendedForwarded());
+            s.setConnectors(new Connector[] {
+                    ConnectorsLauncher.createConnector(conf, s, httpConfig, false)
+            });
+        } else {
+            HttpConfiguration httpsConfig = createHttpConfiguration();
+            // for client-cert auth
+            httpsConfig.addCustomizer(new SecureRequestCustomizer());
+            s.setConnectors(new Connector[] {
+                    ConnectorsLauncher.createConnector(conf, s, httpsConfig, true), ConnectorsLauncher.createConnector(conf, s, httpConfig, false)
+            });
+        }
+    }
+
+    private void initEmails(GigiConfig conf) throws GeneralSecurityException, IOException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+        KeyStore privateStore = conf.getPrivateStore();
+        Certificate mail = null;
+        Key k = null;
+        if (privateStore != null && privateStore.containsAlias("mail")) {
+            mail = privateStore.getCertificate("mail");
+            k = privateStore.getKey("mail", conf.getPrivateStorePw().toCharArray());
+        }
+        EmailProvider.initSystem(conf.getMainProps(), mail, k);
+    }
+
+    private static class ConnectorsLauncher {
+
+        private ConnectorsLauncher() {}
+
+        protected static ServerConnector createConnector(GigiConfig conf, Server s, HttpConfiguration httpConfig, boolean doHttps) throws GeneralSecurityException, IOException {
+            ServerConnector connector;
+            if (doHttps) {
+                connector = new ServerConnector(s, createConnectionFactory(conf), new HttpConnectionFactory(httpConfig));
+            } else {
+                connector = new ServerConnector(s, new HttpConnectionFactory(httpConfig));
+            }
+            connector.setHost(conf.getMainProps().getProperty("host"));
+            if (doHttps) {
+                connector.setPort(ServerConstants.getSecurePort());
+            } else {
+                connector.setPort(ServerConstants.getPort());
+            }
+            connector.setAcceptQueueSize(100);
+            return connector;
+        }
+
+        private static SslConnectionFactory createConnectionFactory(GigiConfig conf) throws GeneralSecurityException, IOException {
+            final SslContextFactory sslContextFactory = generateSSLContextFactory(conf, "www");
+            final SslContextFactory secureContextFactory = generateSSLContextFactory(conf, "secure");
+            secureContextFactory.setWantClientAuth(true);
+            secureContextFactory.setNeedClientAuth(true);
+            final SslContextFactory staticContextFactory = generateSSLContextFactory(conf, "static");
+            final SslContextFactory apiContextFactory = generateSSLContextFactory(conf, "api");
+            apiContextFactory.setWantClientAuth(true);
+            try {
+                secureContextFactory.start();
+                staticContextFactory.start();
+                apiContextFactory.start();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            return new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()) {
+
+                @Override
+                public boolean shouldRestartSSL() {
+                    return true;
+                }
+
+                @Override
+                public SSLEngine restartSSL(SSLSession sslSession) {
+                    SSLEngine e2 = null;
+                    if (sslSession instanceof ExtendedSSLSession) {
+                        ExtendedSSLSession es = (ExtendedSSLSession) sslSession;
+                        List<SNIServerName> names = es.getRequestedServerNames();
+                        for (SNIServerName sniServerName : names) {
+                            if (sniServerName instanceof SNIHostName) {
+                                SNIHostName host = (SNIHostName) sniServerName;
+                                String hostname = host.getAsciiName();
+                                if (hostname.equals(ServerConstants.getWwwHostName())) {
+                                    e2 = sslContextFactory.newSSLEngine();
+                                } else if (hostname.equals(ServerConstants.getStaticHostName())) {
+                                    e2 = staticContextFactory.newSSLEngine();
+                                } else if (hostname.equals(ServerConstants.getSecureHostName())) {
+                                    e2 = secureContextFactory.newSSLEngine();
+                                } else if (hostname.equals(ServerConstants.getApiHostName())) {
+                                    e2 = apiContextFactory.newSSLEngine();
+                                }
+                                break;
+                            }
+                        }
+                    }
+                    if (e2 == null) {
+                        e2 = sslContextFactory.newSSLEngine(sslSession.getPeerHost(), sslSession.getPeerPort());
+                    }
+                    e2.setUseClientMode(false);
+                    return e2;
+                }
+            };
+        }
+
+        private static SslContextFactory generateSSLContextFactory(GigiConfig conf, String alias) throws GeneralSecurityException, IOException {
+            SslContextFactory scf = new SslContextFactory() {
+
+                String[] ciphers = null;
+
+                @Override
+                public void customize(SSLEngine sslEngine) {
+                    super.customize(sslEngine);
+
+                    SSLParameters ssl = sslEngine.getSSLParameters();
+                    ssl.setUseCipherSuitesOrder(true);
+                    if (ciphers == null) {
+                        ciphers = CipherInfo.filter(sslEngine.getSupportedCipherSuites());
+                    }
+
+                    ssl.setCipherSuites(ciphers);
+                    sslEngine.setSSLParameters(ssl);
+
+                }
+
+            };
+            scf.setRenegotiationAllowed(false);
+
+            scf.setProtocol("TLS");
+            scf.setIncludeProtocols("TLSv1", "TLSv1.1", "TLSv1.2");
+            scf.setTrustStore(conf.getTrustStore());
+            KeyStore privateStore = conf.getPrivateStore();
+            scf.setKeyStorePassword(conf.getPrivateStorePw());
+            scf.setKeyStore(privateStore);
+            scf.setCertAlias(alias);
+            return scf;
+        }
+    }
+
+    private void initHandlers() throws GeneralSecurityException, IOException {
+        HandlerList hl = new HandlerList();
+        hl.setHandlers(new Handler[] {
+                ContextLauncher.generateStaticContext(), ContextLauncher.generateGigiContexts(conf.getMainProps(), conf.getTrustStore()), ContextLauncher.generateAPIContext()
+        });
+        s.setHandler(hl);
+    }
+
+    private static class ContextLauncher {
+
+        private ContextLauncher() {}
+
+        protected static Handler generateGigiContexts(Properties conf, KeyStore trust) {
+            ServletHolder webAppServlet = new ServletHolder(new Gigi(conf, trust));
+
+            ContextHandler ch = generateGigiServletContext(webAppServlet);
+            ch.setVirtualHosts(new String[] {
+                    ServerConstants.getWwwHostName()
+            });
+            ContextHandler chSecure = generateGigiServletContext(webAppServlet);
+            chSecure.setVirtualHosts(new String[] {
+                    ServerConstants.getSecureHostName()
+            });
+
+            HandlerList hl = new HandlerList();
+            hl.setHandlers(new Handler[] {
+                    ch, chSecure
+            });
+            return hl;
+        }
+
+        private static ContextHandler generateGigiServletContext(ServletHolder webAppServlet) {
+            final ResourceHandler rh = generateResourceHandler();
+            rh.setResourceBase("static/www");
+
+            HandlerWrapper hw = new PolicyRedirector();
+            hw.setHandler(rh);
+
+            ServletContextHandler servlet = new ServletContextHandler(ServletContextHandler.SESSIONS);
+            servlet.setInitParameter(SessionManager.__SessionCookieProperty, "SomeCA-Session");
+            servlet.addServlet(webAppServlet, "/*");
+            ErrorPageErrorHandler epeh = new ErrorPageErrorHandler();
+            epeh.addErrorPage(404, "/error");
+            epeh.addErrorPage(403, "/denied");
+            servlet.setErrorHandler(epeh);
+
+            HandlerList hl = new HandlerList();
+            hl.setHandlers(new Handler[] {
+                    hw, servlet
+            });
+
+            ContextHandler ch = new ContextHandler();
+            ch.setHandler(hl);
+            return ch;
+        }
+
+        protected static Handler generateStaticContext() {
+            final ResourceHandler rh = generateResourceHandler();
+            rh.setResourceBase("static/static");
+
+            ContextHandler ch = new ContextHandler();
+            ch.setHandler(rh);
+            ch.setVirtualHosts(new String[] {
+                    ServerConstants.getStaticHostName()
+            });
+
+            return ch;
+        }
+
+        private static ResourceHandler generateResourceHandler() {
+            ResourceHandler rh = new ResourceHandler() {
+
+                @Override
+                protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType) {
+                    super.doResponseHeaders(response, resource, mimeType);
+                    response.setDateHeader(HttpHeader.EXPIRES.asString(), System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 7);
+                }
+            };
+            rh.setEtags(true);
+            return rh;
+        }
+
+        protected static Handler generateAPIContext() {
+            ServletContextHandler sch = new ServletContextHandler();
+
+            sch.addVirtualHosts(new String[] {
+                    ServerConstants.getApiHostName()
+            });
+            sch.addServlet(new ServletHolder(new GigiAPI()), "/*");
+            return sch;
+        }
+
+    }
+
 }
diff --git a/src/org/cacert/gigi/Name.java b/src/org/cacert/gigi/Name.java
deleted file mode 100644 (file)
index 933982e..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.cacert.gigi;
-
-import java.io.PrintWriter;
-import java.util.Map;
-
-import org.cacert.gigi.output.Outputable;
-
-public class Name implements Outputable {
-       String fname;
-       String mname;
-       String lname;
-       String suffix;
-
-       public Name(String fname, String lname) {
-               this.fname = fname;
-               this.lname = lname;
-       }
-
-       @Override
-       public void output(PrintWriter out, Language l, Map<String, Object> vars) {
-               out.println("<span class=\"accountdetail\">");
-               out.print("<span class=\"fname\">");
-               out.print(fname);
-               out.print("</span> ");
-               out.print("<span class=\"lname\">");
-               out.print(lname);
-               out.print("</span>");
-               out.println("</span>");
-       }
-       @Override
-       public boolean equals(Object obj) {
-               if (!(obj instanceof Name)) {
-                       return false;
-               }
-               Name n = (Name) obj;
-               if (!(n.fname.equals(fname) && n.lname.equals(lname))) {
-                       return false;
-               }
-               if (mname == null) {
-                       if (n.mname != null) {
-                               return false;
-                       }
-               } else if (!mname.equals(n.mname)) {
-                       return false;
-               }
-               if (suffix == null) {
-                       if (n.suffix != null) {
-                               return false;
-                       }
-               } else if (!suffix.equals(n.suffix)) {
-                       return false;
-               }
-               return true;
-
-       }
-}
diff --git a/src/org/cacert/gigi/PermissionCheckable.java b/src/org/cacert/gigi/PermissionCheckable.java
new file mode 100644 (file)
index 0000000..4b868f2
--- /dev/null
@@ -0,0 +1,9 @@
+package org.cacert.gigi;
+
+import org.cacert.gigi.util.AuthorizationContext;
+
+public interface PermissionCheckable {
+
+    public boolean isPermitted(AuthorizationContext u);
+
+}
index 8714d959f6c0f91173857ec3e6ca5529dfdee757..e907c37620771015e4fe03780cc2fc73cfc3684c 100644 (file)
@@ -10,16 +10,21 @@ import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.handler.HandlerWrapper;
 
 public class PolicyRedirector extends HandlerWrapper {
-       @Override
-       public void handle(String target, Request baseRequest,
-                       HttpServletRequest request, HttpServletResponse response)
-                       throws IOException, ServletException {
-               if (target.startsWith("/policy/") && target.endsWith(".php")) {
-                       target = target.replace(".php", ".html");
-                       response.sendRedirect("/static" + target);
-                       baseRequest.setHandled(true);
-                       return;
-               }
-               super.handle(target, baseRequest, request, response);
-       }
+
+    @Override
+    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+        if (target.equals("/")) {
+            return;
+        }
+        if (target.equals("/policy/")) {
+            return;
+        }
+        if (target.startsWith("/policy/") && target.endsWith(".php")) {
+            target = target.replace(".php", ".html");
+            response.sendRedirect(target);
+            baseRequest.setHandled(true);
+            return;
+        }
+        super.handle(target, baseRequest, request, response);
+    }
 }
diff --git a/src/org/cacert/gigi/TestServlet.java b/src/org/cacert/gigi/TestServlet.java
deleted file mode 100644 (file)
index 82d8795..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.cacert.gigi;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.security.cert.X509Certificate;
-import java.util.Enumeration;
-
-import javax.net.ssl.SSLEngine;
-import javax.security.auth.x500.X500Principal;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.eclipse.jetty.io.EndPoint;
-import org.eclipse.jetty.server.HttpChannel;
-import org.eclipse.jetty.server.Request;
-
-public class TestServlet extends HttpServlet {
-       @Override
-       protected void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws ServletException, IOException {
-               Request r = (Request) req;
-               HttpChannel<?> hc = r.getHttpChannel();
-               EndPoint ep = hc.getEndPoint();
-               SSLEngine se;
-               Enumeration<String> names = req.getAttributeNames();
-               X509Certificate[] cert = (X509Certificate[]) req
-                               .getAttribute("javax.servlet.request.X509Certificate");
-               int keySize = (Integer) req
-                               .getAttribute("javax.servlet.request.key_size");
-               String ciphers = (String) req
-                               .getAttribute("javax.servlet.request.cipher_suite");
-               String sid = (String) req
-                               .getAttribute("javax.servlet.request.ssl_session_id");
-               PrintWriter out = resp.getWriter();
-               out.println("KeySize: " + keySize);
-               out.println("cipher: " + ciphers);
-               X509Certificate c1 = cert[0];
-               out.println("Serial:" + c1.getSerialNumber());
-               X500Principal client = c1.getSubjectX500Principal();
-               out.println("Name " + client.getName());
-               out.println(client.getName(X500Principal.RFC1779));
-               out.println(client.getName(X500Principal.RFC2253));
-               out.println("signature: " + c1.getSigAlgName());
-               out.println("issuer: " + c1.getSubjectX500Principal());
-               out.println("certCount: " + cert.length);
-       }
-}
diff --git a/src/org/cacert/gigi/User.java b/src/org/cacert/gigi/User.java
deleted file mode 100644 (file)
index 96e0a11..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-package org.cacert.gigi;
-
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.Date;
-
-import org.cacert.gigi.database.DatabaseConnection;
-import org.cacert.gigi.util.PasswordHash;
-
-public class User {
-
-       private int id;
-       Name name = new Name(null, null);
-
-       Date dob;
-       String email;
-
-       public User(int id) {
-               this.id = id;
-               try {
-                       PreparedStatement ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "SELECT `fname`, `lname`, `dob`, `email` FROM `users` WHERE id=?");
-                       ps.setInt(1, id);
-                       ResultSet rs = ps.executeQuery();
-                       if (rs.next()) {
-                               name = new Name(rs.getString(1), rs.getString(2));
-                               dob = rs.getDate(3);
-                               email = rs.getString(4);
-                       }
-                       rs.close();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-       }
-       public User() {
-       }
-       public int getId() {
-               return id;
-       }
-       public String getFname() {
-               return name.fname;
-       }
-       public String getLname() {
-               return name.lname;
-       }
-       public String getMname() {
-               return name.mname;
-       }
-       public Name getName() {
-               return name;
-       }
-       public void setMname(String mname) {
-               this.name.mname = mname;
-       }
-       public String getSuffix() {
-               return name.suffix;
-       }
-       public void setSuffix(String suffix) {
-               this.name.suffix = suffix;
-       }
-       public Date getDob() {
-               return dob;
-       }
-       public void setDob(Date dob) {
-               this.dob = dob;
-       }
-       public String getEmail() {
-               return email;
-       }
-       public void setEmail(String email) {
-               this.email = email;
-       }
-       public void setId(int id) {
-               this.id = id;
-       }
-       public void setFname(String fname) {
-               this.name.fname = fname;
-       }
-       public void setLname(String lname) {
-               this.name.lname = lname;
-       }
-       public void insert(String password) throws SQLException {
-               if (id != 0) {
-                       throw new Error("refusing to insert");
-               }
-               PreparedStatement query = DatabaseConnection.getInstance().prepare(
-                               "insert into `users` set `email`=?, `password`=?, "
-                                               + "`fname`=?, `mname`=?, `lname`=?, "
-                                               + "`suffix`=?, `dob`=?, `created`=NOW(), locked=0");
-               query.setString(1, email);
-               query.setString(2, PasswordHash.hash(password));
-               query.setString(3, name.fname);
-               query.setString(4, name.mname);
-               query.setString(5, name.lname);
-               query.setString(6, name.suffix);
-               query.setDate(7, new java.sql.Date(dob.getTime()));
-               query.execute();
-               id = DatabaseConnection.lastInsertId(query);
-               System.out.println("Inserted: " + id);
-       }
-
-       public boolean canAssure() throws SQLException {
-               if (getAssurancePoints() < 100) {
-                       return false;
-               }
-
-               return hasPassedCATS();
-
-       }
-       public boolean hasPassedCATS() throws SQLException {
-               PreparedStatement query = DatabaseConnection.getInstance().prepare(
-                               "SELECT 1 FROM `cats_passed` where `user_id`=?");
-               query.setInt(1, id);
-               ResultSet rs = query.executeQuery();
-               if (rs.next()) {
-                       return true;
-               } else {
-                       return false;
-               }
-       }
-       public int getAssurancePoints() throws SQLException {
-               PreparedStatement query = DatabaseConnection
-                               .getInstance()
-                               .prepare(
-                                               "SELECT sum(points) FROM `notary` where `to`=? AND `deleted`=0");
-               query.setInt(1, id);
-               ResultSet rs = query.executeQuery();
-               int points = 0;
-               if (rs.next()) {
-                       points = rs.getInt(1);
-               }
-               rs.close();
-               return points;
-       }
-       public int getExperiencePoints() throws SQLException {
-               PreparedStatement query = DatabaseConnection.getInstance().prepare(
-                               "SELECT count(*) FROM `notary` where `from`=? AND `deleted`=0");
-               query.setInt(1, id);
-               ResultSet rs = query.executeQuery();
-               int points = 0;
-               if (rs.next()) {
-                       points = rs.getInt(1) * 2;
-               }
-               rs.close();
-               return points;
-       }
-       @Override
-       public boolean equals(Object obj) {
-               if (!(obj instanceof User)) {
-                       return false;
-               }
-               User s = (User) obj;
-               return name.equals(s.name) && email.equals(s.email)
-                               && dob.equals(s.dob);
-       }
-       public int getMaxAssurePoints() throws SQLException {
-               int exp = getExperiencePoints();
-               int points = 10;
-               if (exp >= 10) {
-                       points += 5;
-               }
-               if (exp >= 20) {
-                       points += 5;
-               }
-               if (exp >= 30) {
-                       points += 5;
-               }
-               if (exp >= 40) {
-                       points += 5;
-               }
-               if (exp >= 50) {
-                       points += 5;
-               }
-               return points;
-       }
-}
diff --git a/src/org/cacert/gigi/api/APIPoint.java b/src/org/cacert/gigi/api/APIPoint.java
new file mode 100644 (file)
index 0000000..6847291
--- /dev/null
@@ -0,0 +1,51 @@
+package org.cacert.gigi.api;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.LoginPage;
+
+public abstract class APIPoint {
+
+    public void process(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        X509Certificate cert = LoginPage.getCertificateFromRequest(req);
+        if (cert == null) {
+            resp.sendError(403, "Error, cert authing required. No cert found.");
+            return;
+        }
+        String serial = LoginPage.extractSerialFormCert(cert);
+        CertificateOwner u = CertificateOwner.getByEnabledSerial(serial);
+        if (u == null) {
+            resp.sendError(403, "Error, cert authing required. Serial not found: " + serial);
+            return;
+        }
+
+        if ( !req.getMethod().equals("POST")) {
+            resp.sendError(500, "Error, POST required.");
+            return;
+        }
+        if (req.getQueryString() != null) {
+            resp.sendError(500, "Error, no query String allowed.");
+            return;
+        }
+        process(req, resp, u);
+    }
+
+    protected void process(HttpServletRequest req, HttpServletResponse resp, CertificateOwner u) throws IOException {
+        if (u instanceof User) {
+            process(req, resp, (User) u);
+        } else {
+            resp.sendError(500, "Error, requires a User certificate.");
+            return;
+        }
+    }
+
+    protected void process(HttpServletRequest req, HttpServletResponse resp, User u) throws IOException {
+
+    }
+}
diff --git a/src/org/cacert/gigi/api/CATSImport.java b/src/org/cacert/gigi/api/CATSImport.java
new file mode 100644 (file)
index 0000000..49960cd
--- /dev/null
@@ -0,0 +1,59 @@
+package org.cacert.gigi.api;
+
+import java.io.IOException;
+import java.util.Date;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.CATS;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+
+public class CATSImport extends APIPoint {
+
+    public static final String PATH = "/cats/import";
+
+    @Override
+    public void process(HttpServletRequest req, HttpServletResponse resp, CertificateOwner u) throws IOException {
+        if ( !(u instanceof Organisation)) {
+            resp.sendError(500, "Error, invalid cert");
+            return;
+        }
+        if ( !((Organisation) u).isSelfOrganisation()) {
+            resp.sendError(500, "Error, invalid cert");
+            return;
+
+        }
+        String target = req.getParameter("mid");
+        String testType = req.getParameter("variant");
+        String date = req.getParameter("date");
+        if (target == null || testType == null || date == null) {
+            resp.sendError(500, "Error, requires mid, variant and date");
+            return;
+        }
+        String language = req.getParameter("language");
+        String version = req.getParameter("version");
+        if (language == null || version == null) {
+            resp.sendError(500, "Error, requires also language and version");
+            return;
+        }
+        int id;
+        try {
+            id = Integer.parseInt(target);
+        } catch (NumberFormatException e) {
+            resp.sendError(500, "Error, requires mid to be integer.");
+            return;
+        }
+        CertificateOwner o = CertificateOwner.getById(id);
+        if ( !(o instanceof User)) {
+            resp.sendError(500, "Error, requires valid userid");
+            return;
+        }
+        System.out.println("CATS: " + target + ": " + testType);
+        User targetUser = (User) o;
+        System.out.println(targetUser.getId());
+        CATS.enterResult(targetUser, testType, new Date(Long.parseLong(date)), language, version);
+    }
+}
diff --git a/src/org/cacert/gigi/api/CATSResolve.java b/src/org/cacert/gigi/api/CATSResolve.java
new file mode 100644 (file)
index 0000000..1b25e9d
--- /dev/null
@@ -0,0 +1,40 @@
+package org.cacert.gigi.api;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+
+public class CATSResolve extends APIPoint {
+
+    public static final String PATH = "/cats/resolve";
+
+    @Override
+    public void process(HttpServletRequest req, HttpServletResponse resp, CertificateOwner u) throws IOException {
+        if ( !(u instanceof Organisation)) {
+            resp.sendError(500, "Error, invalid cert");
+            return;
+        }
+        if ( !((Organisation) u).isSelfOrganisation()) {
+            resp.sendError(500, "Error, invalid cert");
+            return;
+        }
+        String target = req.getParameter("serial");
+        if (target == null) {
+            resp.sendError(500, "Error, requires a serial parameter");
+            return;
+        }
+
+        CertificateOwner o = CertificateOwner.getByEnabledSerial(target);
+        if ( !(o instanceof User)) {
+            resp.sendError(500, "Error, requires valid serial");
+            return;
+        }
+        resp.setContentType("text/plain; charset=UTF-8");
+        resp.getWriter().print(o.getId());
+    }
+}
diff --git a/src/org/cacert/gigi/api/CreateCertificate.java b/src/org/cacert/gigi/api/CreateCertificate.java
new file mode 100644 (file)
index 0000000..0d5a27e
--- /dev/null
@@ -0,0 +1,86 @@
+package org.cacert.gigi.api;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.security.GeneralSecurityException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Job;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.account.certs.CertificateRequest;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.CertExporter;
+
+public class CreateCertificate extends APIPoint {
+
+    public static final String PATH = "/account/certs/new";
+
+    @Override
+    public void process(HttpServletRequest req, HttpServletResponse resp, User u) throws IOException {
+        String csr = req.getParameter("csr");
+        if (csr == null) {
+            resp.sendError(500, "Error, no CSR found");
+            return;
+        }
+        CertificateProfile cp = null;
+        String cpS = req.getParameter("profile");
+        if (cpS != null) {
+            cp = CertificateProfile.getByName(cpS);
+            if (cp == null) {
+                resp.sendError(500, "Error, profile not found");
+                return;
+            }
+        }
+        AuthorizationContext ctx = new AuthorizationContext(u, u);
+        String asOrg = req.getParameter("asOrg");
+        if (asOrg != null) {
+            try {
+                int i = Integer.parseInt(asOrg);
+                Organisation o0 = null;
+                for (Organisation o : u.getOrganisations()) {
+                    if (o.getId() == i) {
+                        o0 = o;
+                        break;
+                    }
+                }
+                if (o0 == null) {
+                    resp.sendError(500, "Error, Organisation with id " + i + " not found.");
+                    return;
+                } else {
+                    ctx = new AuthorizationContext(o0, u);
+                }
+            } catch (NumberFormatException e) {
+                resp.sendError(500, "Error, as Org is not an integer");
+                return;
+            }
+        }
+        try {
+            CertificateRequest cr = new CertificateRequest(ctx, csr, cp);
+            Certificate result = cr.draft();
+            Job job = result.issue(null, "2y", u);
+            job.waitFor(60000);
+            if (result.getStatus() != CertificateStatus.ISSUED) {
+                resp.sendError(510, "Error, issuing timed out");
+                return;
+            }
+
+            CertExporter.writeCertCrt(result, resp.getOutputStream(), req.getParameter("chain") != null, req.getParameter("noAnchor") == null);
+            return;
+        } catch (GeneralSecurityException e) {
+            resp.sendError(500, "Crypto failed");
+        } catch (GigiApiException e) {
+            resp.setStatus(500);
+            PrintWriter wr = resp.getWriter();
+            e.formatPlain(wr);
+        } catch (InterruptedException e) {
+            resp.sendError(500, "Interrupted");
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/api/GigiAPI.java b/src/org/cacert/gigi/api/GigiAPI.java
new file mode 100644 (file)
index 0000000..dbac5a8
--- /dev/null
@@ -0,0 +1,57 @@
+package org.cacert.gigi.api;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.database.DatabaseConnection.Link;
+
+public class GigiAPI extends HttpServlet {
+
+    private static final long serialVersionUID = 659963677032635817L;
+
+    HashMap<String, APIPoint> api = new HashMap<>();
+
+    public GigiAPI() {
+        api.put(CreateCertificate.PATH, new CreateCertificate());
+        api.put(RevokeCertificate.PATH, new RevokeCertificate());
+        api.put(CATSImport.PATH, new CATSImport());
+        api.put(CATSResolve.PATH, new CATSResolve());
+    }
+
+    @Override
+    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String pi = req.getPathInfo();
+        if (pi == null) {
+            return;
+        }
+        if (pi.equals("/security/csp/report")) {
+            ServletInputStream sis = req.getInputStream();
+            InputStreamReader isr = new InputStreamReader(sis, "UTF-8");
+            StringBuffer strB = new StringBuffer();
+            char[] buffer = new char[4 * 1024];
+            int len;
+            while ((len = isr.read(buffer)) > 0) {
+                strB.append(buffer, 0, len);
+            }
+            System.out.println(strB);
+            return;
+        }
+
+        APIPoint p = api.get(pi);
+        try (Link l = DatabaseConnection.newLink(false)) {
+            if (p != null) {
+                p.process(req, resp);
+            }
+        } catch (InterruptedException e) {
+            throw new Error(e);
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/api/RevokeCertificate.java b/src/org/cacert/gigi/api/RevokeCertificate.java
new file mode 100644 (file)
index 0000000..404d73c
--- /dev/null
@@ -0,0 +1,51 @@
+package org.cacert.gigi.api;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.Job;
+import org.cacert.gigi.dbObjects.User;
+
+public class RevokeCertificate extends APIPoint {
+
+    public static final String PATH = "/account/certs/revoke";
+
+    @Override
+    public void process(HttpServletRequest req, HttpServletResponse resp, User u) throws IOException {
+
+        if ( !req.getMethod().equals("POST")) {
+            resp.sendError(500, "Error, POST required.");
+            return;
+        }
+        if (req.getQueryString() != null) {
+            resp.sendError(500, "Error, no query String allowed.");
+            return;
+        }
+        String tserial = req.getParameter("serial");
+        if (tserial == null) {
+            resp.sendError(500, "Error, no Serial found");
+            return;
+        }
+        try {
+            Certificate c = Certificate.getBySerial(tserial);
+            if (c == null || c.getOwner() != u) {
+                resp.sendError(403, "Access Denied");
+                return;
+            }
+            Job job = c.revoke();
+            job.waitFor(60000);
+            if (c.getStatus() != CertificateStatus.REVOKED) {
+                resp.sendError(510, "Error, issuing timed out");
+                return;
+            }
+            resp.getWriter().println("OK");
+            return;
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/crypto/SMIME.java b/src/org/cacert/gigi/crypto/SMIME.java
new file mode 100644 (file)
index 0000000..321a4df
--- /dev/null
@@ -0,0 +1,106 @@
+package org.cacert.gigi.crypto;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.util.Random;
+
+import org.cacert.gigi.util.PEM;
+
+import sun.security.pkcs.ContentInfo;
+import sun.security.pkcs.PKCS7;
+import sun.security.pkcs.SignerInfo;
+import sun.security.util.DerOutputStream;
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.X500Name;
+
+public class SMIME {
+
+    public static String doAlternatives(String plain, String html) {
+
+        plain = "Content-type: text/plain\r\n\r\n" + plain;
+        html = "Content-type: text/html\r\n\r\n" + html;
+        String boundary = generateBoundary(plain, html);
+        StringBuffer content = new StringBuffer("Content-Type: multipart/alternative; boundary=\"");
+        content.append(boundary);
+        content.append("\"\r\n\r\n");
+        content.append("--");
+        content.append(boundary);
+        content.append("\r\n");
+        content.append(plain);
+        content.append("\r\n--");
+        content.append(boundary);
+        content.append("\r\n");
+        content.append(html);
+        content.append("\r\n--");
+        content.append(boundary);
+        content.append("--\r\n");
+        return content.toString();
+
+    }
+
+    public static void smime(String contents, PrivateKey pKey, X509Certificate c, PrintWriter to) throws IOException, GeneralSecurityException {
+
+        Signature signature = Signature.getInstance("SHA1WithRSA");
+        signature.initSign(pKey);
+        signature.update(contents.getBytes("UTF-8"));
+        byte[] signedData = signature.sign();
+
+        // "IssuerAndSerialNumber"
+        X500Name xName = X500Name.asX500Name(c.getIssuerX500Principal());
+        BigInteger serial = c.getSerialNumber();
+
+        SignerInfo sInfo = new SignerInfo(xName, serial, new AlgorithmId(AlgorithmId.SHA_oid), null, new AlgorithmId(AlgorithmId.RSAEncryption_oid), signedData, null);
+
+        // Content is outside so content here is null.
+        ContentInfo cInfo = new ContentInfo(ContentInfo.DATA_OID, null);
+
+        // Create PKCS7 Signed data
+        PKCS7 p7 = new PKCS7(new AlgorithmId[] {
+                new AlgorithmId(AlgorithmId.SHA_oid)
+        }, cInfo, new java.security.cert.X509Certificate[] {
+                c
+        }, new SignerInfo[] {
+                sInfo
+        });
+
+        ByteArrayOutputStream bOut = new DerOutputStream();
+        p7.encodeSignedData(bOut);
+
+        mimeEncode(contents, PEM.formatBase64(bOut.toByteArray()), to);
+    }
+
+    private static Random r = new Random();
+
+    private static void mimeEncode(String contents, String signature, PrintWriter to) {
+        String boundary = generateBoundary(contents, null);
+        to.println("MIME-Version: 1.0");
+        to.println("Content-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=\"sha1\"; boundary=\"" + boundary + "\"");
+        to.println("");
+        to.println("This is an S/MIME signed message");
+        to.println("");
+        to.println("--" + boundary);
+        to.println(contents);
+        to.println("--" + boundary);
+        to.println("Content-Type: application/x-pkcs7-signature; name=\"smime.p7s\"");
+        to.println("Content-Transfer-Encoding: base64");
+        to.println("Content-Disposition: attachment; filename=\"smime.p7s\"");
+        to.println("");
+        to.println(signature);
+        to.println();
+        to.println("--" + boundary + "--");
+    }
+
+    private static String generateBoundary(String contents, String contents2) {
+        String boundary = "";
+        while (contents.contains(boundary) || (contents2 != null && contents2.contains(boundary))) {
+            boundary = "--" + new BigInteger(16 * 8, r).toString(16).toUpperCase();
+        }
+        return boundary;
+    }
+}
diff --git a/src/org/cacert/gigi/crypto/SPKAC.java b/src/org/cacert/gigi/crypto/SPKAC.java
new file mode 100644 (file)
index 0000000..c97f5ac
--- /dev/null
@@ -0,0 +1,104 @@
+package org.cacert.gigi.crypto;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.Signature;
+import java.security.SignatureException;
+
+import sun.security.util.DerInputStream;
+import sun.security.util.DerOutputStream;
+import sun.security.util.DerValue;
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.X509Key;
+
+/**
+ * This class handles a SPKAC. A SPKAC has the following structure;
+ * 
+ * <pre>
+ * PublicKeyAndChallenge ::= SEQUENCE {
+ *     spki SubjectPublicKeyInfo,
+ *     challenge IA5STRING
+ * }
+ * 
+ * SignedPublicKeyAndChallenge ::= SEQUENCE {
+ *     publicKeyAndChallenge PublicKeyAndChallenge,
+ *     signatureAlgorithm AlgorithmIdentifier,
+ *     signature BIT STRING
+ * }
+ * </pre>
+ */
+public class SPKAC {
+
+    private X509Key pubkey;
+
+    private String challenge;
+
+    public SPKAC(byte[] data) throws IOException, GeneralSecurityException {
+        DerInputStream derIn = new DerInputStream(data);
+
+        DerValue derSPKACContent[] = derIn.getSequence(3);
+        if (derIn.available() != 0) {
+            throw new IllegalArgumentException("Additional data after SPKAC.");
+        }
+
+        AlgorithmId id = AlgorithmId.parse(derSPKACContent[1]);
+
+        derIn = derSPKACContent[0].toDerInputStream();
+
+        pubkey = (X509Key) X509Key.parse(derIn.getDerValue());
+
+        DerValue derChallenge = derIn.getDerValue();
+        if (derIn.available() != 0) {
+            throw new IllegalArgumentException("Additional data after SPKAC.");
+        }
+        if (derChallenge.length() != 0) {
+            challenge = derChallenge.getIA5String();
+        }
+
+        Signature s = Signature.getInstance(id.getName());
+        s.initVerify(pubkey);
+        s.update(derSPKACContent[0].toByteArray());
+        byte[] signature = derSPKACContent[2].getBitString();
+        if ( !s.verify(signature)) {
+            throw new SignatureException();
+        }
+
+    }
+
+    public String getChallenge() {
+        return challenge;
+    }
+
+    public X509Key getPubkey() {
+        return pubkey;
+    }
+
+    public SPKAC(X509Key pubkey, String challange) {
+        this.pubkey = pubkey;
+        challenge = challange;
+    }
+
+    public byte[] getEncoded(Signature sign) throws GeneralSecurityException, IOException {
+        DerOutputStream SPKAC = new DerOutputStream();
+        DerOutputStream SPKI = new DerOutputStream();
+
+        pubkey.encode(SPKI);
+        SPKI.putIA5String(challenge);
+
+        SPKAC.write(DerValue.tag_Sequence, SPKI);
+        byte[] toSign = SPKAC.toByteArray();
+        SPKI.close();
+
+        AlgorithmId aid = AlgorithmId.get(sign.getAlgorithm());
+        aid.encode(SPKAC);
+        sign.update(toSign);
+        SPKAC.putBitString(sign.sign());
+
+        DerOutputStream res = new DerOutputStream();
+        res.write(DerValue.tag_Sequence, SPKAC);
+        SPKAC.close();
+        byte[] result = res.toByteArray();
+        res.close();
+        return result;
+    }
+}
index 6bed8bd979c2f62aec1b7922779bed3048716063..b4f2f0bba895c737573499c20c6e281e50a3cb78 100644 (file)
 package org.cacert.gigi.database;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.Statement;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
 import java.util.Properties;
-import java.sql.Statement;
+import java.util.StringJoiner;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.database.SQLFileManager.ImportType;
 
 public class DatabaseConnection {
-       public static final int CONNECTION_TIMEOUT = 24 * 60 * 60;
-       Connection c;
-       HashMap<String, PreparedStatement> statements = new HashMap<String, PreparedStatement>();
-       private static Properties credentials;
-       Statement adHoc;
-       public DatabaseConnection() {
-               try {
-                       Class.forName(credentials.getProperty("sql.driver"));
-               } catch (ClassNotFoundException e) {
-                       e.printStackTrace();
-               }
-               tryConnect();
-
-       }
-       private void tryConnect() {
-               try {
-                       c = DriverManager.getConnection(credentials.getProperty("sql.url")
-                                       + "?zeroDateTimeBehavior=convertToNull",
-                                       credentials.getProperty("sql.user"),
-                                       credentials.getProperty("sql.password"));
-                       PreparedStatement ps = c
-                                       .prepareStatement("SET SESSION wait_timeout=?;");
-                       ps.setInt(1, CONNECTION_TIMEOUT);
-                       ps.execute();
-                       ps.close();
-                       adHoc = c.createStatement();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-       }
-       public PreparedStatement prepare(String query) throws SQLException {
-               ensureOpen();
-               PreparedStatement statement = statements.get(query);
-               if (statement == null) {
-                       statement = c.prepareStatement(query,
-                                       Statement.RETURN_GENERATED_KEYS);
-                       statements.put(query, statement);
-               }
-               return statement;
-       }
-       long lastAction = System.currentTimeMillis();
-       private void ensureOpen() {
-               if (System.currentTimeMillis() - lastAction > CONNECTION_TIMEOUT * 1000L) {
-                       try {
-                               ResultSet rs = adHoc.executeQuery("SELECT 1");
-                               rs.close();
-                               lastAction = System.currentTimeMillis();
-                               return;
-                       } catch (SQLException e) {
-                       }
-                       statements.clear();
-                       tryConnect();
-               }
-               lastAction = System.currentTimeMillis();
-       }
-       public static int lastInsertId(PreparedStatement query) throws SQLException {
-               ResultSet rs = query.getGeneratedKeys();
-               rs.next();
-               int id = rs.getInt(1);
-               rs.close();
-               return id;
-       }
-       static ThreadLocal<DatabaseConnection> instances = new ThreadLocal<DatabaseConnection>() {
-               @Override
-               protected DatabaseConnection initialValue() {
-                       return new DatabaseConnection();
-               }
-       };
-       public static DatabaseConnection getInstance() {
-               return instances.get();
-       }
-       public static boolean isInited() {
-               return credentials != null;
-       }
-       public static void init(Properties conf) {
-               if (credentials != null) {
-                       throw new Error("Re-initiaizing is forbidden.");
-               }
-               credentials = conf;
-       }
-       public void beginTransaction() throws SQLException {
-               c.setAutoCommit(false);
-       }
-       public void commitTransaction() throws SQLException {
-               c.commit();
-               c.setAutoCommit(true);
-       }
-       public void quitTransaction() {
-               try {
-                       if (!c.getAutoCommit()) {
-                               c.rollback();
-                               c.setAutoCommit(true);
-                       }
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-       }
+
+    public static class Link implements AutoCloseable {
+
+        private DatabaseConnection target;
+
+        protected Link(DatabaseConnection target) {
+            this.target = target;
+        }
+
+        @Override
+        public void close() {
+            synchronized (DatabaseConnection.class) {
+                Link i = instances.get(Thread.currentThread());
+                if (i != this) {
+                    throw new Error();
+                }
+                instances.remove(Thread.currentThread());
+                pool.add(target);
+            }
+        }
+
+    }
+
+    public static final int MAX_CACHED_INSTANCES = 3;
+
+    private static class StatementDescriptor {
+
+        String query;
+
+        boolean scrollable;
+
+        int instance;
+
+        PreparedStatement target;
+
+        public StatementDescriptor(String query, boolean scrollable) {
+            this.query = query;
+            this.scrollable = scrollable;
+            this.instance = 0;
+        }
+
+        public synchronized void instanciate(Connection c) throws SQLException {
+            if (scrollable) {
+                target = c.prepareStatement(query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+            } else {
+                target = c.prepareStatement(query, query.startsWith("SELECT ") ? Statement.NO_GENERATED_KEYS : Statement.RETURN_GENERATED_KEYS);
+            }
+
+        }
+
+        public synchronized PreparedStatement getTarget() {
+            return target;
+        }
+
+        public synchronized void increase() {
+            if (target != null) {
+                throw new IllegalStateException();
+            }
+            instance++;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + instance;
+            result = prime * result + ((query == null) ? 0 : query.hashCode());
+            result = prime * result + (scrollable ? 1231 : 1237);
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            StatementDescriptor other = (StatementDescriptor) obj;
+            if (instance != other.instance) {
+                return false;
+            }
+            if (query == null) {
+                if (other.query != null) {
+                    return false;
+                }
+            } else if ( !query.equals(other.query)) {
+                return false;
+            }
+            if (scrollable != other.scrollable) {
+                return false;
+            }
+            return true;
+        }
+
+    }
+
+    public static final int CURRENT_SCHEMA_VERSION = 15;
+
+    public static final int CONNECTION_TIMEOUT = 24 * 60 * 60;
+
+    private Connection c;
+
+    private HashMap<StatementDescriptor, PreparedStatement> statements = new HashMap<StatementDescriptor, PreparedStatement>();
+
+    private HashSet<PreparedStatement> underUse = new HashSet<>();
+
+    private static Properties credentials;
+
+    private Statement adHoc;
+
+    public DatabaseConnection() {
+        try {
+            Class.forName(credentials.getProperty("sql.driver"));
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        }
+        tryConnect();
+
+    }
+
+    private void tryConnect() {
+        try {
+            c = DriverManager.getConnection(credentials.getProperty("sql.url") + "?socketTimeout=" + CONNECTION_TIMEOUT, credentials.getProperty("sql.user"), credentials.getProperty("sql.password"));
+            adHoc = c.createStatement();
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    protected synchronized PreparedStatement prepareInternal(String query) throws SQLException {
+        return prepareInternal(query, false);
+    }
+
+    protected synchronized PreparedStatement prepareInternal(String query, boolean scrollable) throws SQLException {
+
+        ensureOpen();
+        query = preprocessQuery(query);
+        StatementDescriptor searchHead = new StatementDescriptor(query, scrollable);
+        PreparedStatement statement = null;
+        while (statement == null) {
+            statement = statements.get(searchHead);
+            if (statement == null) {
+                searchHead.instanciate(c);
+                statement = searchHead.getTarget();
+                if (searchHead.instance >= MAX_CACHED_INSTANCES) {
+                    return statement;
+                }
+                underUse.add(statement);
+                statements.put(searchHead, statement);
+            } else if (underUse.contains(statement)) {
+                searchHead.increase();
+                statement = null;
+            } else {
+                underUse.add(statement);
+            }
+        }
+        return statement;
+    }
+
+    protected synchronized PreparedStatement prepareInternalScrollable(String query) throws SQLException {
+        return prepareInternal(query, true);
+    }
+
+    private long lastAction = System.currentTimeMillis();
+
+    private void ensureOpen() {
+        if (System.currentTimeMillis() - lastAction > CONNECTION_TIMEOUT * 1000L) {
+            try {
+                ResultSet rs = adHoc.executeQuery("SELECT 1");
+                rs.close();
+                lastAction = System.currentTimeMillis();
+                return;
+            } catch (SQLException e) {
+            }
+            statements.clear();
+            tryConnect();
+        }
+        lastAction = System.currentTimeMillis();
+    }
+
+    private static HashMap<Thread, Link> instances = new HashMap<>();
+
+    private static LinkedBlockingDeque<DatabaseConnection> pool = new LinkedBlockingDeque<>();
+
+    private static int connCount = 0;
+
+    public static synchronized DatabaseConnection getInstance() {
+        Link l = instances.get(Thread.currentThread());
+        if (l == null) {
+            throw new Error("No database connection allocated");
+        }
+        return l.target;
+    }
+
+    public static synchronized boolean hasInstance() {
+        Link l = instances.get(Thread.currentThread());
+        return l != null;
+    }
+
+    public static boolean isInited() {
+        return credentials != null;
+    }
+
+    public static void init(Properties conf) {
+        if (credentials != null) {
+            throw new Error("Re-initiaizing is forbidden.");
+        }
+        credentials = conf;
+        try (Link i = newLink(false)) {
+            int version = 0;
+            try (GigiPreparedStatement gigiPreparedStatement = new GigiPreparedStatement("SELECT version FROM \"schemeVersion\" ORDER BY version DESC LIMIT 1;")) {
+                GigiResultSet rs = gigiPreparedStatement.executeQuery();
+                if (rs.next()) {
+                    version = rs.getInt(1);
+                }
+            }
+            if (version == CURRENT_SCHEMA_VERSION) {
+                return; // Good to go
+            }
+            if (version > CURRENT_SCHEMA_VERSION) {
+                throw new Error("Invalid database version. Please fix this.");
+            }
+            upgrade(version);
+        } catch (InterruptedException e) {
+            throw new Error(e);
+        }
+    }
+
+    private static void upgrade(int version) {
+        try {
+            Statement s = getInstance().c.createStatement();
+            try {
+                while (version < CURRENT_SCHEMA_VERSION) {
+                    addUpgradeScript(Integer.toString(version), s);
+                    version++;
+                }
+                s.addBatch("UPDATE \"schemeVersion\" SET version='" + version + "'");
+                System.out.println("UPGRADING Database to version " + version);
+                s.executeBatch();
+                System.out.println("done.");
+            } finally {
+                s.close();
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static void addUpgradeScript(String version, Statement s) throws Error, IOException, SQLException {
+        try (InputStream resourceAsStream = DatabaseConnection.class.getResourceAsStream("upgrade/from_" + version + ".sql")) {
+            if (resourceAsStream == null) {
+                throw new Error("Upgrade script from version " + version + " was not found.");
+            }
+            SQLFileManager.addFile(s, resourceAsStream, ImportType.PRODUCTION);
+        }
+    }
+
+    public static final String preprocessQuery(String originalQuery) {
+        originalQuery = originalQuery.replace('`', '"');
+        if (originalQuery.matches("^INSERT INTO [^ ]+ SET .*")) {
+            Pattern p = Pattern.compile("INSERT INTO ([^ ]+) SET (.*)");
+            Matcher m = p.matcher(originalQuery);
+            if (m.matches()) {
+                String replacement = "INSERT INTO " + toIdentifier(m.group(1));
+                String[] parts = m.group(2).split(",");
+                StringJoiner columns = new StringJoiner(", ");
+                StringJoiner values = new StringJoiner(", ");
+                for (int i = 0; i < parts.length; i++) {
+                    String[] split = parts[i].split("=", 2);
+                    columns.add(toIdentifier(split[0]));
+                    values.add(split[1]);
+                }
+                replacement += "(" + columns.toString() + ") VALUES(" + values.toString() + ")";
+                return replacement;
+            }
+        }
+
+        //
+        return originalQuery;
+    }
+
+    private static CharSequence toIdentifier(String ident) {
+        ident = ident.trim();
+        if ( !ident.startsWith("\"")) {
+            ident = "\"" + ident;
+        }
+        if ( !ident.endsWith("\"")) {
+            ident = ident + "\"";
+        }
+        return ident;
+    }
+
+    protected synchronized void returnStatement(PreparedStatement target) throws SQLException {
+        if ( !underUse.remove(target)) {
+            target.close();
+        }
+    }
+
+    public synchronized int getNumberOfLockedStatements() {
+        return underUse.size();
+    }
+
+    public synchronized void lockedStatements(PrintWriter writer) {
+        writer.println(underUse.size());
+        for (PreparedStatement ps : underUse) {
+            for (Entry<StatementDescriptor, PreparedStatement> e : statements.entrySet()) {
+                if (e.getValue() == ps) {
+                    writer.println("<br/>");
+                    writer.println(e.getKey().instance + ":");
+
+                    writer.println(e.getKey().query);
+                }
+            }
+        }
+    }
+
+    public static synchronized Link newLink(boolean readOnly) throws InterruptedException {
+        if (instances.get(Thread.currentThread()) != null) {
+            throw new Error("There is already a connection allocated for this thread.");
+        }
+        if (pool.isEmpty() && connCount < 5) {
+            pool.addLast(new DatabaseConnection());
+            connCount++;
+        }
+        DatabaseConnection conn = pool.takeFirst();
+        try {
+            conn.c.setReadOnly(readOnly);
+        } catch (SQLException e) {
+            throw new Error(e);
+        }
+        Link l = new Link(conn);
+        instances.put(Thread.currentThread(), l);
+        return l;
+    }
 }
diff --git a/src/org/cacert/gigi/database/GigiPreparedStatement.java b/src/org/cacert/gigi/database/GigiPreparedStatement.java
new file mode 100644 (file)
index 0000000..eae804b
--- /dev/null
@@ -0,0 +1,140 @@
+package org.cacert.gigi.database;
+
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+
+public class GigiPreparedStatement implements AutoCloseable {
+
+    private PreparedStatement target;
+
+    private GigiResultSet rs;
+
+    protected GigiPreparedStatement(PreparedStatement preparedStatement) {
+        target = preparedStatement;
+    }
+
+    public GigiPreparedStatement(String stmt) {
+        this(stmt, false);
+    }
+
+    public GigiPreparedStatement(String stmt, boolean scroll) {
+        try {
+            target = DatabaseConnection.getInstance().prepareInternal(stmt, scroll);
+        } catch (SQLException e) {
+            throw new Error(e);
+        }
+    }
+
+    public GigiResultSet executeQuery() {
+        try {
+            return rs = new GigiResultSet(target.executeQuery());
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void executeUpdate() {
+        try {
+            int updated = target.executeUpdate();
+            if (updated != 1) {
+                throw new Error("FATAL: multiple or no data updated: " + updated);
+            }
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public boolean execute() {
+        try {
+            return target.execute();
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void setInt(int parameterIndex, int x) {
+        try {
+            target.setInt(parameterIndex, x);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void setString(int parameterIndex, String x) {
+        try {
+            target.setString(parameterIndex, x);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void setDate(int parameterIndex, Date x) {
+        try {
+            target.setDate(parameterIndex, x);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void setTimestamp(int parameterIndex, Timestamp x) {
+        try {
+            target.setTimestamp(parameterIndex, x);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public int lastInsertId() {
+        try {
+            ResultSet rs = target.getGeneratedKeys();
+            rs.next();
+            int id = rs.getInt(1);
+            rs.close();
+            return id;
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void setBoolean(int parameterIndex, boolean x) {
+        try {
+            target.setBoolean(parameterIndex, x);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    private void handleSQL(SQLException e) {
+        // TODO Auto-generated method stub
+
+    }
+
+    @Override
+    public void close() {
+        GigiResultSet r = rs;
+        if (r != null) {
+            r.close();
+        }
+        PreparedStatement tg = target;
+        target = null;
+        try {
+            DatabaseConnection.getInstance().returnStatement(tg);
+        } catch (SQLException e) {
+            throw new Error(e);
+        }
+
+    }
+
+}
diff --git a/src/org/cacert/gigi/database/GigiResultSet.java b/src/org/cacert/gigi/database/GigiResultSet.java
new file mode 100644 (file)
index 0000000..d31c9cc
--- /dev/null
@@ -0,0 +1,158 @@
+package org.cacert.gigi.database;
+
+import java.io.Closeable;
+import java.sql.Date;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+
+public class GigiResultSet implements Closeable {
+
+    private ResultSet target;
+
+    public GigiResultSet(ResultSet target) {
+        this.target = target;
+    }
+
+    public String getString(int columnIndex) {
+        try {
+            return target.getString(columnIndex);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public boolean getBoolean(int columnIndex) {
+        try {
+            return target.getBoolean(columnIndex);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public int getInt(int columnIndex) {
+        try {
+            return target.getInt(columnIndex);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public Date getDate(int columnIndex) {
+        try {
+            return target.getDate(columnIndex);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public Timestamp getTimestamp(int columnIndex) {
+        try {
+            return target.getTimestamp(columnIndex);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public String getString(String columnLabel) {
+        try {
+            return target.getString(columnLabel);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public boolean getBoolean(String columnLabel) {
+        try {
+            return target.getBoolean(columnLabel);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public int getInt(String columnLabel) {
+        try {
+            return target.getInt(columnLabel);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public Date getDate(String columnLabel) {
+        try {
+            return target.getDate(columnLabel);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public Timestamp getTimestamp(String columnLabel) {
+        try {
+            return target.getTimestamp(columnLabel);
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public boolean next() {
+        try {
+            return target.next();
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public int getRow() {
+        try {
+            return target.getRow();
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void beforeFirst() {
+        try {
+            target.beforeFirst();
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void last() {
+        try {
+            target.last();
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+    }
+
+    public void close() {
+        try {
+            target.close();
+        } catch (SQLException e) {
+            handleSQL(e);
+            throw new Error(e);
+        }
+
+    }
+
+    private void handleSQL(SQLException e) {
+        // TODO Auto-generated method stub
+
+    }
+
+}
diff --git a/src/org/cacert/gigi/database/SQLFileManager.java b/src/org/cacert/gigi/database/SQLFileManager.java
new file mode 100644 (file)
index 0000000..9f563db
--- /dev/null
@@ -0,0 +1,66 @@
+package org.cacert.gigi.database;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SQLFileManager {
+
+    public static enum ImportType {
+        /**
+         * Execute Script as-is
+         */
+        PRODUCTION,
+        /**
+         * Execute Script, but change Engine=InnoDB to Engine=Memory
+         */
+        TEST,
+        /**
+         * Execute INSERT statements as-is, and TRUNCATE instead of DROPPING
+         */
+        TRUNCATE
+    }
+
+    public static void addFile(Statement stmt, InputStream f, ImportType type) throws IOException, SQLException {
+        String sql = readFile(f);
+        sql = sql.replaceAll("--[^\n]*\n", "\n");
+        sql = sql.replaceAll("#[^\n]*\n", "\n");
+        String[] stmts = sql.split(";");
+        Pattern p = Pattern.compile("\\s*DROP TABLE IF EXISTS \"([^\"]+)\"");
+        for (String string : stmts) {
+            Matcher m = p.matcher(string);
+            string = string.trim();
+            if (string.equals("")) {
+                continue;
+            }
+            if ((string.contains("profiles") || string.contains("cacerts") || string.contains("cats_type") || string.contains("countryIsoCode")) && type == ImportType.TRUNCATE) {
+                continue;
+            }
+            string = DatabaseConnection.preprocessQuery(string);
+            if (m.matches() && type == ImportType.TRUNCATE) {
+                String sql2 = "DELETE FROM \"" + m.group(1) + "\"";
+                stmt.addBatch(sql2);
+                continue;
+            }
+            if (type == ImportType.PRODUCTION || string.startsWith("INSERT")) {
+                stmt.addBatch(string);
+            } else if (type == ImportType.TEST) {
+                stmt.addBatch(string.replace("ENGINE=InnoDB", "ENGINE=Memory"));
+            }
+        }
+    }
+
+    private static String readFile(InputStream f) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        int len;
+        byte[] buf = new byte[4096];
+        while ((len = f.read(buf)) > 0) {
+            baos.write(buf, 0, len);
+        }
+        return new String(baos.toByteArray(), "UTF-8");
+    }
+}
diff --git a/src/org/cacert/gigi/database/tableStructure.sql b/src/org/cacert/gigi/database/tableStructure.sql
new file mode 100644 (file)
index 0000000..8cd2911
--- /dev/null
@@ -0,0 +1,656 @@
+DROP TABLE IF EXISTS "certOwners";
+CREATE TABLE "certOwners" (
+  "id" serial NOT NULL,
+  "created" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "modified" timestamp NULL DEFAULT NULL,
+  "deleted" timestamp NULL DEFAULT NULL,
+  PRIMARY KEY ("id")
+);
+
+DROP TABLE IF EXISTS "users";
+CREATE TABLE "users" (
+  "id" int NOT NULL,
+  "email" varchar(255) NOT NULL DEFAULT '',
+  "password" varchar(255) NOT NULL DEFAULT '',
+  "fname" varchar(255) NOT NULL DEFAULT '',
+  "mname" varchar(255) NOT NULL DEFAULT '',
+  "lname" varchar(255) NOT NULL DEFAULT '',
+  "suffix" varchar(50) NOT NULL DEFAULT '',
+  "dob" date NOT NULL,
+  "verified" boolean NOT NULL DEFAULT 'false',
+  "ccid" int NOT NULL DEFAULT '0',
+  "regid" int NOT NULL DEFAULT '0',
+  "locid" int NOT NULL DEFAULT '0',
+  "listme" boolean NOT NULL DEFAULT 'false',
+  "contactinfo" varchar(255) NOT NULL DEFAULT '',
+  "language" varchar(5) NOT NULL DEFAULT '',
+  PRIMARY KEY ("id")
+);
+CREATE INDEX ON "users" ("ccid");
+CREATE INDEX ON "users" ("regid");
+CREATE INDEX ON "users" ("locid");
+CREATE INDEX ON "users" ("email");
+CREATE INDEX ON "users" ("verified");
+
+
+DROP TABLE IF EXISTS "organisations";
+CREATE TABLE IF NOT EXISTS "organisations" (
+  "id" int NOT NULL,
+  "name" varchar(64) NOT NULL,
+  "state" varchar(2) NOT NULL,
+  "province" varchar(100) NOT NULL,
+  "city" varchar(100) NOT NULL,
+  "contactEmail" varchar(100) NOT NULL,
+  "creator" int NOT NULL,
+  "optional_name" text,
+  "postal_address" text,
+  PRIMARY KEY ("id")
+);
+
+DROP TABLE IF EXISTS "domains";
+CREATE TABLE "domains" (
+  "id" serial NOT NULL,
+  "memid" int NOT NULL,
+  "domain" varchar(255) NOT NULL,
+  "created" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "modified" timestamp NULL DEFAULT NULL,
+  "deleted" timestamp NULL DEFAULT NULL,
+  PRIMARY KEY ("id")
+);
+CREATE INDEX ON "domains" ("memid");
+CREATE INDEX ON "domains" ("domain");
+CREATE INDEX ON "domains" ("deleted");
+
+DROP TABLE IF EXISTS "emails";
+CREATE TABLE "emails" (
+  "id" serial NOT NULL,
+  "memid" int NOT NULL DEFAULT '0',
+  "email" varchar(255) NOT NULL DEFAULT '',
+  "created" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "modified" timestamp NULL DEFAULT NULL,
+  "deleted" timestamp NULL DEFAULT NULL,
+  PRIMARY KEY ("id")
+);
+CREATE INDEX ON "emails" ("memid");
+CREATE INDEX ON "emails" ("deleted");
+CREATE INDEX ON "emails" ("email");
+
+DROP TABLE IF EXISTS "emailPinglog";
+DROP TABLE IF EXISTS "domainPinglog";
+
+DROP TYPE IF EXISTS "emailPingType";
+CREATE TYPE "emailPingType" AS ENUM ('fast', 'active');
+DROP TYPE IF EXISTS "pingState";
+CREATE TYPE "pingState" AS ENUM ('open', 'success', 'failed');
+
+CREATE TABLE "emailPinglog" (
+  "when" timestamp NOT NULL,
+  "uid" int NOT NULL,
+  "email" varchar(255) NOT NULL,
+  "type" "emailPingType" NOT NULL,
+  "status" "pingState" NOT NULL,
+  "result" varchar(255) NOT NULL,
+  "challenge" varchar(255) NULL DEFAULT NULL
+);
+
+DROP TABLE IF EXISTS "pingconfig";
+
+DROP TYPE IF EXISTS "pingType";
+CREATE TYPE "pingType" AS ENUM ('email', 'ssl', 'http', 'dns');
+
+CREATE TABLE "pingconfig" (
+  "id" serial NOT NULL,
+  "domainid" int NOT NULL,
+  "type" "pingType" NOT NULL,
+  "info" varchar(255) NOT NULL,
+  "deleted" timestamp NULL DEFAULT NULL,
+  PRIMARY KEY ("id")
+);
+
+
+CREATE TABLE "domainPinglog" (
+  "when" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "configId" int NOT NULL,
+  "state" "pingState" NOT NULL,
+  "challenge" varchar(16),
+  "result" varchar(255)
+);
+
+DROP TABLE IF EXISTS "baddomains";
+CREATE TABLE "baddomains" (
+  "domain" varchar(255) NOT NULL DEFAULT ''
+);
+
+
+DROP TABLE IF EXISTS "alerts";
+CREATE TABLE "alerts" (
+  "memid" int NOT NULL DEFAULT '0',
+  "general" boolean NOT NULL DEFAULT 'false',
+  "country" boolean NOT NULL DEFAULT 'false',
+  "regional" boolean NOT NULL DEFAULT 'false',
+  "radius" boolean NOT NULL DEFAULT 'false',
+  PRIMARY KEY ("memid")
+);
+
+DROP TABLE IF EXISTS "user_agreements";
+CREATE TABLE "user_agreements" (
+  "id" serial NOT NULL,
+  "memid" int NOT NULL,
+  "secmemid" int DEFAULT NULL,
+  "document" varchar(50) DEFAULT NULL,
+  "date" timestamp DEFAULT NULL,
+  "active" boolean NOT NULL,
+  "method" varchar(100) NOT NULL,
+  "comment" varchar(100) DEFAULT NULL,
+  PRIMARY KEY ("id")
+);
+
+DROP TABLE IF EXISTS "certs";
+
+DROP TYPE IF EXISTS "mdType";
+CREATE TYPE "mdType" AS ENUM('md5','sha1','sha256','sha512');
+
+DROP TYPE IF EXISTS "csrType";
+CREATE TYPE "csrType" AS ENUM ('CSR', 'SPKAC');
+
+CREATE TABLE "certs" (
+  "id" serial NOT NULL,
+  "memid" int NOT NULL DEFAULT '0',
+  "serial" varchar(50) NOT NULL DEFAULT '',
+  "keytype" char(2) NOT NULL DEFAULT 'NS',
+  "codesign" boolean NOT NULL DEFAULT 'false',
+  "md" "mdType" NOT NULL DEFAULT 'sha512',
+  "profile" int NOT NULL,
+  "caid" int NULL DEFAULT NULL,
+
+  "csr_name" varchar(255) NOT NULL DEFAULT '',
+  "csr_type" "csrType" NOT NULL,
+  "crt_name" varchar(255) NOT NULL DEFAULT '',
+  "created" timestamp NULL DEFAULT NULL,
+  "modified" timestamp NULL DEFAULT NULL,
+  "revoked" timestamp NULL DEFAULT NULL,
+  "expire" timestamp NULL DEFAULT NULL,
+  "renewed" boolean NOT NULL DEFAULT 'false',
+  "disablelogin" boolean NOT NULL DEFAULT 'false',
+  "pkhash" char(40) DEFAULT NULL,
+  "certhash" char(40) DEFAULT NULL,
+  "description" varchar(100) NOT NULL DEFAULT '',
+  PRIMARY KEY ("id")
+);
+CREATE INDEX ON "certs" ("pkhash");
+CREATE INDEX ON "certs" ("revoked");
+CREATE INDEX ON "certs" ("created");
+CREATE INDEX ON "certs" ("memid");
+CREATE INDEX ON "certs" ("serial");
+CREATE INDEX ON "certs" ("expire");
+CREATE INDEX ON "certs" ("crt_name");
+
+
+
+DROP TABLE IF EXISTS "certAvas";
+CREATE TABLE "certAvas" (
+  "certId" int NOT NULL,
+  "name" varchar(20) NOT NULL,
+  "value" varchar(255) NOT NULL,
+
+  PRIMARY KEY ("certId", "name")
+);
+
+DROP TABLE IF EXISTS "clientcerts";
+CREATE TABLE "clientcerts" (
+  "id" int NOT NULL,
+  "disablelogin" boolean NOT NULL DEFAULT 'false',
+
+  PRIMARY KEY ("id")
+);
+
+DROP TABLE IF EXISTS "profiles";
+CREATE TABLE "profiles" (
+  "id" serial NOT NULL,
+  "keyname" varchar(60) NOT NULL,
+  "include" varchar(200) NOT NULL,
+  "requires" varchar(200) NOT NULL,
+  "name" varchar(100) NOT NULL,
+  PRIMARY KEY ("id"),
+  UNIQUE ("keyname")
+);
+
+DROP TABLE IF EXISTS "subjectAlternativeNames";
+
+DROP TYPE IF EXISTS "SANType";
+CREATE TYPE "SANType" AS ENUM ('email', 'DNS');
+
+CREATE TABLE "subjectAlternativeNames" (
+  "certId" int NOT NULL,
+  "contents" varchar(50) NOT NULL,
+  "type" "SANType" NOT NULL
+);
+
+DROP TABLE IF EXISTS "cacerts";
+CREATE TABLE "cacerts" (
+  "id" serial NOT NULL,
+  "keyname" varchar(60) NOT NULL,
+  "link" varchar(160) NOT NULL,
+  "parentRoot" int NOT NULL,
+  "validFrom" timestamp NULL DEFAULT NULL,
+  "validTo" timestamp NULL DEFAULT NULL,
+  PRIMARY KEY ("id"),
+  UNIQUE ("keyname")
+);
+
+DROP TABLE IF EXISTS "jobs";
+
+DROP TYPE IF EXISTS "jobType";
+CREATE TYPE "jobType" AS ENUM ('sign', 'revoke');
+DROP TYPE IF EXISTS "jobState";
+CREATE TYPE "jobState" AS ENUM ('open', 'done', 'error');
+
+
+CREATE TABLE "jobs" (
+  "id" serial NOT NULL,
+  "targetId" int NOT NULL,
+  "task" "jobType" NOT NULL,
+  "state" "jobState" NOT NULL DEFAULT 'open',
+  "warning" smallint NOT NULL DEFAULT '0',
+  "executeFrom" DATE,
+  "executeTo" VARCHAR(11),
+  PRIMARY KEY ("id")
+);
+
+CREATE INDEX ON "jobs" ("state");
+
+DROP TABLE IF EXISTS "notary";
+
+DROP TYPE IF EXISTS "notaryType";
+CREATE TYPE "notaryType" AS enum('Face to Face Meeting', 'TOPUP', 'TTP-Assisted', 'Nucleus Bonus');
+
+CREATE TABLE "notary" (
+  "id" serial NOT NULL,
+  "from" int NOT NULL DEFAULT '0',
+  "to" int NOT NULL DEFAULT '0',
+# total points that have been entered
+  "points" int NOT NULL DEFAULT '0',
+# awarded and the "experience points" are calculated virtually
+# Face to Face is default, TOPUP is for the remaining 30Points after two TTP
+# TTP is default ttp assurance
+  "method" "notaryType" NOT NULL DEFAULT 'Face to Face Meeting',
+  "location" varchar(255) NOT NULL DEFAULT '',
+  "date" varchar(255) NOT NULL DEFAULT '',
+# date when assurance was entered
+  "when" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+#?
+  "expire" timestamp NULL DEFAULT NULL,
+#?????????????????
+  "sponsor" int NOT NULL DEFAULT '0',
+# date when assurance was deleted (or 0)
+  "deleted" timestamp NULL DEFAULT NULL,
+  PRIMARY KEY ("id")
+);
+
+CREATE INDEX ON "notary"("from");
+CREATE INDEX ON "notary"("to");
+CREATE INDEX ON "notary"("when");
+CREATE INDEX ON "notary"("method");
+
+
+DROP TABLE IF EXISTS "cats_passed";
+CREATE TABLE "cats_passed" (
+  "id" serial NOT NULL,
+  "user_id" int NOT NULL,
+  "variant_id" int NOT NULL,
+  "pass_date" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "language" varchar(5) NOT NULL DEFAULT '',
+  "version" varchar(10) NOT NULL DEFAULT '',
+  PRIMARY KEY ("id"),
+  UNIQUE ("user_id","variant_id","pass_date")
+);
+
+# --------------------------------------------------------
+
+#
+# Table structure for table "cats_type"
+#
+
+DROP TABLE IF EXISTS "cats_type";
+CREATE TABLE "cats_type" (
+  "id" serial NOT NULL,
+  "type_text" varchar(255) NOT NULL,
+  PRIMARY KEY ("id"),
+  UNIQUE ("type_text")
+);
+
+DROP TABLE IF EXISTS "arbitrations";
+CREATE TABLE IF NOT EXISTS "arbitrations" (
+  "user" int NOT NULL,
+  "arbitration" varchar(20) NOT NULL,
+  PRIMARY KEY ("user","arbitration")
+);
+
+DROP TABLE IF EXISTS "user_groups";
+
+DROP TYPE IF EXISTS "userGroup";
+CREATE TYPE "userGroup" AS enum('supporter','arbitrator','blockedassuree','blockedassurer','blockedlogin','ttp-assurer','ttp-applicant', 'codesigning', 'orgassurer', 'blockedcert', 'nucleus-assurer');
+
+CREATE TABLE IF NOT EXISTS "user_groups" (
+  "id" serial NOT NULL,
+  "user" int NOT NULL,
+  "permission" "userGroup" NOT NULL,
+  "granted" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "deleted" timestamp NULL DEFAULT NULL,
+  "grantedby" int NOT NULL,
+  "revokedby" int DEFAULT NULL,
+  PRIMARY KEY ("id")
+);
+
+DROP TABLE IF EXISTS "org_admin";
+
+DROP TYPE IF EXISTS "yesno";
+CREATE TYPE "yesno" AS enum('y', 'n');
+
+
+CREATE TABLE IF NOT EXISTS "org_admin" (
+  "orgid" int NOT NULL,
+  "memid" int NOT NULL,
+  "master" "yesno" NOT NULL,
+  "creator" int NOT NULL,
+  "created" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "deleter" int NULL DEFAULT NULL,
+  "deleted" timestamp NULL DEFAULT NULL
+);
+CREATE INDEX ON "org_admin"("orgid", "memid");
+
+
+DROP TABLE IF EXISTS "adminLog";
+CREATE TABLE "adminLog" (
+  "when" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "uid" int NOT NULL,
+  "admin" int NOT NULL,
+  "type" varchar(100) NOT NULL DEFAULT '',
+  "information" varchar(50) NOT NULL DEFAULT ''
+);
+CREATE INDEX ON "adminLog"("when");
+
+
+DROP TABLE IF EXISTS "schemeVersion";
+CREATE TABLE "schemeVersion" (
+  "version" smallint NOT NULL,
+  PRIMARY KEY ("version")
+);
+INSERT INTO "schemeVersion" (version)  VALUES(15);
+
+DROP TABLE IF EXISTS `passwordResetTickets`;
+CREATE TABLE `passwordResetTickets` (
+  `id` serial NOT NULL,
+  `memid` int NOT NULL,
+  `creator` int NOT NULL,
+  `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `used` timestamp NULL DEFAULT NULL,
+  `token` varchar(32) NOT NULL,
+  `private_token` varchar(255) NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+
+/* Create table countryIsoCode' */
+
+DROP TABLE IF EXISTS `countryIsoCode`;
+CREATE TABLE `countryIsoCode` (
+  `id` serial NOT NULL,
+  `english` text NOT NULL,
+  `code2` varchar(2) NOT NULL,
+  `code3` varchar(3) NOT NULL,
+  `obp_id` int NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+/* Fill table countryIsoCode' */
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Andorra', 'AD',  'AND',  020);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United Arab Emirates (the)',  'AE',  'ARE',  784);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Afghanistan',  'AF',  'AFG',  004);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Antigua and Barbuda',  'AG',  'ATG',  028);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Anguilla',  'AI',  'AIA',  660);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Albania',  'AL',  'ALB',  008);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Armenia',  'AM',  'ARM',  051);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Angola',  'AO',  'AGO',  024);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Antarctica',  'AQ',  'ATA',  010);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Argentina',  'AR',  'ARG',  032);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('American Samoa',  'AS',  'ASM',  016);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Austria',  'AT',  'AUT',  040);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Australia',  'AU',  'AUS',  036);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Aruba',  'AW',  'ABW',  533);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Åland Islands',  'AX',  'ALA',  248);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Azerbaijan',  'AZ',  'AZE',  031);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bosnia and Herzegovina',  'BA',  'BIH',  070);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Barbados',  'BB',  'BRB',  052);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bangladesh',  'BD',  'BGD',  050);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Belgium',  'BE',  'BEL',  056);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Burkina Faso',  'BF',  'BFA',  854);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bulgaria',  'BG',  'BGR',  100);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bahrain',  'BH',  'BHR',  048);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Burundi',  'BI',  'BDI',  108);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Benin',  'BJ',  'BEN',  204);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Barthélemy',  'BL',  'BLM',  652);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bermuda',  'BM',  'BMU',  060);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Brunei Darussalam',  'BN',  'BRN',  096);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bolivia (Plurinational State of)',  'BO',  'BOL',  068);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bonaire, Sint Eustatius and Saba',  'BQ',  'BES',  535);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Brazil',  'BR',  'BRA',  076);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bahamas (the)',  'BS',  'BHS',  044);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bhutan',  'BT',  'BTN',  064);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bouvet Island',  'BV',  'BVT',  074);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Botswana',  'BW',  'BWA',  072);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Belarus',  'BY',  'BLR',  112);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Belize',  'BZ',  'BLZ',  084);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Canada',  'CA',  'CAN',  124);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cocos (Keeling) Islands (the)',  'CC',  'CCK',  166);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Congo (the Democratic Republic of the)',  'CD',  'COD',  180);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Central African Republic (the)',  'CF',  'CAF',  140);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Congo (the)',  'CG',  'COG',  178);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Switzerland',  'CH',  'CHE',  756);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Côte d`Ivoire',  'CI',  'CIV',  384);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cook Islands (the)',  'CK',  'COK',  184);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Chile',  'CL',  'CHL',  152);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cameroon',  'CM',  'CMR',  120);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('China',  'CN',  'CHN',  156);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Colombia',  'CO',  'COL',  170);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Costa Rica',  'CR',  'CRI',  188);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cuba',  'CU',  'CUB',  192);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cabo Verde',  'CV',  'CPV',  132);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Curaçao',  'CW',  'CUW',  531);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Christmas Island',  'CX',  'CXR',  162);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cyprus',  'CY',  'CYP',  196);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Czech Republic (the)',  'CZ',  'CZE',  203);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Germany',  'DE',  'DEU',  276);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Djibouti',  'DJ',  'DJI',  262);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Denmark',  'DK',  'DNK',  208);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Dominica',  'DM',  'DMA',  212);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Dominican Republic (the)',  'DO',  'DOM',  214);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Algeria',  'DZ',  'DZA',  012);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ecuador',  'EC',  'ECU',  218);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Estonia',  'EE',  'EST',  233);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Egypt',  'EG',  'EGY',  818);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Western Sahara*',  'EH',  'ESH',  732);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Eritrea',  'ER',  'ERI',  232);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Spain',  'ES',  'ESP',  724);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ethiopia',  'ET',  'ETH',  231);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Finland',  'FI',  'FIN',  246);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Fiji',  'FJ',  'FJI',  242);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Falkland Islands (the) [Malvinas]',  'FK',  'FLK',  238);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Micronesia (Federated States of)',  'FM',  'FSM',  583);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Faroe Islands (the)',  'FO',  'FRO',  234);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('France',  'FR',  'FRA',  250);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Gabon',  'GA',  'GAB',  266);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United Kingdom of Great Britain and Northern Ireland (the)',  'GB',  'GBR',  826);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Grenada',  'GD',  'GRD',  308);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Georgia',  'GE',  'GEO',  268);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('French Guiana',  'GF',  'GUF',  254);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guernsey',  'GG',  'GGY',  831);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ghana',  'GH',  'GHA',  288);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Gibraltar',  'GI',  'GIB',  292);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Greenland',  'GL',  'GRL',  304);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Gambia (the)',  'GM',  'GMB',  270);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guinea',  'GN',  'GIN',  324);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guadeloupe',  'GP',  'GLP',  312);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Equatorial Guinea',  'GQ',  'GNQ',  226);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Greece',  'GR',  'GRC',  300);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('South Georgia and the South Sandwich Islands',  'GS',  'SGS',  239);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guatemala',  'GT',  'GTM',  320);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guam',  'GU',  'GUM',  316);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guinea-Bissau',  'GW',  'GNB',  624);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guyana',  'GY',  'GUY',  328);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Hong Kong',  'HK',  'HKG',  344);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Heard Island and McDonald Islands',  'HM',  'HMD',  334);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Honduras',  'HN',  'HND',  340);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Croatia',  'HR',  'HRV',  191);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Haiti',  'HT',  'HTI',  332);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Hungary',  'HU',  'HUN',  348);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Indonesia',  'ID',  'IDN',  360);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ireland',  'IE',  'IRL',  372);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Israel',  'IL',  'ISR',  376);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Isle of Man',  'IM',  'IMN',  833);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('India',  'IN',  'IND',  356);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('British Indian Ocean Territory (the)',  'IO',  'IOT',  086);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Iraq',  'IQ',  'IRQ',  368);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Iran (Islamic Republic of)',  'IR',  'IRN',  364);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Iceland',  'IS',  'ISL',  352);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Italy',  'IT',  'ITA',  380);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Jersey',  'JE',  'JEY',  832);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Jamaica',  'JM',  'JAM',  388);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Jordan',  'JO',  'JOR',  400);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Japan',  'JP',  'JPN',  392);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kenya',  'KE',  'KEN',  404);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kyrgyzstan',  'KG',  'KGZ',  417);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cambodia',  'KH',  'KHM',  116);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kiribati',  'KI',  'KIR',  296);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Comoros (the)',  'KM',  'COM',  174);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Kitts and Nevis',  'KN',  'KNA',  659);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Korea (the Democratic People`s Republic of)',  'KP',  'PRK',  408);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Korea (the Republic of)',  'KR',  'KOR',  410);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kuwait',  'KW',  'KWT',  414);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cayman Islands (the)',  'KY',  'CYM',  136);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kazakhstan',  'KZ',  'KAZ',  398);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lao People`s Democratic Republic (the)',  'LA',  'LAO',  418);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lebanon',  'LB',  'LBN',  422);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Lucia',  'LC',  'LCA',  662);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Liechtenstein',  'LI',  'LIE',  438);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sri Lanka',  'LK',  'LKA',  144);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Liberia',  'LR',  'LBR',  430);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lesotho',  'LS',  'LSO',  426);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lithuania',  'LT',  'LTU',  440);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Luxembourg',  'LU',  'LUX',  442);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Latvia',  'LV',  'LVA',  428);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Libya',  'LY',  'LBY',  434);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Morocco',  'MA',  'MAR',  504);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Monaco',  'MC',  'MCO',  492);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Moldova (the Republic of)',  'MD',  'MDA',  498);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Montenegro',  'ME',  'MNE',  499);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Martin (French part)',  'MF',  'MAF',  663);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Madagascar',  'MG',  'MDG',  450);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Marshall Islands (the)',  'MH',  'MHL',  584);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Macedonia (the former Yugoslav Republic of)',  'MK',  'MKD',  807);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mali',  'ML',  'MLI',  466);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Myanmar',  'MM',  'MMR',  104);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mongolia',  'MN',  'MNG',  496);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Macao',  'MO',  'MAC',  446);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Northern Mariana Islands (the)',  'MP',  'MNP',  580);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Martinique',  'MQ',  'MTQ',  474);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mauritania',  'MR',  'MRT',  478);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Montserrat',  'MS',  'MSR',  500);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Malta',  'MT',  'MLT',  470);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mauritius',  'MU',  'MUS',  480);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Maldives',  'MV',  'MDV',  462);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Malawi',  'MW',  'MWI',  454);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mexico',  'MX',  'MEX',  484);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Malaysia',  'MY',  'MYS',  458);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mozambique',  'MZ',  'MOZ',  508);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Namibia',  'NA',  'NAM',  516);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('New Caledonia',  'NC',  'NCL',  540);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Niger (the)',  'NE',  'NER',  562);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Norfolk Island',  'NF',  'NFK',  574);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nigeria',  'NG',  'NGA',  566);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nicaragua',  'NI',  'NIC',  558);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Netherlands (the)',  'NL',  'NLD',  528);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Norway',  'NO',  'NOR',  578);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nepal',  'NP',  'NPL',  524);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nauru',  'NR',  'NRU',  520);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Niue',  'NU',  'NIU',  570);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('New Zealand',  'NZ',  'NZL',  554);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Oman',  'OM',  'OMN',  512);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Panama',  'PA',  'PAN',  591);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Peru',  'PE',  'PER',  604);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('French Polynesia',  'PF',  'PYF',  258);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Papua New Guinea',  'PG',  'PNG',  598);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Philippines (the)',  'PH',  'PHL',  608);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Pakistan',  'PK',  'PAK',  586);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Poland',  'PL',  'POL',  616);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Pierre and Miquelon',  'PM',  'SPM',  666);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Pitcairn',  'PN',  'PCN',  612);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Puerto Rico',  'PR',  'PRI',  630);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Palestine,  State of',  'PS',  'PSE',  275);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Portugal',  'PT',  'PRT',  620);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Palau',  'PW',  'PLW',  585);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Paraguay',  'PY',  'PRY',  600);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Qatar',  'QA',  'QAT',  634);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Réunion',  'RE',  'REU',  638);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Romania',  'RO',  'ROU',  642);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Serbia',  'RS',  'SRB',  688);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Russian Federation (the)',  'RU',  'RUS',  643);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Rwanda',  'RW',  'RWA',  646);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saudi Arabia',  'SA',  'SAU',  682);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Solomon Islands',  'SB',  'SLB',  090);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Seychelles',  'SC',  'SYC',  690);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sudan (the)',  'SD',  'SDN',  729);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sweden',  'SE',  'SWE',  752);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Singapore',  'SG',  'SGP',  702);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Helena, Ascension and Tristan da Cunha',  'SH',  'SHN',  654);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Slovenia',  'SI',  'SVN',  705);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Svalbard and Jan Mayen',  'SJ',  'SJM',  744);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Slovakia',  'SK',  'SVK',  703);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sierra Leone',  'SL',  'SLE',  694);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('San Marino',  'SM',  'SMR',  674);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Senegal',  'SN',  'SEN',  686);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Somalia',  'SO',  'SOM',  706);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Suriname',  'SR',  'SUR',  740);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('South Sudan',  'SS',  'SSD',  728);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sao Tome and Principe',  'ST',  'STP',  678);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('El Salvador',  'SV',  'SLV',  222);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sint Maarten (Dutch part)',  'SX',  'SXM',  534);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Syrian Arab Republic',  'SY',  'SYR',  760);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Swaziland',  'SZ',  'SWZ',  748);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Turks and Caicos Islands (the)',  'TC',  'TCA',  796);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Chad',  'TD',  'TCD',  148);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('French Southern Territories (the)',  'TF',  'ATF',  260);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Togo',  'TG',  'TGO',  768);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Thailand',  'TH',  'THA',  764);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tajikistan',  'TJ',  'TJK',  762);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tokelau',  'TK',  'TKL',  772);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Timor-Leste',  'TL',  'TLS',  626);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Turkmenistan',  'TM',  'TKM',  795);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tunisia',  'TN',  'TUN',  788);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tonga',  'TO',  'TON',  776);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Turkey',  'TR',  'TUR',  792);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Trinidad and Tobago',  'TT',  'TTO',  780);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tuvalu',  'TV',  'TUV',  798);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Taiwan (Province of China)',  'TW',  'TWN',  158);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tanzania,  United Republic of',  'TZ',  'TZA',  834);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ukraine',  'UA',  'UKR',  804);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Uganda',  'UG',  'UGA',  800);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United States Minor Outlying Islands (the)',  'UM',  'UMI',  581);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United States of America (the)',  'US',  'USA',  840);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Uruguay',  'UY',  'URY',  858);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Uzbekistan',  'UZ',  'UZB',  860);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Holy See (the)',  'VA',  'VAT',  336);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Vincent and the Grenadines',  'VC',  'VCT',  670);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Venezuela (Bolivarian Republic of)',  'VE',  'VEN',  862);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Virgin Islands (British)',  'VG',  'VGB',  092);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Virgin Islands (U.S.)',  'VI',  'VIR',  850);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Viet Nam',  'VN',  'VNM',  704);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Vanuatu',  'VU',  'VUT',  548);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Wallis and Futuna',  'WF',  'WLF',  876);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Samoa',  'WS',  'WSM',  882);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Yemen',  'YE',  'YEM',  887);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mayotte',  'YT',  'MYT',  175);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('South Africa',  'ZA',  'ZAF',  710);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Zambia',  'ZM',  'ZMB',  894);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Zimbabwe',  'ZW',  'ZWE',  716);
+
diff --git a/src/org/cacert/gigi/database/upgrade/from_1.sql b/src/org/cacert/gigi/database/upgrade/from_1.sql
new file mode 100644 (file)
index 0000000..e7c2577
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE adminLog DROP PRIMARY KEY;
+ALTER TABLE `adminLog` ADD INDEX(`when`);
+
diff --git a/src/org/cacert/gigi/database/upgrade/from_10.sql b/src/org/cacert/gigi/database/upgrade/from_10.sql
new file mode 100644 (file)
index 0000000..8bbb6bb
--- /dev/null
@@ -0,0 +1,2 @@
+UPDATE "emails" SET "email" = lower("email");
+UPDATE "domains" SET "domain" = lower("domain");
diff --git a/src/org/cacert/gigi/database/upgrade/from_11.sql b/src/org/cacert/gigi/database/upgrade/from_11.sql
new file mode 100644 (file)
index 0000000..cc7340f
--- /dev/null
@@ -0,0 +1 @@
+UPDATE `cats_type` SET `type_text` = 'Assurer''s Challenge' WHERE `type_text` = 'Assurer''s Challange';
diff --git a/src/org/cacert/gigi/database/upgrade/from_12.sql b/src/org/cacert/gigi/database/upgrade/from_12.sql
new file mode 100644 (file)
index 0000000..1035e09
--- /dev/null
@@ -0,0 +1,4 @@
+ALTER TABLE "organisations" ALTER COLUMN  "name" TYPE  varchar(64);
+
+ALTER TABLE "organisations" ADD COLUMN  "optional_name"  text;
+ALTER TABLE "organisations" ADD COLUMN  "postal_address"  text;
diff --git a/src/org/cacert/gigi/database/upgrade/from_13.sql b/src/org/cacert/gigi/database/upgrade/from_13.sql
new file mode 100644 (file)
index 0000000..1082341
--- /dev/null
@@ -0,0 +1,263 @@
+/* Create table countryIsoCode' */
+
+DROP TABLE IF EXISTS `countryIsoCode`;
+CREATE TABLE `countryIsoCode` (
+  `id` serial NOT NULL,
+  `english` text NOT NULL,
+  `code2` varchar(2) NOT NULL,
+  `code3` varchar(3) NOT NULL,
+  `obp_id` int NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+/* Fill table countryIsoCode' */
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Andorra', 'AD',  'AND',  020);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United Arab Emirates (the)',  'AE',  'ARE',  784);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Afghanistan',  'AF',  'AFG',  004);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Antigua and Barbuda',  'AG',  'ATG',  028);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Anguilla',  'AI',  'AIA',  660);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Albania',  'AL',  'ALB',  008);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Armenia',  'AM',  'ARM',  051);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Angola',  'AO',  'AGO',  024);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Antarctica',  'AQ',  'ATA',  010);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Argentina',  'AR',  'ARG',  032);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('American Samoa',  'AS',  'ASM',  016);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Austria',  'AT',  'AUT',  040);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Australia',  'AU',  'AUS',  036);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Aruba',  'AW',  'ABW',  533);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Åland Islands',  'AX',  'ALA',  248);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Azerbaijan',  'AZ',  'AZE',  031);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bosnia and Herzegovina',  'BA',  'BIH',  070);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Barbados',  'BB',  'BRB',  052);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bangladesh',  'BD',  'BGD',  050);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Belgium',  'BE',  'BEL',  056);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Burkina Faso',  'BF',  'BFA',  854);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bulgaria',  'BG',  'BGR',  100);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bahrain',  'BH',  'BHR',  048);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Burundi',  'BI',  'BDI',  108);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Benin',  'BJ',  'BEN',  204);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Barthélemy',  'BL',  'BLM',  652);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bermuda',  'BM',  'BMU',  060);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Brunei Darussalam',  'BN',  'BRN',  096);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bolivia (Plurinational State of)',  'BO',  'BOL',  068);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bonaire, Sint Eustatius and Saba',  'BQ',  'BES',  535);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Brazil',  'BR',  'BRA',  076);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bahamas (the)',  'BS',  'BHS',  044);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bhutan',  'BT',  'BTN',  064);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Bouvet Island',  'BV',  'BVT',  074);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Botswana',  'BW',  'BWA',  072);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Belarus',  'BY',  'BLR',  112);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Belize',  'BZ',  'BLZ',  084);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Canada',  'CA',  'CAN',  124);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cocos (Keeling) Islands (the)',  'CC',  'CCK',  166);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Congo (the Democratic Republic of the)',  'CD',  'COD',  180);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Central African Republic (the)',  'CF',  'CAF',  140);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Congo (the)',  'CG',  'COG',  178);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Switzerland',  'CH',  'CHE',  756);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Côte d`Ivoire',  'CI',  'CIV',  384);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cook Islands (the)',  'CK',  'COK',  184);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Chile',  'CL',  'CHL',  152);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cameroon',  'CM',  'CMR',  120);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('China',  'CN',  'CHN',  156);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Colombia',  'CO',  'COL',  170);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Costa Rica',  'CR',  'CRI',  188);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cuba',  'CU',  'CUB',  192);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cabo Verde',  'CV',  'CPV',  132);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Curaçao',  'CW',  'CUW',  531);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Christmas Island',  'CX',  'CXR',  162);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cyprus',  'CY',  'CYP',  196);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Czech Republic (the)',  'CZ',  'CZE',  203);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Germany',  'DE',  'DEU',  276);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Djibouti',  'DJ',  'DJI',  262);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Denmark',  'DK',  'DNK',  208);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Dominica',  'DM',  'DMA',  212);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Dominican Republic (the)',  'DO',  'DOM',  214);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Algeria',  'DZ',  'DZA',  012);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ecuador',  'EC',  'ECU',  218);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Estonia',  'EE',  'EST',  233);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Egypt',  'EG',  'EGY',  818);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Western Sahara*',  'EH',  'ESH',  732);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Eritrea',  'ER',  'ERI',  232);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Spain',  'ES',  'ESP',  724);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ethiopia',  'ET',  'ETH',  231);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Finland',  'FI',  'FIN',  246);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Fiji',  'FJ',  'FJI',  242);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Falkland Islands (the) [Malvinas]',  'FK',  'FLK',  238);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Micronesia (Federated States of)',  'FM',  'FSM',  583);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Faroe Islands (the)',  'FO',  'FRO',  234);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('France',  'FR',  'FRA',  250);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Gabon',  'GA',  'GAB',  266);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United Kingdom of Great Britain and Northern Ireland (the)',  'GB',  'GBR',  826);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Grenada',  'GD',  'GRD',  308);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Georgia',  'GE',  'GEO',  268);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('French Guiana',  'GF',  'GUF',  254);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guernsey',  'GG',  'GGY',  831);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ghana',  'GH',  'GHA',  288);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Gibraltar',  'GI',  'GIB',  292);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Greenland',  'GL',  'GRL',  304);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Gambia (the)',  'GM',  'GMB',  270);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guinea',  'GN',  'GIN',  324);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guadeloupe',  'GP',  'GLP',  312);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Equatorial Guinea',  'GQ',  'GNQ',  226);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Greece',  'GR',  'GRC',  300);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('South Georgia and the South Sandwich Islands',  'GS',  'SGS',  239);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guatemala',  'GT',  'GTM',  320);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guam',  'GU',  'GUM',  316);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guinea-Bissau',  'GW',  'GNB',  624);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Guyana',  'GY',  'GUY',  328);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Hong Kong',  'HK',  'HKG',  344);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Heard Island and McDonald Islands',  'HM',  'HMD',  334);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Honduras',  'HN',  'HND',  340);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Croatia',  'HR',  'HRV',  191);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Haiti',  'HT',  'HTI',  332);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Hungary',  'HU',  'HUN',  348);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Indonesia',  'ID',  'IDN',  360);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ireland',  'IE',  'IRL',  372);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Israel',  'IL',  'ISR',  376);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Isle of Man',  'IM',  'IMN',  833);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('India',  'IN',  'IND',  356);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('British Indian Ocean Territory (the)',  'IO',  'IOT',  086);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Iraq',  'IQ',  'IRQ',  368);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Iran (Islamic Republic of)',  'IR',  'IRN',  364);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Iceland',  'IS',  'ISL',  352);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Italy',  'IT',  'ITA',  380);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Jersey',  'JE',  'JEY',  832);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Jamaica',  'JM',  'JAM',  388);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Jordan',  'JO',  'JOR',  400);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Japan',  'JP',  'JPN',  392);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kenya',  'KE',  'KEN',  404);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kyrgyzstan',  'KG',  'KGZ',  417);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cambodia',  'KH',  'KHM',  116);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kiribati',  'KI',  'KIR',  296);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Comoros (the)',  'KM',  'COM',  174);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Kitts and Nevis',  'KN',  'KNA',  659);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Korea (the Democratic People`s Republic of)',  'KP',  'PRK',  408);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Korea (the Republic of)',  'KR',  'KOR',  410);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kuwait',  'KW',  'KWT',  414);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Cayman Islands (the)',  'KY',  'CYM',  136);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Kazakhstan',  'KZ',  'KAZ',  398);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lao People`s Democratic Republic (the)',  'LA',  'LAO',  418);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lebanon',  'LB',  'LBN',  422);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Lucia',  'LC',  'LCA',  662);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Liechtenstein',  'LI',  'LIE',  438);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sri Lanka',  'LK',  'LKA',  144);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Liberia',  'LR',  'LBR',  430);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lesotho',  'LS',  'LSO',  426);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Lithuania',  'LT',  'LTU',  440);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Luxembourg',  'LU',  'LUX',  442);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Latvia',  'LV',  'LVA',  428);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Libya',  'LY',  'LBY',  434);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Morocco',  'MA',  'MAR',  504);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Monaco',  'MC',  'MCO',  492);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Moldova (the Republic of)',  'MD',  'MDA',  498);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Montenegro',  'ME',  'MNE',  499);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Martin (French part)',  'MF',  'MAF',  663);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Madagascar',  'MG',  'MDG',  450);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Marshall Islands (the)',  'MH',  'MHL',  584);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Macedonia (the former Yugoslav Republic of)',  'MK',  'MKD',  807);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mali',  'ML',  'MLI',  466);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Myanmar',  'MM',  'MMR',  104);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mongolia',  'MN',  'MNG',  496);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Macao',  'MO',  'MAC',  446);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Northern Mariana Islands (the)',  'MP',  'MNP',  580);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Martinique',  'MQ',  'MTQ',  474);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mauritania',  'MR',  'MRT',  478);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Montserrat',  'MS',  'MSR',  500);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Malta',  'MT',  'MLT',  470);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mauritius',  'MU',  'MUS',  480);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Maldives',  'MV',  'MDV',  462);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Malawi',  'MW',  'MWI',  454);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mexico',  'MX',  'MEX',  484);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Malaysia',  'MY',  'MYS',  458);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mozambique',  'MZ',  'MOZ',  508);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Namibia',  'NA',  'NAM',  516);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('New Caledonia',  'NC',  'NCL',  540);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Niger (the)',  'NE',  'NER',  562);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Norfolk Island',  'NF',  'NFK',  574);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nigeria',  'NG',  'NGA',  566);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nicaragua',  'NI',  'NIC',  558);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Netherlands (the)',  'NL',  'NLD',  528);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Norway',  'NO',  'NOR',  578);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nepal',  'NP',  'NPL',  524);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Nauru',  'NR',  'NRU',  520);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Niue',  'NU',  'NIU',  570);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('New Zealand',  'NZ',  'NZL',  554);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Oman',  'OM',  'OMN',  512);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Panama',  'PA',  'PAN',  591);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Peru',  'PE',  'PER',  604);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('French Polynesia',  'PF',  'PYF',  258);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Papua New Guinea',  'PG',  'PNG',  598);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Philippines (the)',  'PH',  'PHL',  608);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Pakistan',  'PK',  'PAK',  586);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Poland',  'PL',  'POL',  616);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Pierre and Miquelon',  'PM',  'SPM',  666);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Pitcairn',  'PN',  'PCN',  612);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Puerto Rico',  'PR',  'PRI',  630);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Palestine,  State of',  'PS',  'PSE',  275);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Portugal',  'PT',  'PRT',  620);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Palau',  'PW',  'PLW',  585);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Paraguay',  'PY',  'PRY',  600);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Qatar',  'QA',  'QAT',  634);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Réunion',  'RE',  'REU',  638);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Romania',  'RO',  'ROU',  642);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Serbia',  'RS',  'SRB',  688);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Russian Federation (the)',  'RU',  'RUS',  643);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Rwanda',  'RW',  'RWA',  646);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saudi Arabia',  'SA',  'SAU',  682);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Solomon Islands',  'SB',  'SLB',  090);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Seychelles',  'SC',  'SYC',  690);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sudan (the)',  'SD',  'SDN',  729);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sweden',  'SE',  'SWE',  752);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Singapore',  'SG',  'SGP',  702);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Helena, Ascension and Tristan da Cunha',  'SH',  'SHN',  654);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Slovenia',  'SI',  'SVN',  705);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Svalbard and Jan Mayen',  'SJ',  'SJM',  744);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Slovakia',  'SK',  'SVK',  703);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sierra Leone',  'SL',  'SLE',  694);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('San Marino',  'SM',  'SMR',  674);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Senegal',  'SN',  'SEN',  686);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Somalia',  'SO',  'SOM',  706);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Suriname',  'SR',  'SUR',  740);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('South Sudan',  'SS',  'SSD',  728);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sao Tome and Principe',  'ST',  'STP',  678);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('El Salvador',  'SV',  'SLV',  222);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Sint Maarten (Dutch part)',  'SX',  'SXM',  534);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Syrian Arab Republic',  'SY',  'SYR',  760);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Swaziland',  'SZ',  'SWZ',  748);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Turks and Caicos Islands (the)',  'TC',  'TCA',  796);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Chad',  'TD',  'TCD',  148);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('French Southern Territories (the)',  'TF',  'ATF',  260);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Togo',  'TG',  'TGO',  768);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Thailand',  'TH',  'THA',  764);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tajikistan',  'TJ',  'TJK',  762);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tokelau',  'TK',  'TKL',  772);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Timor-Leste',  'TL',  'TLS',  626);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Turkmenistan',  'TM',  'TKM',  795);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tunisia',  'TN',  'TUN',  788);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tonga',  'TO',  'TON',  776);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Turkey',  'TR',  'TUR',  792);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Trinidad and Tobago',  'TT',  'TTO',  780);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tuvalu',  'TV',  'TUV',  798);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Taiwan (Province of China)',  'TW',  'TWN',  158);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Tanzania,  United Republic of',  'TZ',  'TZA',  834);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Ukraine',  'UA',  'UKR',  804);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Uganda',  'UG',  'UGA',  800);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United States Minor Outlying Islands (the)',  'UM',  'UMI',  581);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('United States of America (the)',  'US',  'USA',  840);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Uruguay',  'UY',  'URY',  858);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Uzbekistan',  'UZ',  'UZB',  860);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Holy See (the)',  'VA',  'VAT',  336);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Saint Vincent and the Grenadines',  'VC',  'VCT',  670);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Venezuela (Bolivarian Republic of)',  'VE',  'VEN',  862);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Virgin Islands (British)',  'VG',  'VGB',  092);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Virgin Islands (U.S.)',  'VI',  'VIR',  850);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Viet Nam',  'VN',  'VNM',  704);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Vanuatu',  'VU',  'VUT',  548);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Wallis and Futuna',  'WF',  'WLF',  876);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Samoa',  'WS',  'WSM',  882);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Yemen',  'YE',  'YEM',  887);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Mayotte',  'YT',  'MYT',  175);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('South Africa',  'ZA',  'ZAF',  710);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Zambia',  'ZM',  'ZMB',  894);
+INSERT INTO `countryIsoCode`(english, code2, code3, obp_id) VALUES ('Zimbabwe',  'ZW',  'ZWE',  716);
+
diff --git a/src/org/cacert/gigi/database/upgrade/from_14.sql b/src/org/cacert/gigi/database/upgrade/from_14.sql
new file mode 100644 (file)
index 0000000..dc4ab81
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE "cats_passed" ADD COLUMN "language" varchar(5) NOT NULL DEFAULT '';
+ALTER TABLE "cats_passed" ADD COLUMN "version" varchar(10) NOT NULL DEFAULT '';
diff --git a/src/org/cacert/gigi/database/upgrade/from_2.sql b/src/org/cacert/gigi/database/upgrade/from_2.sql
new file mode 100644 (file)
index 0000000..c36fbd4
--- /dev/null
@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS `cacerts`;
+CREATE TABLE `cacerts` (
+  `id` int(3) NOT NULL AUTO_INCREMENT,
+  `keyname` varchar(60) NOT NULL,
+  `link` varchar(160) NOT NULL,
+  `parentRoot` int(3) NOT NULL,
+  `validFrom` datetime NULL DEFAULT NULL,
+  `validTo` datetime NULL DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE (`keyname`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
diff --git a/src/org/cacert/gigi/database/upgrade/from_3.sql b/src/org/cacert/gigi/database/upgrade/from_3.sql
new file mode 100644 (file)
index 0000000..f6c3c08
--- /dev/null
@@ -0,0 +1,10 @@
+DROP TABLE IF EXISTS `profiles`;
+CREATE TABLE `profiles` (
+  `id` int(3) NOT NULL AUTO_INCREMENT,
+  `keyname` varchar(60) NOT NULL,
+  `include` varchar(200) NOT NULL,
+  `requires` varchar(200) NOT NULL,
+  `name` varchar(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE (`keyname`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
diff --git a/src/org/cacert/gigi/database/upgrade/from_4.sql b/src/org/cacert/gigi/database/upgrade/from_4.sql
new file mode 100644 (file)
index 0000000..80e78ca
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE "certAvas" RENAME "certid" TO "certId";
diff --git a/src/org/cacert/gigi/database/upgrade/from_5.sql b/src/org/cacert/gigi/database/upgrade/from_5.sql
new file mode 100644 (file)
index 0000000..6f9d8f7
--- /dev/null
@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS `passwordResetTickets`;
+CREATE TABLE `passwordResetTickets` (
+  `id` serial NOT NULL,
+  `memid` int NOT NULL,
+  `creator` int NOT NULL,
+  `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `used` timestamp NULL DEFAULT NULL,
+  `token` varchar(32) NOT NULL,
+  `private_token` varchar(255) NOT NULL,
+  PRIMARY KEY (`id`)
+);
diff --git a/src/org/cacert/gigi/database/upgrade/from_6.sql b/src/org/cacert/gigi/database/upgrade/from_6.sql
new file mode 100644 (file)
index 0000000..62e65bd
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE "pingconfig" ADD COLUMN "deleted" timestamp NULL DEFAULT NULL;
diff --git a/src/org/cacert/gigi/database/upgrade/from_7.sql b/src/org/cacert/gigi/database/upgrade/from_7.sql
new file mode 100644 (file)
index 0000000..6ba8682
--- /dev/null
@@ -0,0 +1 @@
+ALTER TYPE "userGroup" ADD VALUE 'blockedcert'
diff --git a/src/org/cacert/gigi/database/upgrade/from_8.sql b/src/org/cacert/gigi/database/upgrade/from_8.sql
new file mode 100644 (file)
index 0000000..7e20cf3
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TYPE "notaryType" ADD VALUE 'Nucleus Bonus';
+
+ALTER TYPE "userGroup" ADD VALUE  'nucleus-assurer';
diff --git a/src/org/cacert/gigi/database/upgrade/from_9.sql b/src/org/cacert/gigi/database/upgrade/from_9.sql
new file mode 100644 (file)
index 0000000..ec2b0be
--- /dev/null
@@ -0,0 +1,5 @@
+ALTER TABLE "emailPinglog" ADD COLUMN "challenge" varchar(255) NULL DEFAULT NULL;
+
+INSERT INTO "emailPinglog" SELECT CURRENT_TIMESTAMP AS "when", "memid" AS "uid", "email", 'active'::"emailPingType" AS "type", CASE WHEN "hash"='' THEN 'success'::"pingState" ELSE 'open'::"pingState" END AS state, '' AS result, "hash" AS "challenge" FROM "emails";
+ALTER TABLE "emails" DROP COLUMN "attempts";
+ALTER TABLE "emails" DROP COLUMN "hash";
diff --git a/src/org/cacert/gigi/dbObjects/Assurance.java b/src/org/cacert/gigi/dbObjects/Assurance.java
new file mode 100644 (file)
index 0000000..65a5a59
--- /dev/null
@@ -0,0 +1,75 @@
+package org.cacert.gigi.dbObjects;
+
+import org.cacert.gigi.dbObjects.wrappers.DataContainer;
+
+@DataContainer
+public class Assurance {
+
+    public enum AssuranceType {
+        FACE_TO_FACE("Face to Face Meeting"), TOPUP("TOPUP"), TTP_ASSISTED("TTP-Assisted"), NUCLEUS("Nucleus Bonus");
+
+        private final String description;
+
+        private AssuranceType(String description) {
+            this.description = description;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+    }
+
+    private int id;
+
+    private User from;
+
+    private User to;
+
+    private String location;
+
+    private String method;
+
+    private int points;
+
+    private String date;
+
+    public Assurance(int id, User from, User to, String location, String method, int points, String date) {
+        this.id = id;
+        this.from = from;
+        this.to = to;
+        this.location = location;
+        this.method = method;
+        this.points = points;
+        this.date = date;
+
+    }
+
+    public User getFrom() {
+        return from;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public int getPoints() {
+        return points;
+    }
+
+    public User getTo() {
+        return to;
+    }
+
+    public String getMethod() {
+        return method;
+    }
+
+    public String getDate() {
+        return date;
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/CACertificate.java b/src/org/cacert/gigi/dbObjects/CACertificate.java
new file mode 100644 (file)
index 0000000..41401b6
--- /dev/null
@@ -0,0 +1,181 @@
+package org.cacert.gigi.dbObjects;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.util.ServerConstants;
+
+public class CACertificate implements IdCachable {
+
+    private final String keyname;
+
+    private final int id;
+
+    private CACertificate parent = null;
+
+    private final X509Certificate cert;
+
+    private final String link;
+
+    private CACertificate(int id) {
+        this.id = id;
+        int parentRoot;
+        try (GigiPreparedStatement conn = new GigiPreparedStatement("SELECT `keyname`, `parentRoot`, `link` FROM `cacerts` WHERE `id`=?")) {
+            conn.setInt(1, id);
+            GigiResultSet res = conn.executeQuery();
+            if ( !res.next()) {
+                throw new IllegalArgumentException();
+            }
+            keyname = res.getString("keyname");
+            link = res.getString("link");
+            parentRoot = res.getInt("parentRoot");
+            if (res.next()) {
+                throw new RuntimeException("DB is broken");
+            }
+        }
+        if (parentRoot == id) {
+            parent = this;
+        } else {
+            parent = getById(parentRoot);
+        }
+        try {
+            FileInputStream fis = new FileInputStream("config/ca/" + keyname + ".crt");
+            CertificateFactory cf = CertificateFactory.getInstance("X509");
+            cert = (X509Certificate) cf.generateCertificate(fis);
+        } catch (FileNotFoundException e) {
+            throw new Error(e);
+        } catch (GeneralSecurityException e) {
+            throw new Error(e);
+        }
+    }
+
+    public CACertificate getParent() {
+        return parent;
+    }
+
+    public X509Certificate getCertificate() {
+        return cert;
+    }
+
+    @Override
+    public String toString() {
+        return "CACertificate: " + keyname;
+    }
+
+    static {
+        try {
+            update();
+        } catch (CertificateException e) {
+            throw new Error(e);
+        } catch (FileNotFoundException e) {
+            throw new Error(e);
+        }
+    }
+
+    private static void update() throws CertificateException, FileNotFoundException {
+        File scandir = new File("config/ca");
+        CertificateFactory xf = CertificateFactory.getInstance("X509");
+        HashMap<X500Principal, X509Certificate> map = new HashMap<>();
+        HashMap<X500Principal, String> names = new HashMap<>();
+        for (File f : scandir.listFiles()) {
+            X509Certificate cert = (X509Certificate) xf.generateCertificate(new FileInputStream(f));
+            X500Principal princip = cert.getSubjectX500Principal();
+            map.put(princip, cert);
+            String name = f.getName();
+            names.put(princip, name.substring(0, name.length() - 4));
+        }
+        HashMap<X500Principal, Integer> inserted = new HashMap<>();
+        for (X509Certificate i : map.values()) {
+            if (inserted.containsKey(i.getSubjectX500Principal())) {
+                continue;
+            }
+            Deque<X509Certificate> toInserts = new ArrayDeque<>();
+            toInserts.add(i);
+            while ( !inserted.containsKey(i.getIssuerX500Principal()) && !i.getIssuerX500Principal().equals(i.getSubjectX500Principal())) {
+                i = map.get(i.getIssuerX500Principal());
+                toInserts.addFirst(i);
+            }
+            for (X509Certificate toInsert : toInserts) {
+
+                X500Principal subj = toInsert.getSubjectX500Principal();
+                boolean self = toInsert.getIssuerX500Principal().equals(subj);
+                try (GigiPreparedStatement q = new GigiPreparedStatement("SELECT `id`, `parentRoot` FROM `cacerts` WHERE `keyname`=?")) {
+                    q.setString(1, names.get(subj));
+                    GigiResultSet res = q.executeQuery();
+                    int id;
+                    if (res.next()) {
+                        id = res.getInt("id");
+                        if (res.getInt("parentRoot") != (self ? id : inserted.get(toInsert.getIssuerX500Principal()))) {
+                            throw new Error("Invalid DB structure: " + subj + "->" + inserted.get(toInsert.getIssuerX500Principal()) + " vs " + res.getInt("parentRoot"));
+                        }
+                    } else {
+                        String link;
+                        String keyname = names.get(subj);
+                        if ( !keyname.contains("_")) {
+                            link = "https://g2.crt." + ServerConstants.getSuffix() + "/g2/" + keyname + ".crt";
+                        } else {
+                            String[] parts = keyname.split("_");
+                            link = "https://g2.crt." + ServerConstants.getSuffix() + "/g2/" + parts[1] + "/" + parts[0] + "-" + parts[2] + ".crt";
+
+                        }
+                        try (GigiPreparedStatement q2 = new GigiPreparedStatement("INSERT INTO `cacerts` SET `parentRoot`=?, `keyname`=?, `link`=?")) {
+                            q2.setInt(1, self ? 0 : inserted.get(toInsert.getIssuerX500Principal()));
+                            q2.setString(2, keyname);
+                            q2.setString(3, link);
+                            q2.execute();
+                            id = q2.lastInsertId();
+                        }
+                        if (self) {
+                            try (GigiPreparedStatement q3 = new GigiPreparedStatement("UPDATE `cacerts` SET `parentRoot`=?, `id`=?")) {
+                                q3.setInt(1, id);
+                                q3.setInt(2, id);
+                                q3.execute();
+                            }
+                        }
+                    }
+                    inserted.put(subj, id);
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getId() {
+        return id;
+    }
+
+    private static ObjectCache<CACertificate> myCache = new ObjectCache<>();
+
+    public String getKeyname() {
+        return keyname;
+    }
+
+    public String getLink() {
+        return link;
+    }
+
+    public static synchronized CACertificate getById(int id) throws IllegalArgumentException {
+        CACertificate em = myCache.get(id);
+        if (em == null) {
+            myCache.put(em = new CACertificate(id));
+        }
+        return em;
+    }
+
+    public boolean isSelfsigned() {
+        return this == getParent();
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/CATS.java b/src/org/cacert/gigi/dbObjects/CATS.java
new file mode 100644 (file)
index 0000000..700d76e
--- /dev/null
@@ -0,0 +1,55 @@
+package org.cacert.gigi.dbObjects;
+
+import java.sql.Timestamp;
+import java.util.Date;
+import java.util.HashMap;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+
+public class CATS {
+
+    private static HashMap<String, Integer> names = new HashMap<>();
+
+    public static final String ASSURER_CHALLENGE_NAME = "Assurer's Challenge";
+
+    public static final int ASSURER_CHALLENGE_ID;
+
+    private CATS() {
+
+    }
+
+    static {
+        try (GigiPreparedStatement st = new GigiPreparedStatement("SELECT `id`, `type_text` FROM `cats_type`")) {
+            GigiResultSet res = st.executeQuery();
+            while (res.next()) {
+                names.put(res.getString(2), res.getInt(1));
+            }
+        }
+        ASSURER_CHALLENGE_ID = getID(ASSURER_CHALLENGE_NAME);
+    }
+
+    public static synchronized int getID(String name) {
+        Integer i = names.get(name);
+        if (i == null) {
+            try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `cats_type` SET `type_text`=?")) {
+                ps.setString(1, name);
+                ps.execute();
+                i = ps.lastInsertId();
+            }
+            names.put(name, i);
+        }
+        return i;
+    }
+
+    public static void enterResult(User user, String testType, Date passDate, String language, String version) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `cats_passed` SET `user_id`=?, `variant_id`=?, `pass_date`=?, `language`=?, `version`=?")) {
+            ps.setInt(1, user.getId());
+            ps.setInt(2, getID(testType));
+            ps.setTimestamp(3, new Timestamp(passDate.getTime()));
+            ps.setString(4, language);
+            ps.setString(5, version);
+            ps.execute();
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/CPS.properties b/src/org/cacert/gigi/dbObjects/CPS.properties
new file mode 100644 (file)
index 0000000..87acaed
--- /dev/null
@@ -0,0 +1,3 @@
+# from http://www.cacert.org/policy/CertificationPracticeStatement.php#p3.1.7 on 07.11.2014
+IDN-enabled=ac,ar,at,biz,br,cat,ch,cl,cn,de,dk,es,fi,gr,hu,info,io,ir,is,jp,kr,li,lt,museum,no,org,pl,pr,se,sh,th,tm,tw,vn
+# from https://data.iana.org/TLD/tlds-alpha-by-domain.txt
diff --git a/src/org/cacert/gigi/dbObjects/Certificate.java b/src/org/cacert/gigi/dbObjects/Certificate.java
new file mode 100644 (file)
index 0000000..5dfaa5d
--- /dev/null
@@ -0,0 +1,448 @@
+package org.cacert.gigi.dbObjects;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.sql.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.TranslateCommand;
+import org.cacert.gigi.util.KeyStorage;
+import org.cacert.gigi.util.Notary;
+
+public class Certificate implements IdCachable {
+
+    public enum SANType {
+        EMAIL("email"), DNS("DNS");
+
+        private final String opensslName;
+
+        private SANType(String opensslName) {
+            this.opensslName = opensslName;
+        }
+
+        public String getOpensslName() {
+            return opensslName;
+        }
+    }
+
+    public static class SubjectAlternateName implements Comparable<SubjectAlternateName> {
+
+        private SANType type;
+
+        private String name;
+
+        public SubjectAlternateName(SANType type, String name) {
+            this.type = type;
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public SANType getType() {
+            return type;
+        }
+
+        @Override
+        public int compareTo(SubjectAlternateName o) {
+            int i = type.compareTo(o.type);
+            if (i != 0) {
+                return i;
+            }
+            return name.compareTo(o.name);
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((name == null) ? 0 : name.hashCode());
+            result = prime * result + ((type == null) ? 0 : type.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            SubjectAlternateName other = (SubjectAlternateName) obj;
+            if (name == null) {
+                if (other.name != null) {
+                    return false;
+                }
+            } else if ( !name.equals(other.name)) {
+                return false;
+            }
+            if (type != other.type) {
+                return false;
+            }
+            return true;
+        }
+
+    }
+
+    public enum CSRType {
+        CSR, SPKAC;
+    }
+
+    private int id;
+
+    private CertificateOwner owner;
+
+    private String serial;
+
+    private Digest md;
+
+    private String csrName;
+
+    private String crtName;
+
+    private String csr = null;
+
+    private CSRType csrType;
+
+    private List<SubjectAlternateName> sans;
+
+    private CertificateProfile profile;
+
+    private HashMap<String, String> dn;
+
+    private String dnString;
+
+    private CACertificate ca;
+
+    public Certificate(CertificateOwner owner, User actor, HashMap<String, String> dn, Digest md, String csr, CSRType csrType, CertificateProfile profile, SubjectAlternateName... sans) throws GigiApiException, IOException {
+        if ( !profile.canBeIssuedBy(owner, actor)) {
+            throw new GigiApiException("You are not allowed to issue these certificates.");
+        }
+        this.owner = owner;
+        this.dn = dn;
+        if (dn.size() == 0) {
+            throw new GigiApiException("DN must not be empty.");
+        }
+        dnString = stringifyDN(dn);
+        this.md = md;
+        this.csr = csr;
+        this.csrType = csrType;
+        this.profile = profile;
+        this.sans = Arrays.asList(sans);
+        synchronized (Certificate.class) {
+
+            try (GigiPreparedStatement inserter = new GigiPreparedStatement("INSERT INTO certs SET md=?::`mdType`, csr_type=?::`csrType`, crt_name='', memid=?, profile=?")) {
+                inserter.setString(1, md.toString().toLowerCase());
+                inserter.setString(2, csrType.toString());
+                inserter.setInt(3, owner.getId());
+                inserter.setInt(4, profile.getId());
+                inserter.execute();
+                id = inserter.lastInsertId();
+            }
+
+            try (GigiPreparedStatement san = new GigiPreparedStatement("INSERT INTO `subjectAlternativeNames` SET `certId`=?, contents=?, type=?::`SANType`")) {
+                for (SubjectAlternateName subjectAlternateName : sans) {
+                    san.setInt(1, id);
+                    san.setString(2, subjectAlternateName.getName());
+                    san.setString(3, subjectAlternateName.getType().getOpensslName());
+                    san.execute();
+                }
+            }
+
+            try (GigiPreparedStatement insertAVA = new GigiPreparedStatement("INSERT INTO `certAvas` SET `certId`=?, name=?, value=?")) {
+                insertAVA.setInt(1, id);
+                for (Entry<String, String> e : dn.entrySet()) {
+                    insertAVA.setString(2, e.getKey());
+                    insertAVA.setString(3, e.getValue());
+                    insertAVA.execute();
+                }
+            }
+            File csrFile = KeyStorage.locateCsr(id);
+            csrName = csrFile.getPath();
+            try (FileOutputStream fos = new FileOutputStream(csrFile)) {
+                fos.write(csr.getBytes("UTF-8"));
+            }
+            try (GigiPreparedStatement updater = new GigiPreparedStatement("UPDATE `certs` SET `csr_name`=? WHERE id=?")) {
+                updater.setString(1, csrName);
+                updater.setInt(2, id);
+                updater.execute();
+            }
+
+            cache.put(this);
+        }
+    }
+
+    private Certificate(GigiResultSet rs) {
+        this.id = rs.getInt("id");
+        dnString = rs.getString("subject");
+        md = Digest.valueOf(rs.getString("md").toUpperCase());
+        csrName = rs.getString("csr_name");
+        crtName = rs.getString("crt_name");
+        owner = CertificateOwner.getById(rs.getInt("memid"));
+        profile = CertificateProfile.getById(rs.getInt("profile"));
+        this.serial = rs.getString("serial");
+
+        try (GigiPreparedStatement ps2 = new GigiPreparedStatement("SELECT `contents`, `type` FROM `subjectAlternativeNames` WHERE `certId`=?")) {
+            ps2.setInt(1, id);
+            GigiResultSet rs2 = ps2.executeQuery();
+            sans = new LinkedList<>();
+            while (rs2.next()) {
+                sans.add(new SubjectAlternateName(SANType.valueOf(rs2.getString("type").toUpperCase()), rs2.getString("contents")));
+            }
+        }
+    }
+
+    public enum CertificateStatus {
+        /**
+         * This certificate is not in the database, has no id and only exists as
+         * this java object.
+         */
+        DRAFT("draft"),
+        /**
+         * The certificate has been signed. It is stored in the database.
+         * {@link Certificate#cert()} is valid.
+         */
+        ISSUED("issued"),
+
+        /**
+         * The certificate has been revoked.
+         */
+        REVOKED("revoked"),
+
+        /**
+         * If this certificate cannot be updated because an error happened in
+         * the signer.
+         */
+        ERROR("error");
+
+        private final Outputable name;
+
+        private CertificateStatus(String codename) {
+            this.name = new TranslateCommand(codename);
+
+        }
+
+        public Outputable getName() {
+            return name;
+        }
+
+    }
+
+    public synchronized CertificateStatus getStatus() {
+        try (GigiPreparedStatement searcher = new GigiPreparedStatement("SELECT crt_name, created, revoked, serial, caid FROM certs WHERE id=?")) {
+            searcher.setInt(1, id);
+            GigiResultSet rs = searcher.executeQuery();
+            if ( !rs.next()) {
+                throw new IllegalStateException("Certificate not in Database");
+            }
+
+            crtName = rs.getString(1);
+            serial = rs.getString(4);
+            if (rs.getTimestamp(2) == null) {
+                return CertificateStatus.DRAFT;
+            }
+            ca = CACertificate.getById(rs.getInt("caid"));
+            if (rs.getTimestamp(2) != null && rs.getTimestamp(3) == null) {
+                return CertificateStatus.ISSUED;
+            }
+            return CertificateStatus.REVOKED;
+        }
+    }
+
+    /**
+     * @param start
+     *            the date from which on the certificate should be valid. (or
+     *            null if it should be valid instantly)
+     * @param period
+     *            the period for which the date should be valid. (a
+     *            <code>yyyy-mm-dd</code> or a "2y" (2 calendar years), "6m" (6
+     *            months)
+     * @return A job which can be used to monitor the progress of this task.
+     * @throws IOException
+     *             for problems with writing the CSR/SPKAC
+     * @throws GigiApiException
+     *             if the period is bogus
+     */
+    public Job issue(Date start, String period, User actor) throws IOException, GigiApiException {
+        if (getStatus() != CertificateStatus.DRAFT) {
+            throw new IllegalStateException();
+        }
+        Notary.writeUserAgreement(actor, "ToS", "certificate issuance", "", true, 0);
+
+        return Job.sign(this, start, period);
+
+    }
+
+    public Job revoke() {
+        if (getStatus() != CertificateStatus.ISSUED) {
+            throw new IllegalStateException();
+        }
+        return Job.revoke(this);
+
+    }
+
+    public CACertificate getParent() {
+        CertificateStatus status = getStatus();
+        if (status != CertificateStatus.REVOKED && status != CertificateStatus.ISSUED) {
+            throw new IllegalStateException(status + " is not wanted here.");
+        }
+        return ca;
+    }
+
+    public X509Certificate cert() throws IOException, GeneralSecurityException {
+        CertificateStatus status = getStatus();
+        if (status != CertificateStatus.REVOKED && status != CertificateStatus.ISSUED) {
+            throw new IllegalStateException(status + " is not wanted here.");
+        }
+        InputStream is = null;
+        X509Certificate crt = null;
+        try {
+            is = new FileInputStream(crtName);
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            crt = (X509Certificate) cf.generateCertificate(is);
+        } finally {
+            if (is != null) {
+                is.close();
+            }
+        }
+        return crt;
+    }
+
+    public Certificate renew() {
+        return null;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String getSerial() {
+        getStatus();
+        // poll changes
+        return serial;
+    }
+
+    public String getDistinguishedName() {
+        return dnString;
+    }
+
+    public Digest getMessageDigest() {
+        return md;
+    }
+
+    public CertificateOwner getOwner() {
+        return owner;
+    }
+
+    public List<SubjectAlternateName> getSANs() {
+        return Collections.unmodifiableList(sans);
+    }
+
+    public CertificateProfile getProfile() {
+        return profile;
+    }
+
+    public synchronized static Certificate getBySerial(String serial) {
+        if (serial == null || "".equals(serial)) {
+            return null;
+        }
+        String concat = "string_agg(concat('/', `name`, '=', REPLACE(REPLACE(value, '\\\\', '\\\\\\\\'), '/', '\\\\/')), '')";
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + concat + " as `subject`, `md`, `csr_name`, `crt_name`,`memid`, `profile`, `certs`.`serial` FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=`certs`.`id` WHERE `serial`=? GROUP BY `certs`.`id`")) {
+            ps.setString(1, serial);
+            GigiResultSet rs = ps.executeQuery();
+            if ( !rs.next()) {
+                return null;
+            }
+            int id = rs.getInt(1);
+            Certificate c1 = cache.get(id);
+            if (c1 != null) {
+                return c1;
+            }
+            Certificate certificate = new Certificate(rs);
+            cache.put(certificate);
+            return certificate;
+        }
+    }
+
+    private static ObjectCache<Certificate> cache = new ObjectCache<>();
+
+    public synchronized static Certificate getById(int id) {
+        Certificate cacheRes = cache.get(id);
+        if (cacheRes != null) {
+            return cacheRes;
+        }
+
+        try {
+            String concat = "string_agg(concat('/', `name`, '=', REPLACE(REPLACE(value, '\\\\', '\\\\\\\\'), '/', '\\\\/')), '')";
+            try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + concat + " as subject, md, csr_name, crt_name,memid, profile, certs.serial FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=certs.id WHERE certs.id=? GROUP BY certs.id")) {
+                ps.setInt(1, id);
+                GigiResultSet rs = ps.executeQuery();
+                if ( !rs.next()) {
+                    return null;
+                }
+
+                Certificate c = new Certificate(rs);
+                cache.put(c);
+                return c;
+            }
+        } catch (IllegalArgumentException e) {
+
+        }
+        return null;
+    }
+
+    public static String escapeAVA(String value) {
+
+        return value.replace("\\", "\\\\").replace("/", "\\/");
+    }
+
+    public static String stringifyDN(HashMap<String, String> contents) {
+        StringBuffer res = new StringBuffer();
+        for (Entry<String, String> i : contents.entrySet()) {
+            res.append("/" + i.getKey() + "=");
+            res.append(escapeAVA(i.getValue()));
+        }
+        return res.toString();
+    }
+
+    public static HashMap<String, String> buildDN(String... contents) {
+        HashMap<String, String> res = new HashMap<>();
+        for (int i = 0; i + 1 < contents.length; i += 2) {
+            res.put(contents[i], contents[i + 1]);
+        }
+        return res;
+    }
+
+    public java.util.Date getRevocationDate() {
+        if (getStatus() == CertificateStatus.REVOKED) {
+            try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT revoked FROM certs WHERE id=?")) {
+                prep.setInt(1, getId());
+                GigiResultSet res = prep.executeQuery();
+                if (res.next()) {
+                    return new java.util.Date(res.getDate("revoked").getTime());
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/CertificateOwner.java b/src/org/cacert/gigi/dbObjects/CertificateOwner.java
new file mode 100644 (file)
index 0000000..3ca821f
--- /dev/null
@@ -0,0 +1,128 @@
+package org.cacert.gigi.dbObjects;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+
+public abstract class CertificateOwner implements IdCachable {
+
+    private static final ObjectCache<CertificateOwner> myCache = new ObjectCache<>();
+
+    private int id;
+
+    protected CertificateOwner(int id) {
+        this.id = id;
+    }
+
+    protected CertificateOwner() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `certOwners` DEFAULT VALUES")) {
+            ps.execute();
+            id = ps.lastInsertId();
+        }
+        myCache.put(this);
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public static synchronized CertificateOwner getById(int id) {
+        CertificateOwner u = myCache.get(id);
+        if (u == null) {
+            try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT *, `users`.`id` AS uid, `organisations`.`id` AS oid FROM `certOwners` LEFT JOIN `users` ON `users`.`id`=`certOwners`.`id` LEFT JOIN `organisations` ON `organisations`.`id` = `certOwners`.`id` WHERE `certOwners`.`id`=? AND `deleted` is null")) {
+                ps.setInt(1, id);
+                try (GigiResultSet rs = ps.executeQuery()) {
+                    if ( !rs.next()) {
+                        return null;
+                    }
+                    if (rs.getString("uid") != null) {
+                        myCache.put(u = new User(rs));
+                    } else if (rs.getString("oid") != null) {
+                        myCache.put(u = new Organisation(rs));
+                    } else {
+                        System.err.print("Malformed cert owner: " + id);
+                    }
+                }
+            }
+        }
+        return u;
+    }
+
+    public Domain[] getDomains() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id` FROM `domains` WHERE `memid`=? AND `deleted` IS NULL")) {
+            ps.setInt(1, getId());
+
+            try (GigiResultSet rs = ps.executeQuery()) {
+                LinkedList<Domain> data = new LinkedList<Domain>();
+
+                while (rs.next()) {
+                    data.add(Domain.getById(rs.getInt(1)));
+                }
+
+                return data.toArray(new Domain[0]);
+            }
+        }
+    }
+
+    public Certificate[] getCertificates(boolean includeRevoked) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement(includeRevoked ? "SELECT id FROM certs WHERE memid=?" : "SELECT id FROM certs WHERE memid=? AND revoked IS NULL")) {
+            ps.setInt(1, getId());
+
+            GigiResultSet rs = ps.executeQuery();
+            LinkedList<Certificate> data = new LinkedList<Certificate>();
+
+            while (rs.next()) {
+                data.add(Certificate.getById(rs.getInt(1)));
+            }
+
+            return data.toArray(new Certificate[0]);
+        }
+    }
+
+    public boolean isValidDomain(String domainname) {
+        for (Domain d : getDomains()) {
+            String sfx = d.getSuffix();
+            if (domainname.equals(sfx) || domainname.endsWith("." + sfx)) {
+                return d.isVerified();
+            }
+        }
+
+        return false;
+    }
+
+    public abstract boolean isValidEmail(String email);
+
+    public void delete() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `certOwners` SET `deleted`=NOW() WHERE `id`=?")) {
+            ps.setInt(1, getId());
+            ps.execute();
+        }
+        myCache.remove(this);
+    }
+
+    public String[] getAdminLog() {
+        try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `when`, type, information FROM `adminLog` WHERE uid=? ORDER BY `when` ASC")) {
+            prep.setInt(1, getId());
+            GigiResultSet res = prep.executeQuery();
+            List<String> entries = new LinkedList<String>();
+
+            while (res.next()) {
+                entries.add(res.getString(2) + " (" + res.getString(3) + ")");
+            }
+            return entries.toArray(new String[0]);
+        }
+    }
+
+    public static CertificateOwner getByEnabledSerial(String serial) {
+        try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `memid` FROM `certs` WHERE serial=? AND `disablelogin`='f' AND `revoked` is NULL")) {
+            prep.setString(1, serial.toLowerCase());
+            GigiResultSet res = prep.executeQuery();
+            if (res.next()) {
+                return getById(res.getInt(1));
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/CertificateProfile.java b/src/org/cacert/gigi/dbObjects/CertificateProfile.java
new file mode 100644 (file)
index 0000000..5704497
--- /dev/null
@@ -0,0 +1,282 @@
+package org.cacert.gigi.dbObjects;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+
+public class CertificateProfile implements IdCachable {
+
+    private final int id;
+
+    private final String keyName;
+
+    private final String visibleName;
+
+    private static final Map<String, CertificateProfile> byName;
+
+    private static final Map<Integer, CertificateProfile> byId;
+
+    private final Map<String, PropertyTemplate> pt;
+
+    private final List<String> req;
+
+    private CertificateProfile(int id, String keyName, String visibleName, String requires, String include) {
+        this.id = id;
+        this.keyName = keyName;
+        this.visibleName = visibleName;
+        req = parseConditions(requires);
+        pt = parsePropertyTemplates(include);
+    }
+
+    public static class PropertyTemplate implements Comparable<PropertyTemplate> {
+
+        private boolean required = true;
+
+        private boolean multiple = false;
+
+        private String base;
+
+        public PropertyTemplate(String inc) {
+            if (inc.endsWith("?") || inc.endsWith("*") || inc.endsWith("+")) {
+                char sfx = inc.charAt(inc.length() - 1);
+                if (sfx == '?') {
+                    required = false;
+                } else if (sfx == '*') {
+                    multiple = true;
+                    required = false;
+                } else {
+                    multiple = true;
+                }
+                inc = inc.substring(0, inc.length() - 1);
+            }
+            this.base = inc;
+        }
+
+        public String getBase() {
+            return base;
+        }
+
+        public boolean isMultiple() {
+            return multiple;
+        }
+
+        public boolean isRequired() {
+            return required;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((base == null) ? 0 : base.hashCode());
+            result = prime * result + (multiple ? 1231 : 1237);
+            result = prime * result + (required ? 1231 : 1237);
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            PropertyTemplate other = (PropertyTemplate) obj;
+            if (base == null) {
+                if (other.base != null) {
+                    return false;
+                }
+            } else if ( !base.equals(other.base)) {
+                return false;
+            }
+            if (multiple != other.multiple) {
+                return false;
+            }
+            if (required != other.required) {
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return base + (multiple ? (required ? "+" : "*") : (required ? "" : "?"));
+        }
+
+        @Override
+        public int compareTo(PropertyTemplate o) {
+            return toString().compareTo(o.toString());
+        }
+
+    }
+
+    private CertificateProfile(File f) throws IOException {
+        Properties p = new Properties();
+        try (FileInputStream inStream = new FileInputStream(f)) {
+            p.load(inStream);
+        }
+        String[] parts = f.getName().split("\\.")[0].split("-", 2);
+        id = Integer.parseInt(parts[0]);
+        keyName = parts[1];
+        visibleName = "";
+        pt = parsePropertyTemplates(p.getProperty("include"));
+        req = parseConditions(p.getProperty("requires", ""));
+    }
+
+    private List<String> parseConditions(String property) {
+        String[] split2 = property.split(",");
+        if (split2.length == 1 && split2[0].equals("")) {
+            split2 = new String[0];
+        }
+        return Collections.unmodifiableList(Arrays.asList(split2));
+    }
+
+    private Map<String, PropertyTemplate> parsePropertyTemplates(String property) {
+        String[] split = property.split(",");
+        HashMap<String, PropertyTemplate> map = new HashMap<>(split.length);
+        for (int i = 0; i < split.length; i++) {
+
+            PropertyTemplate value = new PropertyTemplate(split[i]);
+
+            map.put(value.getBase(), value);
+        }
+        return Collections.unmodifiableMap(map);
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String getKeyName() {
+        return keyName;
+    }
+
+    public String getVisibleName() {
+        return visibleName;
+    }
+
+    public Map<String, PropertyTemplate> getTemplates() {
+        return pt;
+    }
+
+    public List<String> getReqireds() {
+        return req;
+    }
+
+    static {
+        final HashMap<String, CertificateProfile> myName = new HashMap<String, CertificateProfile>();
+        final HashMap<Integer, CertificateProfile> myId = new HashMap<Integer, CertificateProfile>();
+
+        for (File f : new File("config/profiles").listFiles()) {
+            Properties p = new Properties();
+            try (FileInputStream inStream = new FileInputStream(f)) {
+                p.load(inStream);
+            } catch (IOException e) {
+                throw new Error("Unable to load profile from " + f.getName(), e);
+            }
+
+            String[] parts = f.getName().split("\\.")[0].split("-", 2);
+            try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `keyname`, `include`, `requires`, `name` FROM `profiles` WHERE `id`=?")) {
+                ps.setInt(1, Integer.parseInt(parts[0]));
+                GigiResultSet rs = ps.executeQuery();
+
+                if (rs.next()) {
+                    if ( !rs.getString("keyname").equals(parts[1])) {
+                        throw new Error("Config error. Certificate Profile mismatch");
+                    }
+                    if ( !rs.getString("include").equals(p.getProperty("include"))) {
+                        throw new Error("Config error. Certificate Profile mismatch");
+                    }
+                    if ( !rs.getString("requires").equals(p.getProperty("requires", ""))) {
+                        throw new Error("Config error. Certificate Profile mismatch");
+                    }
+                } else {
+                    try (GigiPreparedStatement insert = new GigiPreparedStatement("INSERT INTO `profiles` SET `keyname`=?, `include`=?, `requires`=?, `name`=?, `id`=?")) {
+                        insert.setString(1, parts[1]);
+                        insert.setString(2, p.getProperty("include"));
+                        insert.setString(3, p.getProperty("requires", ""));
+                        insert.setString(4, p.getProperty("name"));
+                        insert.setInt(5, Integer.parseInt(parts[0]));
+                        insert.execute();
+                    }
+                }
+            }
+        }
+
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id`, `keyname`, `name`, `requires`, `include` FROM `profiles`")) {
+            GigiResultSet rs = ps.executeQuery();
+            while (rs.next()) {
+                CertificateProfile cp = new CertificateProfile(rs.getInt("id"), rs.getString("keyName"), rs.getString("name"), rs.getString("requires"), rs.getString("include"));
+                myId.put(cp.getId(), cp);
+                myName.put(cp.getKeyName(), cp);
+            }
+        }
+        byName = Collections.unmodifiableMap(myName);
+        byId = Collections.unmodifiableMap(myId);
+    }
+
+    public static CertificateProfile getById(int id) {
+        return byId.get(id);
+    }
+
+    public static CertificateProfile getByName(String name) {
+        return byName.get(name);
+    }
+
+    public static CertificateProfile[] getAll() {
+        return byId.values().toArray(new CertificateProfile[byId.size()]);
+    }
+
+    public boolean canBeIssuedBy(CertificateOwner owner, User actor) {
+        if (pt.containsKey("orga")) {
+            if ( !(owner instanceof Organisation)) {
+                return false;
+            }
+        } else {
+            if (owner instanceof Organisation) {
+                return false;
+            }
+        }
+        for (String s : req) {
+            if (s.equals("points>=50")) {
+                if (actor.getAssurancePoints() < 50) {
+                    return false;
+                }
+            } else if (s.equals("points>=100")) {
+                if (actor.getAssurancePoints() < 100) {
+                    return false;
+                }
+            } else if (s.equals("codesign")) {
+                if ( !actor.isInGroup(Group.CODESIGNING)) {
+                    return false;
+                }
+            } else if (s.equals("ocsp")) {
+                if ( !(owner instanceof Organisation)) {
+                    return false;
+                }
+                Organisation o = (Organisation) owner;
+                if ( !o.isSelfOrganisation()) {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+
+        }
+        return true;
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/Digest.java b/src/org/cacert/gigi/dbObjects/Digest.java
new file mode 100644 (file)
index 0000000..5924712
--- /dev/null
@@ -0,0 +1,23 @@
+package org.cacert.gigi.dbObjects;
+
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.TranslateCommand;
+
+public enum Digest {
+    SHA256("Currently recommended, because the other algorithms" + " might break on some older versions of the GnuTLS library" + " (older than 3.x) still shipped in Debian for example."), SHA384(""), SHA512("Highest protection against hash collision attacks of the algorithms offered here.");
+
+    private final Outputable exp;
+
+    private Digest(String explanation) {
+        exp = new TranslateCommand(explanation);
+    }
+
+    public Outputable getExp() {
+        return exp;
+    }
+
+    public static Digest getDefault() {
+        return SHA256;
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/Domain.java b/src/org/cacert/gigi/dbObjects/Domain.java
new file mode 100644 (file)
index 0000000..fa6a6d0
--- /dev/null
@@ -0,0 +1,265 @@
+package org.cacert.gigi.dbObjects;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.IDN;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.util.PublicSuffixes;
+
+public class Domain implements IdCachable, Verifyable {
+
+    private CertificateOwner owner;
+
+    private String suffix;
+
+    private int id;
+
+    private static final Set<String> IDNEnabledTLDs;
+
+    static {
+        Properties CPS = new Properties();
+        try (InputStream resourceAsStream = Domain.class.getResourceAsStream("CPS.properties")) {
+            CPS.load(resourceAsStream);
+            IDNEnabledTLDs = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(CPS.getProperty("IDN-enabled").split(","))));
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
+    private Domain(int id) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid`, `domain` FROM `domains` WHERE `id`=? AND `deleted` IS NULL")) {
+            ps.setInt(1, id);
+
+            GigiResultSet rs = ps.executeQuery();
+            if ( !rs.next()) {
+                throw new IllegalArgumentException("Invalid domain id " + id);
+            }
+            this.id = id;
+            owner = CertificateOwner.getById(rs.getInt(1));
+            suffix = rs.getString(2);
+        }
+    }
+
+    public Domain(User actor, CertificateOwner owner, String suffix) throws GigiApiException {
+        suffix = suffix.toLowerCase();
+        synchronized (Domain.class) {
+            checkCertifyableDomain(suffix, actor.isInGroup(Group.CODESIGNING));
+            this.owner = owner;
+            this.suffix = suffix;
+            insert();
+        }
+    }
+
+    public static void checkCertifyableDomain(String s, boolean hasPunycodeRight) throws GigiApiException {
+        String[] parts = s.split("\\.", -1);
+        if (parts.length < 2) {
+            throw new GigiApiException("Domain does not contain '.'.");
+        }
+        for (int i = parts.length - 1; i >= 0; i--) {
+            if ( !isVaildDomainPart(parts[i], hasPunycodeRight)) {
+                throw new GigiApiException("Syntax error in Domain");
+            }
+        }
+        String publicSuffix = PublicSuffixes.getInstance().getRegistrablePart(s);
+        if ( !s.equals(publicSuffix)) {
+            throw new GigiApiException("You may only register a domain with exactly one lable before the public suffix.");
+        }
+        if (("." + s).matches("(\\.[0-9]*)*")) {
+            // This is not reached because we currently have no TLD that is
+            // numbers only. But who knows..
+            // Better safe than sorry.
+            throw new GigiApiException("IP Addresses are not allowed");
+        }
+        checkPunycode(parts[0], s.substring(parts[0].length() + 1));
+    }
+
+    private static void checkPunycode(String label, String domainContext) throws GigiApiException {
+        if (label.charAt(2) != '-' || label.charAt(3) != '-') {
+            return; // is no punycode
+        }
+        if ( !IDNEnabledTLDs.contains(domainContext)) {
+            throw new GigiApiException("Punycode label could not be positively verified.");
+        }
+        if ( !label.startsWith("xn--")) {
+            throw new GigiApiException("Unknown ACE prefix.");
+        }
+        try {
+            String unicode = IDN.toUnicode(label);
+            if (unicode.startsWith("xn--")) {
+                throw new GigiApiException("Punycode label could not be positively verified.");
+            }
+        } catch (IllegalArgumentException e) {
+            throw new GigiApiException("Punycode label could not be positively verified.");
+        }
+    }
+
+    public static boolean isVaildDomainPart(String s, boolean allowPunycode) {
+        if ( !s.matches("[a-z0-9-]+")) {
+            return false;
+        }
+        if (s.charAt(0) == '-' || s.charAt(s.length() - 1) == '-') {
+            return false;
+        }
+        if (s.length() > 63) {
+            return false;
+        }
+        boolean canBePunycode = s.length() >= 4 && s.charAt(2) == '-' && s.charAt(3) == '-';
+        if (canBePunycode && !allowPunycode) {
+            return false;
+        }
+        return true;
+    }
+
+    private static void checkInsert(String suffix) throws GigiApiException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `domains` WHERE (`domain`=? OR (CONCAT('.', `domain`)=RIGHT(?,LENGTH(`domain`)+1)  OR RIGHT(`domain`,LENGTH(?)+1)=CONCAT('.',?))) AND `deleted` IS NULL")) {
+            ps.setString(1, suffix);
+            ps.setString(2, suffix);
+            ps.setString(3, suffix);
+            ps.setString(4, suffix);
+            GigiResultSet rs = ps.executeQuery();
+            boolean existed = rs.next();
+            rs.close();
+            if (existed) {
+                throw new GigiApiException("Domain could not be inserted. Domain is already known to the system.");
+            }
+        }
+    }
+
+    private void insert() throws GigiApiException {
+        if (id != 0) {
+            throw new GigiApiException("already inserted.");
+        }
+        checkInsert(suffix);
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `domains` SET memid=?, domain=?")) {
+            ps.setInt(1, owner.getId());
+            ps.setString(2, suffix);
+            ps.execute();
+            id = ps.lastInsertId();
+        }
+        myCache.put(this);
+    }
+
+    public void delete() throws GigiApiException {
+        if (id == 0) {
+            throw new GigiApiException("not inserted.");
+        }
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `domains` SET `deleted`=CURRENT_TIMESTAMP WHERE `id`=?")) {
+            ps.setInt(1, id);
+            ps.execute();
+        }
+    }
+
+    public CertificateOwner getOwner() {
+        return owner;
+    }
+
+    @Override
+    public int getId() {
+        return id;
+    }
+
+    public String getSuffix() {
+        return suffix;
+    }
+
+    private LinkedList<DomainPingConfiguration> configs = null;
+
+    public List<DomainPingConfiguration> getConfiguredPings() throws GigiApiException {
+        LinkedList<DomainPingConfiguration> configs = this.configs;
+        if (configs == null) {
+            configs = new LinkedList<>();
+            try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT id FROM pingconfig WHERE domainid=? AND `deleted` IS NULL")) {
+                ps.setInt(1, id);
+                GigiResultSet rs = ps.executeQuery();
+                while (rs.next()) {
+                    configs.add(DomainPingConfiguration.getById(rs.getInt(1)));
+                }
+            }
+            this.configs = configs;
+
+        }
+        return Collections.unmodifiableList(configs);
+    }
+
+    public void addPing(DomainPingType type, String config) throws GigiApiException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `pingconfig` SET `domainid`=?, `type`=?::`pingType`, `info`=?")) {
+            ps.setInt(1, id);
+            ps.setString(2, type.toString().toLowerCase());
+            ps.setString(3, config);
+            ps.execute();
+        }
+        configs = null;
+    }
+
+    public void clearPings() throws GigiApiException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `pingconfig` SET `deleted`=CURRENT_TIMESTAMP WHERE `deleted` is NULL AND `domainid`=?")) {
+            ps.setInt(1, id);
+            ps.execute();
+        }
+        configs = null;
+    }
+
+    public synchronized void verify(String hash) throws GigiApiException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `domainPinglog` SET `state`='success' WHERE `challenge`=? AND `state`='open' AND `configId` IN (SELECT `id` FROM `pingconfig` WHERE `domainid`=? AND `type`='email')")) {
+            ps.setString(1, hash);
+            ps.setInt(2, id);
+            ps.executeUpdate();
+        }
+    }
+
+    public boolean isVerified() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `domainPinglog` INNER JOIN `pingconfig` ON `pingconfig`.`id`=`domainPinglog`.`configId` WHERE `domainid`=? AND `state`='success'")) {
+            ps.setInt(1, id);
+            GigiResultSet rs = ps.executeQuery();
+            return rs.next();
+        }
+    }
+
+    public DomainPingExecution[] getPings() throws GigiApiException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `state`, `type`, `info`, `result`, `configId`, `when` FROM `domainPinglog` INNER JOIN `pingconfig` ON `pingconfig`.`id`=`domainPinglog`.`configId` WHERE `pingconfig`.`domainid`=? ORDER BY `when` DESC;", true)) {
+            ps.setInt(1, id);
+            GigiResultSet rs = ps.executeQuery();
+            rs.last();
+            DomainPingExecution[] contents = new DomainPingExecution[rs.getRow()];
+            rs.beforeFirst();
+            for (int i = 0; i < contents.length && rs.next(); i++) {
+                contents[i] = new DomainPingExecution(rs);
+            }
+            return contents;
+        }
+
+    }
+
+    private static final ObjectCache<Domain> myCache = new ObjectCache<>();
+
+    public static synchronized Domain getById(int id) throws IllegalArgumentException {
+        Domain em = myCache.get(id);
+        if (em == null) {
+            myCache.put(em = new Domain(id));
+        }
+        return em;
+    }
+
+    public static Domain searchUserIdByDomain(String domain) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id` FROM `domains` WHERE `domain` = ?")) {
+            ps.setString(1, domain);
+            GigiResultSet res = ps.executeQuery();
+            if (res.next()) {
+                return getById(res.getInt(1));
+            } else {
+                return null;
+            }
+        }
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/DomainPingConfiguration.java b/src/org/cacert/gigi/dbObjects/DomainPingConfiguration.java
new file mode 100644 (file)
index 0000000..772b066
--- /dev/null
@@ -0,0 +1,95 @@
+package org.cacert.gigi.dbObjects;
+
+import java.util.Date;
+
+import org.cacert.gigi.Gigi;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.output.template.SprintfCommand;
+
+public class DomainPingConfiguration implements IdCachable {
+
+    private static final int REPING_MINIMUM_DELAY = 5 * 60 * 1000;
+
+    private int id;
+
+    private Domain target;
+
+    private DomainPingType type;
+
+    private String info;
+
+    private DomainPingConfiguration(int id) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id`, `domainid`, `type`, `info` FROM `pingconfig` WHERE `id`=?")) {
+            ps.setInt(1, id);
+
+            GigiResultSet rs = ps.executeQuery();
+            if ( !rs.next()) {
+                throw new IllegalArgumentException("Invalid pingconfig id " + id);
+            }
+            this.id = rs.getInt("id");
+            target = Domain.getById(rs.getInt("domainid"));
+            type = DomainPingType.valueOf(rs.getString("type").toUpperCase());
+            info = rs.getString("info");
+        }
+    }
+
+    @Override
+    public int getId() {
+        return id;
+    }
+
+    public Domain getTarget() {
+        return target;
+    }
+
+    public DomainPingType getType() {
+        return type;
+    }
+
+    public String getInfo() {
+        return info;
+    }
+
+    private static ObjectCache<DomainPingConfiguration> cache = new ObjectCache<>();
+
+    public static synchronized DomainPingConfiguration getById(int id) {
+        DomainPingConfiguration res = cache.get(id);
+        if (res == null) {
+            cache.put(res = new DomainPingConfiguration(id));
+        }
+        return res;
+    }
+
+    public Date getLastExecution() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `when` AS stamp from `domainPinglog` WHERE `configId`=? ORDER BY `when` DESC LIMIT 1")) {
+            ps.setInt(1, id);
+            GigiResultSet rs = ps.executeQuery();
+            if (rs.next()) {
+                return new Date(rs.getTimestamp("stamp").getTime());
+            }
+            return new Date(0);
+        }
+    }
+
+    public Date getLastSuccess() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `when` AS stamp from `domainPinglog` WHERE `configId`=? AND state='success' ORDER BY `when` DESC LIMIT 1")) {
+            ps.setInt(1, id);
+            GigiResultSet rs = ps.executeQuery();
+            if (rs.next()) {
+                return new Date(rs.getTimestamp("stamp").getTime());
+            }
+            return new Date(0);
+        }
+    }
+
+    public synchronized void requestReping() throws GigiApiException {
+        Date lastExecution = getLastExecution();
+        if (lastExecution.getTime() + REPING_MINIMUM_DELAY < System.currentTimeMillis()) {
+            Gigi.notifyPinger(this);
+            return;
+        }
+        throw new GigiApiException(SprintfCommand.createSimple("Reping is only allowed after {0} minutes, yours end at {1}.", REPING_MINIMUM_DELAY / 60 / 1000, new Date(lastExecution.getTime() + REPING_MINIMUM_DELAY)));
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/DomainPingExecution.java b/src/org/cacert/gigi/dbObjects/DomainPingExecution.java
new file mode 100644 (file)
index 0000000..8512285
--- /dev/null
@@ -0,0 +1,55 @@
+package org.cacert.gigi.dbObjects;
+
+import java.sql.Timestamp;
+import java.util.Date;
+
+import org.cacert.gigi.database.GigiResultSet;
+
+public class DomainPingExecution {
+
+    private String state;
+
+    private String type;
+
+    private String info;
+
+    private String result;
+
+    private DomainPingConfiguration config;
+
+    private Timestamp date;
+
+    public DomainPingExecution(GigiResultSet rs) {
+        state = rs.getString(1);
+        type = rs.getString(2);
+        info = rs.getString(3);
+        result = rs.getString(4);
+        config = DomainPingConfiguration.getById(rs.getInt(5));
+        date = rs.getTimestamp(6);
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getInfo() {
+        return info;
+    }
+
+    public String getResult() {
+        return result;
+    }
+
+    public DomainPingConfiguration getConfig() {
+        return config;
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/DomainPingType.java b/src/org/cacert/gigi/dbObjects/DomainPingType.java
new file mode 100644 (file)
index 0000000..fcef3ce
--- /dev/null
@@ -0,0 +1,5 @@
+package org.cacert.gigi.dbObjects;
+
+public enum DomainPingType {
+    EMAIL, DNS, HTTP, SSL;
+}
diff --git a/src/org/cacert/gigi/dbObjects/EmailAddress.java b/src/org/cacert/gigi/dbObjects/EmailAddress.java
new file mode 100644 (file)
index 0000000..964d1b6
--- /dev/null
@@ -0,0 +1,152 @@
+package org.cacert.gigi.dbObjects;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.Locale;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.email.EmailProvider;
+import org.cacert.gigi.email.MailProbe;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.SprintfCommand;
+import org.cacert.gigi.util.RandomToken;
+
+public class EmailAddress implements IdCachable, Verifyable {
+
+    public static final int REPING_MINIMUM_DELAY = 5 * 60 * 1000;
+
+    private String address;
+
+    private int id;
+
+    private User owner;
+
+    private EmailAddress(int id) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid`, `email` FROM `emails` WHERE `id`=? AND `deleted` IS NULL")) {
+            ps.setInt(1, id);
+
+            GigiResultSet rs = ps.executeQuery();
+            if ( !rs.next()) {
+                throw new IllegalArgumentException("Invalid email id " + id);
+            }
+            this.id = id;
+            owner = User.getById(rs.getInt(1));
+            address = rs.getString(2);
+        }
+    }
+
+    public EmailAddress(User owner, String address, Locale mailLocale) throws GigiApiException {
+        address = address.toLowerCase();
+        if ( !EmailProvider.MAIL.matcher(address).matches()) {
+            throw new IllegalArgumentException("Invalid email.");
+        }
+        this.address = address;
+        this.owner = owner;
+        insert(Language.getInstance(mailLocale));
+    }
+
+    private void insert(Language l) throws GigiApiException {
+        try {
+            synchronized (EmailAddress.class) {
+                if (id != 0) {
+                    throw new IllegalStateException("already inserted.");
+                }
+                try (GigiPreparedStatement psCheck = new GigiPreparedStatement("SELECT 1 FROM `emails` WHERE email=? AND deleted is NULL"); GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `emails` SET memid=?, email=?")) {
+                    ps.setInt(1, owner.getId());
+                    ps.setString(2, address);
+                    psCheck.setString(1, address);
+                    GigiResultSet res = psCheck.executeQuery();
+                    if (res.next()) {
+                        throw new GigiApiException("The email address is already known to the system.");
+                    }
+                    ps.execute();
+                    id = ps.lastInsertId();
+                }
+                myCache.put(this);
+            }
+            ping(l);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void ping(Language l) throws IOException {
+        String hash = RandomToken.generateToken(16);
+        try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`='', `uid`=?, `type`='active', `status`='open'::`pingState`, `challenge`=?")) {
+            statmt.setString(1, address);
+            statmt.setInt(2, owner.getId());
+            statmt.setString(3, hash);
+            statmt.execute();
+        }
+
+        MailProbe.sendMailProbe(l, "email", id, hash, address);
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public synchronized void verify(String hash) throws GigiApiException {
+        try (GigiPreparedStatement stmt = new GigiPreparedStatement("UPDATE `emailPinglog` SET `status`='success'::`pingState` WHERE `email`=? AND `uid`=? AND `type`='active' AND `challenge`=?")) {
+            stmt.setString(1, address);
+            stmt.setInt(2, owner.getId());
+            stmt.setString(3, hash);
+            stmt.executeUpdate();
+        }
+        // Verify user with that primary email
+        try (GigiPreparedStatement ps2 = new GigiPreparedStatement("update `users` set `verified`='1' where `id`=? and `email`=? and `verified`='0'")) {
+            ps2.setInt(1, owner.getId());
+            ps2.setString(2, address);
+            ps2.execute();
+        }
+    }
+
+    public boolean isVerified() {
+        try (GigiPreparedStatement statmt = new GigiPreparedStatement("SELECT 1 FROM `emailPinglog` WHERE `email`=? AND `uid`=? AND `type`='active' AND `status`='success'")) {
+            statmt.setString(1, address);
+            statmt.setInt(2, owner.getId());
+            GigiResultSet e = statmt.executeQuery();
+            return e.next();
+        }
+    }
+
+    public Date getLastPing(boolean onlySuccess) {
+        Date lastExecution;
+        try (GigiPreparedStatement statmt = new GigiPreparedStatement("SELECT MAX(`when`) FROM `emailPinglog` WHERE `email`=? AND `uid`=? AND `type`='active'" + (onlySuccess ? " AND `status`='success'" : ""))) {
+            statmt.setString(1, address);
+            statmt.setInt(2, owner.getId());
+            GigiResultSet e = statmt.executeQuery();
+            if ( !e.next()) {
+                return null;
+            }
+            lastExecution = e.getTimestamp(1);
+        }
+        return lastExecution;
+    }
+
+    public synchronized void requestReping(Language l) throws IOException, GigiApiException {
+        Date lastExecution = getLastPing(false);
+
+        if (lastExecution != null && lastExecution.getTime() + REPING_MINIMUM_DELAY >= System.currentTimeMillis()) {
+            throw new GigiApiException(SprintfCommand.createSimple("Reping is only allowed after {0} minutes, yours end at {1}.", REPING_MINIMUM_DELAY / 60 / 1000, new Date(lastExecution.getTime() + REPING_MINIMUM_DELAY)));
+        }
+        ping(l);
+        return;
+    }
+
+    private static ObjectCache<EmailAddress> myCache = new ObjectCache<>();
+
+    public static synchronized EmailAddress getById(int id) throws IllegalArgumentException {
+        EmailAddress em = myCache.get(id);
+        if (em == null) {
+            myCache.put(em = new EmailAddress(id));
+        }
+        return em;
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/Group.java b/src/org/cacert/gigi/dbObjects/Group.java
new file mode 100644 (file)
index 0000000..16cde6c
--- /dev/null
@@ -0,0 +1,53 @@
+package org.cacert.gigi.dbObjects;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.TranslateCommand;
+
+public enum Group {
+    SUPPORTER("supporter", "supporter"), ARBITRATOR("arbitrator", "arbitrator"), //
+    BLOCKEDASSURER("blockedassurer", "may not assure"), BLOCKEDASSUREE("blockedassuree", "may not be assured"), //
+    BLOCKEDLOGIN("blockedlogin", "may not login"), BLOCKEDCERT("blockedcert", "may not issue certificates"), //
+    TTP_ASSURER("ttp-assurer", "may assure via TTP"), TTP_APPLICANT("ttp-applicant", "requests to be assured via ttp"), //
+    CODESIGNING("codesigning", "may issue codesigning certificates"), ORGASSURER("orgassurer", "may assure organisations"), //
+    NUCLEUS_ASSURER("nucleus-assurer", "may issue nucleus assurances");
+
+    private final String dbName;
+
+    private final TranslateCommand tc;
+
+    private Group(String name, String display) {
+        dbName = name;
+        tc = new TranslateCommand(display);
+    }
+
+    public static Group getByString(String name) {
+        return valueOf(name.toUpperCase().replace('-', '_'));
+    }
+
+    public String getDatabaseName() {
+        return dbName;
+    }
+
+    public User[] getMembers(int offset, int count) {
+        try (GigiPreparedStatement gps = new GigiPreparedStatement("SELECT `user` FROM `user_groups` WHERE `permission`=?::`userGroup` AND `deleted` IS NULL OFFSET ? LIMIT ?", true)) {
+            gps.setString(1, dbName);
+            gps.setInt(2, offset);
+            gps.setInt(3, count);
+            GigiResultSet grs = gps.executeQuery();
+            grs.last();
+            User[] users = new User[grs.getRow()];
+            int i = 0;
+            grs.beforeFirst();
+            while (grs.next()) {
+                users[i++] = User.getById(grs.getInt(1));
+            }
+            return users;
+        }
+    }
+
+    public Outputable getName() {
+        return tc;
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/IdCachable.java b/src/org/cacert/gigi/dbObjects/IdCachable.java
new file mode 100644 (file)
index 0000000..af439bc
--- /dev/null
@@ -0,0 +1,7 @@
+package org.cacert.gigi.dbObjects;
+
+public interface IdCachable {
+
+    public int getId();
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/Job.java b/src/org/cacert/gigi/dbObjects/Job.java
new file mode 100644 (file)
index 0000000..bb357a8
--- /dev/null
@@ -0,0 +1,95 @@
+package org.cacert.gigi.dbObjects;
+
+import java.sql.Date;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.output.CertificateValiditySelector;
+
+public class Job implements IdCachable {
+
+    private int id;
+
+    private Job(int id) {
+        this.id = id;
+    }
+
+    public static enum JobType {
+        SIGN("sign"), REVOKE("revoke");
+
+        private final String name;
+
+        private JobType(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    public synchronized static Job sign(Certificate targetId, Date start, String period) throws GigiApiException {
+        CertificateValiditySelector.checkValidityLength(period);
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `jobs` SET targetId=?, task=?::`jobType`, executeFrom=?, executeTo=?")) {
+            ps.setInt(1, targetId.getId());
+            ps.setString(2, JobType.SIGN.getName());
+            ps.setDate(3, start);
+            ps.setString(4, period);
+            ps.execute();
+            return cache.put(new Job(ps.lastInsertId()));
+        }
+    }
+
+    public synchronized static Job revoke(Certificate targetId) {
+
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `jobs` SET targetId=?, task=?::`jobType`")) {
+            ps.setInt(1, targetId.getId());
+            ps.setString(2, JobType.REVOKE.getName());
+            ps.execute();
+            return cache.put(new Job(ps.lastInsertId()));
+        }
+    }
+
+    public synchronized boolean waitFor(int max) throws InterruptedException {
+        long start = System.currentTimeMillis();
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `jobs` WHERE id=? AND state='open'")) {
+            ps.setInt(1, id);
+            GigiResultSet rs = ps.executeQuery();
+            while (rs.next()) {
+                rs.close();
+                if (max != 0 && System.currentTimeMillis() - start > max) {
+                    return false;
+                }
+                Thread.sleep((long) (2000 + Math.random() * 2000));
+                rs = ps.executeQuery();
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public int getId() {
+        return id;
+    }
+
+    static ObjectCache<Job> cache = new ObjectCache<>();
+
+    public synchronized static Job getById(int id) {
+        Job i = cache.get(id);
+        if (i != null) {
+            return i;
+        }
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `jobs` WHERE id=?'")) {
+            ps.setInt(1, id);
+            GigiResultSet rs = ps.executeQuery();
+            if (rs.next()) {
+                Job j = new Job(id);
+                cache.put(j);
+                return j;
+            }
+            return null;
+        }
+
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/Name.java b/src/org/cacert/gigi/dbObjects/Name.java
new file mode 100644 (file)
index 0000000..d0cf00f
--- /dev/null
@@ -0,0 +1,123 @@
+package org.cacert.gigi.dbObjects;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import org.cacert.gigi.dbObjects.wrappers.DataContainer;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.util.HTMLEncoder;
+
+@DataContainer
+public class Name implements Outputable {
+
+    private final String fname;
+
+    private final String mname;
+
+    private final String lname;
+
+    private final String suffix;
+
+    public Name(String fname, String lname, String mname, String suffix) {
+        this.fname = fname;
+        this.lname = lname;
+        this.mname = mname;
+        this.suffix = suffix;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.println("<span class=\"accountdetail\">");
+        out.print("<span class=\"fname\">");
+        out.print(HTMLEncoder.encodeHTML(fname));
+        out.print("</span> ");
+        out.print("<span class=\"lname\">");
+        out.print(HTMLEncoder.encodeHTML(lname));
+        out.print("</span>");
+        out.println("</span>");
+    }
+
+    @Override
+    public String toString() {
+        return fname + " " + lname;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((fname == null) ? 0 : fname.hashCode());
+        result = prime * result + ((lname == null) ? 0 : lname.hashCode());
+        result = prime * result + ((mname == null) ? 0 : mname.hashCode());
+        result = prime * result + ((suffix == null) ? 0 : suffix.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Name other = (Name) obj;
+        if (fname == null) {
+            if (other.fname != null) {
+                return false;
+            }
+        } else if ( !fname.equals(other.fname)) {
+            return false;
+        }
+        if (lname == null) {
+            if (other.lname != null) {
+                return false;
+            }
+        } else if ( !lname.equals(other.lname)) {
+            return false;
+        }
+        if (mname == null) {
+            if (other.mname != null) {
+                return false;
+            }
+        } else if ( !mname.equals(other.mname)) {
+            return false;
+        }
+        if (suffix == null) {
+            if (other.suffix != null) {
+                return false;
+            }
+        } else if ( !suffix.equals(other.suffix)) {
+            return false;
+        }
+        return true;
+    }
+
+    public boolean matches(String text) {
+        return text.equals(fname + " " + lname) || //
+                (mname != null && text.equals(fname + " " + mname + " " + lname)) || //
+                (suffix != null && text.equals(fname + " " + lname + " " + suffix)) || //
+                (mname != null && suffix != null && text.equals(fname + " " + mname + " " + lname + " " + suffix));
+    }
+
+    public String getFname() {
+        return fname;
+    }
+
+    public String getLname() {
+        return lname;
+    }
+
+    public String getMname() {
+        return mname;
+    }
+
+    public String getSuffix() {
+        return suffix;
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/ObjectCache.java b/src/org/cacert/gigi/dbObjects/ObjectCache.java
new file mode 100644 (file)
index 0000000..0d9fc14
--- /dev/null
@@ -0,0 +1,39 @@
+package org.cacert.gigi.dbObjects;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class ObjectCache<T extends IdCachable> {
+
+    private final HashMap<Integer, WeakReference<T>> hashmap = new HashMap<>();
+
+    private static final HashSet<ObjectCache<? extends IdCachable>> caches = new HashSet<>();
+
+    protected ObjectCache() {
+        caches.add(this);
+    }
+
+    public T put(T c) {
+        hashmap.put(c.getId(), new WeakReference<T>(c));
+        return c;
+    }
+
+    public T get(int id) {
+        WeakReference<T> res = hashmap.get(id);
+        if (res != null) {
+            return res.get();
+        }
+        return null;
+    }
+
+    public static void clearAllCaches() {
+        for (ObjectCache<? extends IdCachable> objectCache : caches) {
+            objectCache.hashmap.clear();
+        }
+    }
+
+    public void remove(T toRm) {
+        hashmap.remove(toRm);
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/Organisation.java b/src/org/cacert/gigi/dbObjects/Organisation.java
new file mode 100644 (file)
index 0000000..101c973
--- /dev/null
@@ -0,0 +1,256 @@
+package org.cacert.gigi.dbObjects;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.wrappers.DataContainer;
+
+public class Organisation extends CertificateOwner {
+
+    @DataContainer
+    public static class Affiliation {
+
+        private final User target;
+
+        private final boolean master;
+
+        private final String fixedOU;
+
+        private Organisation o;
+
+        public Affiliation(Organisation o, User target, boolean master, String fixedOU) {
+            this.o = o;
+            this.target = target;
+            this.master = master;
+            this.fixedOU = fixedOU;
+        }
+
+        public User getTarget() {
+            return target;
+        }
+
+        public boolean isMaster() {
+            return master;
+        }
+
+        public String getFixedOU() {
+            return fixedOU;
+        }
+
+        public Organisation getOrganisation() {
+            return o;
+        }
+    }
+
+    private String name;
+
+    private String state;
+
+    private String province;
+
+    private String city;
+
+    private String email;
+
+    private String optionalName;
+
+    private String postalAddress;
+
+    public Organisation(String name, String state, String province, String city, String email, String optionalName, String postalAddress, User creator) throws GigiApiException {
+        if ( !creator.isInGroup(Group.ORGASSURER)) {
+            throw new GigiApiException("Only org-assurers may create organisations.");
+        }
+        this.name = name;
+        this.state = state;
+        this.province = province;
+        this.city = city;
+        this.email = email;
+        this.optionalName = optionalName;
+        this.postalAddress = postalAddress;
+        int id = getId();
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO organisations SET id=?, name=?, state=?, province=?, city=?, contactEmail=?, optional_name=?, postal_address=?, creator=?")) {
+            ps.setInt(1, id);
+            ps.setString(2, name);
+            ps.setString(3, state);
+            ps.setString(4, province);
+            ps.setString(5, city);
+            ps.setString(6, email);
+            ps.setString(7, optionalName);
+            ps.setString(8, postalAddress);
+            ps.setInt(9, creator.getId());
+            synchronized (Organisation.class) {
+                ps.execute();
+            }
+        }
+    }
+
+    protected Organisation(GigiResultSet rs) {
+        super(rs.getInt("id"));
+        name = rs.getString("name");
+        state = rs.getString("state");
+        province = rs.getString("province");
+        city = rs.getString("city");
+        email = rs.getString("contactEmail");
+        optionalName = rs.getString("optional_name");
+        postalAddress = rs.getString("postal_address");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public String getProvince() {
+        return province;
+    }
+
+    public String getCity() {
+        return city;
+    }
+
+    public String getContactEmail() {
+        return email;
+    }
+
+    public String getOptionalName() {
+        return optionalName;
+    }
+
+    public String getPostalAddress() {
+        return postalAddress;
+    }
+
+    public static synchronized Organisation getById(int id) {
+        CertificateOwner co = CertificateOwner.getById(id);
+        if (co instanceof Organisation) {
+            return (Organisation) co;
+        }
+        throw new IllegalArgumentException("Organisation not found.");
+    }
+
+    public synchronized void addAdmin(User admin, User actor, boolean master) throws GigiApiException {
+        if ( !admin.canAssure()) {
+            throw new GigiApiException("Cannot add non-assurer.");
+        }
+        if ( !actor.isInGroup(Group.ORGASSURER) && !isMaster(actor)) {
+            throw new GigiApiException("Only org assurer or master-admin may add admins to an organisation.");
+        }
+        try (GigiPreparedStatement ps1 = new GigiPreparedStatement("SELECT 1 FROM `org_admin` WHERE `orgid`=? AND `memid`=? AND `deleted` IS NULL")) {
+            ps1.setInt(1, getId());
+            ps1.setInt(2, admin.getId());
+            GigiResultSet result = ps1.executeQuery();
+            if (result.next()) {
+                return;
+            }
+        }
+        try (GigiPreparedStatement ps2 = new GigiPreparedStatement("INSERT INTO `org_admin` SET `orgid`=?, `memid`=?, `creator`=?, `master`=?::`yesno`")) {
+            ps2.setInt(1, getId());
+            ps2.setInt(2, admin.getId());
+            ps2.setInt(3, actor.getId());
+            ps2.setString(4, master ? "y" : "n");
+            ps2.execute();
+        }
+    }
+
+    public void removeAdmin(User admin, User actor) throws GigiApiException {
+        if ( !actor.isInGroup(Group.ORGASSURER) && !isMaster(actor)) {
+            throw new GigiApiException("Only org assurer or master-admin may delete admins from an organisation.");
+        }
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE org_admin SET deleter=?, deleted=NOW() WHERE orgid=? AND memid=?")) {
+            ps.setInt(1, actor.getId());
+            ps.setInt(2, getId());
+            ps.setInt(3, admin.getId());
+            ps.execute();
+        }
+    }
+
+    public List<Affiliation> getAllAdmins() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid`, `master` FROM `org_admin` WHERE `orgid`=? AND `deleted` IS NULL", true)) {
+            ps.setInt(1, getId());
+            GigiResultSet rs = ps.executeQuery();
+            rs.last();
+            ArrayList<Affiliation> al = new ArrayList<>(rs.getRow());
+            rs.beforeFirst();
+            while (rs.next()) {
+                al.add(new Affiliation(this, User.getById(rs.getInt(1)), rs.getString(2).equals("y"), null));
+            }
+            return al;
+        }
+    }
+
+    public static Organisation[] getOrganisations(int offset, int count) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `certOwners`.`id` FROM `organisations` INNER JOIN `certOwners` ON `certOwners`.`id`=`organisations`.`id` WHERE `certOwners`.`deleted` IS NULL OFFSET ? LIMIT ?", true)) {
+            ps.setInt(1, offset);
+            ps.setInt(2, count);
+            GigiResultSet res = ps.executeQuery();
+            res.last();
+            Organisation[] resu = new Organisation[res.getRow()];
+            res.beforeFirst();
+            int i = 0;
+            while (res.next()) {
+                resu[i++] = getById(res.getInt(1));
+            }
+            return resu;
+        }
+    }
+
+    public void updateCertData(String o, String c, String st, String l) {
+        for (Certificate cert : getCertificates(false)) {
+            if (cert.getStatus() == CertificateStatus.ISSUED) {
+                cert.revoke();
+            }
+        }
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `organisations` SET `name`=?, `state`=?, `province`=?, `city`=? WHERE `id`=?")) {
+            ps.setString(1, o);
+            ps.setString(2, c);
+            ps.setString(3, st);
+            ps.setString(4, l);
+            ps.setInt(5, getId());
+            ps.executeUpdate();
+        }
+        name = o;
+        state = c;
+        province = st;
+        city = l;
+    }
+
+    public void updateOrgData(String mail, String o_name, String p_address) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `organisations` SET `contactEmail`=?, `optional_name`=?, `postal_address`=? WHERE `id`=?")) {
+            ps.setString(1, mail);
+            ps.setString(2, o_name);
+            ps.setString(3, p_address);
+            ps.setInt(4, getId());
+            ps.executeUpdate();
+        }
+        email = mail;
+        optionalName = o_name;
+        postalAddress = p_address;
+    }
+
+    public boolean isMaster(User u) {
+        for (Affiliation i : getAllAdmins()) {
+            if (i.isMaster() && i.getTarget() == u) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isValidEmail(String email) {
+        return isValidDomain(email.split("@", 2)[1]);
+    }
+
+    public static final String SELF_ORG_NAME = "SomeCA";
+
+    public boolean isSelfOrganisation() {
+        return SELF_ORG_NAME.equals(getName());
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/SupportedUser.java b/src/org/cacert/gigi/dbObjects/SupportedUser.java
new file mode 100644 (file)
index 0000000..df5b54e
--- /dev/null
@@ -0,0 +1,92 @@
+package org.cacert.gigi.dbObjects;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.util.DayDate;
+
+public class SupportedUser {
+
+    private User target;
+
+    private User supporter;
+
+    private String ticket;
+
+    public SupportedUser(User target, User supporter, String ticket) {
+        this.supporter = supporter;
+        this.target = target;
+        this.ticket = ticket;
+    }
+
+    public boolean setName(Name newName) throws GigiApiException {
+        if (newName.equals(target.getName())) {
+            return false;
+        }
+        writeSELog("SE Name change");
+        target.setName(newName);
+        return true;
+    }
+
+    public boolean setDob(DayDate dob) throws GigiApiException {
+        if (dob.equals(target.getDoB())) {
+            return false;
+        }
+        writeSELog("SE dob change");
+        target.setDoB(dob);
+        return true;
+    }
+
+    public void revokeAllCertificates() throws GigiApiException {
+        writeSELog("SE Revoke certificates");
+        Certificate[] certs = target.getCertificates(false);
+        // TODO Check for open jobs!
+        for (int i = 0; i < certs.length; i++) {
+            if (certs[i].getStatus() == CertificateStatus.ISSUED) {
+                certs[i].revoke();
+            }
+        }
+    }
+
+    private void writeSELog(String type) throws GigiApiException {
+        if (ticket == null) {
+            throw new GigiApiException("No ticket set!");
+        }
+        try (GigiPreparedStatement prep = new GigiPreparedStatement("INSERT INTO `adminLog` SET uid=?, admin=?, type=?, information=?")) {
+            prep.setInt(1, target.getId());
+            prep.setInt(2, supporter.getId());
+            prep.setString(3, type);
+            prep.setString(4, ticket);
+            prep.executeUpdate();
+        }
+    }
+
+    public int getId() {
+        return target.getId();
+    }
+
+    public Certificate[] getCertificates(boolean includeRevoked) {
+        return target.getCertificates(includeRevoked);
+    }
+
+    public String getTicket() {
+        return ticket;
+    }
+
+    public User getTargetUser() {
+        return target;
+    }
+
+    public void submitSupportAction() throws GigiApiException {
+        target.rawUpdateUserData();
+    }
+
+    public void grant(Group toMod) {
+        target.grantGroup(supporter, toMod);
+    }
+
+    public void revoke(Group toMod) {
+        target.revokeGroup(supporter, toMod);
+    }
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/User.java b/src/org/cacert/gigi/dbObjects/User.java
new file mode 100644 (file)
index 0000000..567da97
--- /dev/null
@@ -0,0 +1,558 @@
+package org.cacert.gigi.dbObjects;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.pages.PasswordResetPage;
+import org.cacert.gigi.util.CalendarUtil;
+import org.cacert.gigi.util.DayDate;
+import org.cacert.gigi.util.Notary;
+import org.cacert.gigi.util.PasswordHash;
+import org.cacert.gigi.util.PasswordStrengthChecker;
+
+/**
+ * Represents an acting, assurable, user. Synchronizing on user means: no
+ * name-change and no assurance.
+ */
+public class User extends CertificateOwner {
+
+    private Name name = new Name(null, null, null, null);
+
+    private DayDate dob;
+
+    private String email;
+
+    private Assurance[] receivedAssurances;
+
+    private Assurance[] madeAssurances;
+
+    private Locale locale;
+
+    private final Set<Group> groups = new HashSet<>();
+
+    public static final int MINIMUM_AGE = 16;
+
+    public static final int POJAM_AGE = 14;
+
+    public static final int ADULT_AGE = 18;
+
+    public static final boolean POJAM_ENABLED = false;
+
+    protected User(GigiResultSet rs) {
+        super(rs.getInt("id"));
+        updateName(rs);
+    }
+
+    private void updateName(GigiResultSet rs) {
+        name = new Name(rs.getString("fname"), rs.getString("lname"), rs.getString("mname"), rs.getString("suffix"));
+        dob = new DayDate(rs.getDate("dob"));
+        email = rs.getString("email");
+
+        String localeStr = rs.getString("language");
+        if (localeStr == null || localeStr.equals("")) {
+            locale = Locale.getDefault();
+        } else {
+            locale = Language.getLocaleFromString(localeStr);
+        }
+
+        try (GigiPreparedStatement psg = new GigiPreparedStatement("SELECT `permission` FROM `user_groups` WHERE `user`=? AND `deleted` is NULL")) {
+            psg.setInt(1, rs.getInt("id"));
+
+            try (GigiResultSet rs2 = psg.executeQuery()) {
+                while (rs2.next()) {
+                    groups.add(Group.getByString(rs2.getString(1)));
+                }
+            }
+        }
+    }
+
+    public User(String email, String password, Name name, DayDate dob, Locale locale) throws GigiApiException {
+        this.email = email;
+        this.dob = dob;
+        this.name = name;
+        this.locale = locale;
+        try (GigiPreparedStatement query = new GigiPreparedStatement("INSERT INTO `users` SET `email`=?, `password`=?, " + "`fname`=?, `mname`=?, `lname`=?, " + "`suffix`=?, `dob`=?, `language`=?, id=?")) {
+            query.setString(1, email);
+            query.setString(2, PasswordHash.hash(password));
+            query.setString(3, name.getFname());
+            query.setString(4, name.getMname());
+            query.setString(5, name.getLname());
+            query.setString(6, name.getSuffix());
+            query.setDate(7, dob.toSQLDate());
+            query.setString(8, locale.toString());
+            query.setInt(9, getId());
+            query.execute();
+        }
+        new EmailAddress(this, email, locale);
+    }
+
+    public Name getName() {
+        return name;
+    }
+
+    public DayDate getDoB() {
+        return dob;
+    }
+
+    public void setDoB(DayDate dob) {
+        this.dob = dob;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void changePassword(String oldPass, String newPass) throws GigiApiException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `password` FROM `users` WHERE `id`=?")) {
+            ps.setInt(1, getId());
+            try (GigiResultSet rs = ps.executeQuery()) {
+                if ( !rs.next()) {
+                    throw new GigiApiException("User not found... very bad.");
+                }
+                if (PasswordHash.verifyHash(oldPass, rs.getString(1)) == null) {
+                    throw new GigiApiException("Old password does not match.");
+                }
+            }
+        }
+        setPassword(newPass);
+    }
+
+    private void setPassword(String newPass) throws GigiApiException {
+        PasswordStrengthChecker.assertStrongPassword(newPass, getName(), getEmail());
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE users SET `password`=? WHERE id=?")) {
+            ps.setString(1, PasswordHash.hash(newPass));
+            ps.setInt(2, getId());
+            ps.executeUpdate();
+        }
+    }
+
+    public void setName(Name name) {
+        this.name = name;
+    }
+
+    public boolean canAssure() {
+        if (POJAM_ENABLED) {
+            if ( !CalendarUtil.isOfAge(dob, POJAM_AGE)) { // PoJAM
+                return false;
+            }
+        } else {
+            if ( !CalendarUtil.isOfAge(dob, ADULT_AGE)) {
+                return false;
+            }
+        }
+        if (getAssurancePoints() < 100) {
+            return false;
+        }
+
+        return hasPassedCATS();
+
+    }
+
+    public boolean hasPassedCATS() {
+        try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT 1 FROM `cats_passed` where `user_id`=? AND `variant_id`=?")) {
+            query.setInt(1, getId());
+            query.setInt(2, CATS.ASSURER_CHALLENGE_ID);
+            try (GigiResultSet rs = query.executeQuery()) {
+                if (rs.next()) {
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        }
+    }
+
+    public int getAssurancePoints() {
+        try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT sum(points) FROM `notary` where `to`=? AND `deleted` is NULL AND (`expire` IS NULL OR `expire` > CURRENT_TIMESTAMP)")) {
+            query.setInt(1, getId());
+
+            GigiResultSet rs = query.executeQuery();
+            int points = 0;
+
+            if (rs.next()) {
+                points = rs.getInt(1);
+            }
+
+            return points;
+        }
+    }
+
+    public int getExperiencePoints() {
+        try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT count(*) FROM `notary` where `from`=? AND `deleted` is NULL")) {
+            query.setInt(1, getId());
+
+            GigiResultSet rs = query.executeQuery();
+            int points = 0;
+
+            if (rs.next()) {
+                points = rs.getInt(1) * 2;
+            }
+
+            return points;
+        }
+    }
+
+    /**
+     * Gets the maximum allowed points NOW. Note that an assurance needs to
+     * re-check PoJam as it has taken place in the past.
+     * 
+     * @return the maximal points @
+     */
+    public int getMaxAssurePoints() {
+        if ( !CalendarUtil.isOfAge(dob, ADULT_AGE) && POJAM_ENABLED) {
+            return 10; // PoJAM
+        }
+
+        int exp = getExperiencePoints();
+        int points = 10;
+
+        if (exp >= 10) {
+            points += 5;
+        }
+        if (exp >= 20) {
+            points += 5;
+        }
+        if (exp >= 30) {
+            points += 5;
+        }
+        if (exp >= 40) {
+            points += 5;
+        }
+        if (exp >= 50) {
+            points += 5;
+        }
+
+        return points;
+    }
+
+    public boolean isValidName(String name) {
+        return getName().matches(name);
+    }
+
+    public void updateDefaultEmail(EmailAddress newMail) throws GigiApiException {
+        for (EmailAddress email : getEmails()) {
+            if (email.getAddress().equals(newMail.getAddress())) {
+                if ( !email.isVerified()) {
+                    throw new GigiApiException("Email not verified.");
+                }
+
+                try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE users SET email=? WHERE id=?")) {
+                    ps.setString(1, newMail.getAddress());
+                    ps.setInt(2, getId());
+                    ps.execute();
+                }
+
+                this.email = newMail.getAddress();
+                return;
+            }
+        }
+
+        throw new GigiApiException("Given address not an address of the user.");
+    }
+
+    public void deleteEmail(EmailAddress delMail) throws GigiApiException {
+        if (getEmail().equals(delMail.getAddress())) {
+            throw new GigiApiException("Can't delete user's default e-mail.");
+        }
+
+        for (EmailAddress email : getEmails()) {
+            if (email.getId() == delMail.getId()) {
+                try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `emails` SET `deleted`=CURRENT_TIMESTAMP WHERE `id`=?")) {
+                    ps.setInt(1, delMail.getId());
+                    ps.execute();
+                }
+                return;
+            }
+        }
+        throw new GigiApiException("Email not one of user's email addresses.");
+    }
+
+    public synchronized Assurance[] getReceivedAssurances() {
+        if (receivedAssurances == null) {
+            try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT * FROM `notary` WHERE `to`=? AND `deleted` IS NULL")) {
+                query.setInt(1, getId());
+
+                GigiResultSet res = query.executeQuery();
+                List<Assurance> assurances = new LinkedList<Assurance>();
+
+                while (res.next()) {
+                    assurances.add(assuranceByRes(res));
+                }
+
+                this.receivedAssurances = assurances.toArray(new Assurance[0]);
+            }
+        }
+
+        return receivedAssurances;
+    }
+
+    public synchronized Assurance[] getMadeAssurances() {
+        if (madeAssurances == null) {
+            try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT * FROM notary WHERE `from`=? AND deleted is NULL")) {
+                query.setInt(1, getId());
+
+                try (GigiResultSet res = query.executeQuery()) {
+                    List<Assurance> assurances = new LinkedList<Assurance>();
+
+                    while (res.next()) {
+                        assurances.add(assuranceByRes(res));
+                    }
+
+                    this.madeAssurances = assurances.toArray(new Assurance[0]);
+                }
+            }
+        }
+
+        return madeAssurances;
+    }
+
+    public synchronized void invalidateMadeAssurances() {
+        madeAssurances = null;
+    }
+
+    public synchronized void invalidateReceivedAssurances() {
+        receivedAssurances = null;
+    }
+
+    public void updateUserData() throws GigiApiException {
+        synchronized (Notary.class) {
+            if (getReceivedAssurances().length != 0) {
+                throw new GigiApiException("No change after assurance allowed.");
+            }
+            rawUpdateUserData();
+        }
+    }
+
+    protected void rawUpdateUserData() {
+        try (GigiPreparedStatement update = new GigiPreparedStatement("UPDATE users SET fname=?, lname=?, mname=?, suffix=?, dob=? WHERE id=?")) {
+            update.setString(1, name.getFname());
+            update.setString(2, name.getLname());
+            update.setString(3, name.getMname());
+            update.setString(4, name.getSuffix());
+            update.setDate(5, getDoB().toSQLDate());
+            update.setInt(6, getId());
+            update.executeUpdate();
+        }
+    }
+
+    public Locale getPreferredLocale() {
+        return locale;
+    }
+
+    public void setPreferredLocale(Locale locale) {
+        this.locale = locale;
+
+    }
+
+    public boolean wantsDirectoryListing() {
+        try (GigiPreparedStatement get = new GigiPreparedStatement("SELECT listme FROM users WHERE id=?")) {
+            get.setInt(1, getId());
+            GigiResultSet exec = get.executeQuery();
+            return exec.next() && exec.getBoolean("listme");
+        }
+    }
+
+    public String getContactInformation() {
+        try (GigiPreparedStatement get = new GigiPreparedStatement("SELECT contactinfo FROM users WHERE id=?")) {
+            get.setInt(1, getId());
+
+            GigiResultSet exec = get.executeQuery();
+            exec.next();
+            return exec.getString("contactinfo");
+        }
+    }
+
+    public void setDirectoryListing(boolean on) {
+        try (GigiPreparedStatement update = new GigiPreparedStatement("UPDATE users SET listme = ? WHERE id = ?")) {
+            update.setBoolean(1, on);
+            update.setInt(2, getId());
+            update.executeUpdate();
+        }
+    }
+
+    public void setContactInformation(String contactInfo) {
+        try (GigiPreparedStatement update = new GigiPreparedStatement("UPDATE users SET contactinfo = ? WHERE id = ?")) {
+            update.setString(1, contactInfo);
+            update.setInt(2, getId());
+            update.executeUpdate();
+        }
+    }
+
+    public boolean isInGroup(Group g) {
+        return groups.contains(g);
+    }
+
+    public Set<Group> getGroups() {
+        return Collections.unmodifiableSet(groups);
+    }
+
+    public void grantGroup(User granter, Group toGrant) {
+        groups.add(toGrant);
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `user_groups` SET `user`=?, `permission`=?::`userGroup`, `grantedby`=?")) {
+            ps.setInt(1, getId());
+            ps.setString(2, toGrant.getDatabaseName());
+            ps.setInt(3, granter.getId());
+            ps.execute();
+        }
+    }
+
+    public void revokeGroup(User revoker, Group toRevoke) {
+        groups.remove(toRevoke);
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `user_groups` SET `deleted`=CURRENT_TIMESTAMP, `revokedby`=? WHERE `deleted` IS NULL AND `permission`=?::`userGroup` AND `user`=?")) {
+            ps.setInt(1, revoker.getId());
+            ps.setString(2, toRevoke.getDatabaseName());
+            ps.setInt(3, getId());
+            ps.execute();
+        }
+    }
+
+    public List<Organisation> getOrganisations() {
+        return getOrganisations(false);
+    }
+
+    public List<Organisation> getOrganisations(boolean isAdmin) {
+        List<Organisation> orgas = new ArrayList<>();
+        try (GigiPreparedStatement query = new GigiPreparedStatement("SELECT `orgid` FROM `org_admin` WHERE `memid`=? AND `deleted` IS NULL" + (isAdmin ? " AND master='y'" : ""))) {
+            query.setInt(1, getId());
+            try (GigiResultSet res = query.executeQuery()) {
+                while (res.next()) {
+                    orgas.add(Organisation.getById(res.getInt(1)));
+                }
+
+                return orgas;
+            }
+        }
+    }
+
+    public static synchronized User getById(int id) {
+        CertificateOwner co = CertificateOwner.getById(id);
+        if (co instanceof User) {
+            return (User) co;
+        }
+
+        return null;
+    }
+
+    public static User getByEmail(String mail) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `users`.`id` FROM `users` INNER JOIN `certOwners` ON `certOwners`.`id` = `users`.`id` WHERE `email`=? AND `deleted` IS NULL")) {
+            ps.setString(1, mail);
+            GigiResultSet rs = ps.executeQuery();
+            if ( !rs.next()) {
+                return null;
+            }
+
+            return User.getById(rs.getInt(1));
+        }
+    }
+
+    public static User[] findByEmail(String mail) {
+        LinkedList<User> results = new LinkedList<User>();
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `users`.`id` FROM `users` INNER JOIN `certOwners` ON `certOwners`.`id` = `users`.`id` WHERE `users`.`email` LIKE ? AND `deleted` IS NULL GROUP BY `users`.`id` LIMIT 100")) {
+            ps.setString(1, mail);
+            GigiResultSet rs = ps.executeQuery();
+            while (rs.next()) {
+                results.add(User.getById(rs.getInt(1)));
+            }
+            return results.toArray(new User[results.size()]);
+        }
+    }
+
+    public EmailAddress[] getEmails() {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id` FROM `emails` WHERE `memid`=? AND `deleted` IS NULL")) {
+            ps.setInt(1, getId());
+
+            GigiResultSet rs = ps.executeQuery();
+            LinkedList<EmailAddress> data = new LinkedList<EmailAddress>();
+
+            while (rs.next()) {
+                data.add(EmailAddress.getById(rs.getInt(1)));
+            }
+
+            return data.toArray(new EmailAddress[0]);
+        }
+    }
+
+    @Override
+    public boolean isValidEmail(String email) {
+        for (EmailAddress em : getEmails()) {
+            if (em.getAddress().equals(email)) {
+                return em.isVerified();
+            }
+        }
+
+        return false;
+    }
+
+    public String[] getTrainings() {
+        try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `pass_date`, `type_text` FROM `cats_passed` LEFT JOIN `cats_type` ON `cats_type`.`id`=`cats_passed`.`variant_id`  WHERE `user_id`=? ORDER BY `pass_date` ASC")) {
+            prep.setInt(1, getId());
+            GigiResultSet res = prep.executeQuery();
+            List<String> entries = new LinkedList<String>();
+
+            while (res.next()) {
+
+                entries.add(DateSelector.getDateFormat().format(res.getTimestamp(1)) + " (" + res.getString(2) + ")");
+            }
+
+            return entries.toArray(new String[0]);
+        }
+
+    }
+
+    public int generatePasswordResetTicket(User actor, String token, String privateToken) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `passwordResetTickets` SET `memid`=?, `creator`=?, `token`=?, `private_token`=?")) {
+            ps.setInt(1, getId());
+            ps.setInt(2, getId());
+            ps.setString(3, token);
+            ps.setString(4, PasswordHash.hash(privateToken));
+            ps.execute();
+            return ps.lastInsertId();
+        }
+    }
+
+    public static User getResetWithToken(int id, String token) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid` FROM `passwordResetTickets` WHERE `id`=? AND `token`=? AND `used` IS NULL AND `created` > CURRENT_TIMESTAMP - interval '1 hours' * ?")) {
+            ps.setInt(1, id);
+            ps.setString(2, token);
+            ps.setInt(3, PasswordResetPage.HOUR_MAX);
+            GigiResultSet res = ps.executeQuery();
+            if ( !res.next()) {
+                return null;
+            }
+            return User.getById(res.getInt(1));
+        }
+    }
+
+    public synchronized void consumePasswordResetTicket(int id, String private_token, String newPassword) throws GigiApiException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `private_token` FROM `passwordResetTickets` WHERE `id`=? AND `memid`=? AND `used` IS NULL")) {
+            ps.setInt(1, id);
+            ps.setInt(2, getId());
+            GigiResultSet rs = ps.executeQuery();
+            if ( !rs.next()) {
+                throw new GigiApiException("Token could not be found, has already been used, or is expired.");
+            }
+            if (PasswordHash.verifyHash(private_token, rs.getString(1)) == null) {
+                throw new GigiApiException("Private token does not match.");
+            }
+            setPassword(newPassword);
+        }
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `passwordResetTickets` SET  `used` = CURRENT_TIMESTAMP WHERE `id`=?")) {
+            ps.setInt(1, id);
+            ps.executeUpdate();
+        }
+    }
+
+    private Assurance assuranceByRes(GigiResultSet res) {
+        return new Assurance(res.getInt("id"), User.getById(res.getInt("from")), User.getById(res.getInt("to")), res.getString("location"), res.getString("method"), res.getInt("points"), res.getString("date"));
+    }
+}
diff --git a/src/org/cacert/gigi/dbObjects/Verifyable.java b/src/org/cacert/gigi/dbObjects/Verifyable.java
new file mode 100644 (file)
index 0000000..e1c997e
--- /dev/null
@@ -0,0 +1,9 @@
+package org.cacert.gigi.dbObjects;
+
+import org.cacert.gigi.GigiApiException;
+
+public interface Verifyable {
+
+    public void verify(String hash) throws GigiApiException;
+
+}
diff --git a/src/org/cacert/gigi/dbObjects/wrappers/DataContainer.java b/src/org/cacert/gigi/dbObjects/wrappers/DataContainer.java
new file mode 100644 (file)
index 0000000..db14af6
--- /dev/null
@@ -0,0 +1,9 @@
+package org.cacert.gigi.dbObjects.wrappers;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+public @interface DataContainer {
+
+}
diff --git a/src/org/cacert/gigi/email/CommandlineEmailProvider.java b/src/org/cacert/gigi/email/CommandlineEmailProvider.java
deleted file mode 100644 (file)
index 2f7502b..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.cacert.gigi.email;
-
-import java.io.IOException;
-import java.util.Properties;
-
-public class CommandlineEmailProvider extends EmailProvider {
-       public CommandlineEmailProvider(Properties p) {
-       }
-
-       @Override
-       public void sendmail(String to, String subject, String message,
-                       String from, String replyto, String toname, String fromname,
-                       String errorsto, boolean extra) throws IOException {
-               synchronized (System.out) {
-                       System.out.println("== MAIL ==");
-                       System.out.println("To: " + to);
-                       System.out.println("Subject: " + subject);
-                       System.out.println("From: " + from);
-                       System.out.println("Errors-To: " + errorsto);
-                       System.out.println("Extra: " + extra);
-                       System.out.println(message);
-               }
-
-       }
-       @Override
-       public String checkEmailServer(int forUid, String address)
-                       throws IOException {
-               System.out.println("checkMailBox: " + address);
-               return OK;
-       }
-
-}
index 644c46218709f6fc8d28e6551c093d1fceae12ec..b66ba3f9aa71bcc88f7b1b6247a55063d3070d05 100644 (file)
@@ -3,138 +3,191 @@ package org.cacert.gigi.email;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.net.Socket;
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.util.LinkedList;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Properties;
 import java.util.regex.Pattern;
 
-import org.cacert.gigi.database.DatabaseConnection;
+import javax.naming.NamingException;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.cacert.gigi.crypto.SMIME;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.util.DNSUtil;
 
 public abstract class EmailProvider {
-       public abstract void sendmail(String to, String subject, String message,
-                       String from, String replyto, String toname, String fromname,
-                       String errorsto, boolean extra) throws IOException;
-       private static EmailProvider instance;
-       public static EmailProvider getInstance() {
-               return instance;
-       }
-       public static void init(Properties conf) {
-               try {
-                       Class<?> c = Class.forName(conf.getProperty("emailProvider"));
-                       instance = (EmailProvider) c.getDeclaredConstructor(
-                                       Properties.class).newInstance(conf);
-               } catch (ReflectiveOperationException e) {
-                       e.printStackTrace();
-               }
-       }
-
-       public static final String OK = "OK";
-       public static final String FAIL = "FAIL";
-       private static final Pattern MAIL = Pattern
-                       .compile("^([a-zA-Z0-9])+([a-zA-Z0-9\\+\\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\\._-]+)+$");
-
-       public String checkEmailServer(int forUid, String address)
-                       throws IOException {
-               if (MAIL.matcher(address).matches()) {
-                       String[] parts = address.split("@", 2);
-                       String domain = parts[1];
-
-                       LinkedList<String> mxhosts = getMxHosts(domain);
-
-                       for (String host : mxhosts) {
-                               try (Socket s = new Socket(host, 25);
-                                               BufferedReader br = new BufferedReader(
-                                                               new InputStreamReader(s.getInputStream()));
-                                               PrintWriter pw = new PrintWriter(s.getOutputStream())) {
-                                       String line;
-                                       while ((line = br.readLine()) != null
-                                                       && line.startsWith("220-")) {
-                                       }
-                                       if (line == null || !line.startsWith("220")) {
-                                               continue;
-                                       }
-
-                                       pw.print("HELO www.cacert.org\r\n");
-                                       pw.flush();
-
-                                       while ((line = br.readLine()) != null
-                                                       && line.startsWith("220")) {
-                                       }
-
-                                       if (line == null || !line.startsWith("250")) {
-                                               continue;
-                                       }
-                                       pw.print("MAIL FROM: <returns@cacert.org>\r\n");
-                                       pw.flush();
-
-                                       line = br.readLine();
-
-                                       if (line == null || !line.startsWith("250")) {
-                                               continue;
-                                       }
-                                       pw.print("RCPT TO: <" + address + ">\r\n");
-                                       pw.flush();
-
-                                       line = br.readLine();
-                                       pw.print("QUIT\r\n");
-                                       pw.flush();
-
-                                       try {
-                                               PreparedStatement statmt = DatabaseConnection
-                                                               .getInstance()
-                                                               .prepare(
-                                                                               "insert into `pinglog` set `when`=NOW(), `email`=?, `result`=?, `uid`=?");
-                                               statmt.setString(1, address);
-                                               statmt.setString(2, line);
-                                               statmt.setInt(3, forUid);
-                                               statmt.execute();
-                                       } catch (SQLException e) {
-                                               e.printStackTrace();
-                                       }
-
-                                       if (line == null || !line.startsWith("250")) {
-                                               return line;
-                                       } else {
-                                               return OK;
-                                       }
-                               }
-
-                       }
-               }
-               try {
-                       PreparedStatement statmt = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "insert into `pinglog` set `when`=NOW(), `email`=?, `result`=?, `uid`=?");
-                       statmt.setString(1, address);
-                       statmt.setString(2,
-                                       "Failed to make a connection to the mail server");
-                       statmt.setInt(3, forUid);
-                       statmt.execute();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-               return FAIL;
-       }
-       private static LinkedList<String> getMxHosts(String domain)
-                       throws IOException {
-               LinkedList<String> mxhosts = new LinkedList<String>();
-               Process dig = Runtime.getRuntime().exec(
-                               new String[]{"dig", "+short", "MX", domain});
-               try (BufferedReader br = new BufferedReader(new InputStreamReader(
-                               dig.getInputStream()))) {
-                       String line;
-                       while ((line = br.readLine()) != null) {
-                               String[] mxparts = line.split(" ", 2);
-                               if (mxparts.length != 2) {
-                                       continue;
-                               }
-                               mxhosts.add(mxparts[1].substring(0, mxparts[1].length() - 1));
-                       }
-               }
-               return mxhosts;
-       }
+
+    public abstract void sendmail(String to, String subject, String message, String from, String replyto, String toname, String fromname, String errorsto, boolean extra) throws IOException;
+
+    private static EmailProvider instance;
+
+    private X509Certificate c;
+
+    private PrivateKey k;
+
+    protected void init(Certificate c, Key k) {
+        this.c = (X509Certificate) c;
+        this.k = (PrivateKey) k;
+    }
+
+    protected final void sendSigned(String contents, PrintWriter output) throws IOException, GeneralSecurityException {
+        if (k == null || c == null) {
+            output.println("Content-Transfer-Encoding: base64");
+            output.println();
+            output.print(contents);
+        } else {
+            SMIME.smime(contents, k, c, output);
+        }
+    }
+
+    public static EmailProvider getInstance() {
+        return instance;
+    }
+
+    protected static void setInstance(EmailProvider instance) {
+        EmailProvider.instance = instance;
+    }
+
+    public static void initSystem(Properties conf, Certificate cert, Key pk) {
+        try {
+            Class<?> c = Class.forName(conf.getProperty("emailProvider"));
+            EmailProvider ep = (EmailProvider) c.getDeclaredConstructor(Properties.class).newInstance(conf);
+            ep.init(cert, pk);
+            instance = ep;
+        } catch (ReflectiveOperationException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static final String OK = "OK";
+
+    public static final String FAIL = "FAIL";
+
+    public static final Pattern MAIL = Pattern.compile("^([a-zA-Z0-9])+([a-zA-Z0-9\\+\\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\\._-]+)+$");
+
+    public String checkEmailServer(int forUid, String address) throws IOException {
+        if (MAIL.matcher(address).matches()) {
+            String[] parts = address.split("@", 2);
+            String domain = parts[1];
+
+            String[] mxhosts;
+            try {
+                mxhosts = DNSUtil.getMXEntries(domain);
+            } catch (NamingException e1) {
+                return "MX lookup for your hostname failed.";
+            }
+            sortMX(mxhosts);
+
+            for (String host : mxhosts) {
+                host = host.split(" ", 2)[1];
+                if (host.endsWith(".")) {
+                    host = host.substring(0, host.length() - 1);
+                } else {
+                    return "Strange MX records.";
+                }
+                try (Socket s = new Socket(host, 25);
+                        BufferedReader br0 = new BufferedReader(new InputStreamReader(s.getInputStream(), "UTF-8"));//
+                        PrintWriter pw0 = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), "UTF-8"))) {
+                    BufferedReader br = br0;
+                    PrintWriter pw = pw0;
+                    String line;
+                    if ( !Sendmail.readSMTPResponse(br, 220)) {
+                        continue;
+                    }
+
+                    pw.print("EHLO www.cacert.org\r\n");
+                    pw.flush();
+                    boolean starttls = false;
+                    do {
+                        line = br.readLine();
+                        if (line == null) {
+                            break;
+                        }
+                        starttls |= line.substring(4).equals("STARTTLS");
+                    } while (line.startsWith("250-"));
+                    if (line == null || !line.startsWith("250 ")) {
+                        continue;
+                    }
+
+                    if (starttls) {
+                        pw.print("STARTTLS\r\n");
+                        pw.flush();
+                        if ( !Sendmail.readSMTPResponse(br, 220)) {
+                            continue;
+                        }
+                        Socket s1 = ((SSLSocketFactory) SSLSocketFactory.getDefault()).createSocket(s, host, 25, true);
+                        br = new BufferedReader(new InputStreamReader(s1.getInputStream(), "UTF-8"));
+                        pw = new PrintWriter(new OutputStreamWriter(s1.getOutputStream(), "UTF-8"));
+                        pw.print("EHLO www.cacert.org\r\n");
+                        pw.flush();
+                        if ( !Sendmail.readSMTPResponse(br, 250)) {
+                            continue;
+                        }
+                    }
+
+                    pw.print("MAIL FROM: <returns@cacert.org>\r\n");
+                    pw.flush();
+
+                    if ( !Sendmail.readSMTPResponse(br, 250)) {
+                        continue;
+                    }
+                    pw.print("RCPT TO: <" + address + ">\r\n");
+                    pw.flush();
+
+                    if ( !Sendmail.readSMTPResponse(br, 250)) {
+                        continue;
+                    }
+                    pw.print("QUIT\r\n");
+                    pw.flush();
+                    if ( !Sendmail.readSMTPResponse(br, 221)) {
+                        continue;
+                    }
+
+                    try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`=?, `uid`=?, `type`='fast', `status`=?::`pingState`")) {
+                        statmt.setString(1, address);
+                        statmt.setString(2, line);
+                        statmt.setInt(3, forUid);
+                        statmt.setString(4, "success");
+                        statmt.execute();
+                    }
+
+                    if (line == null || !line.startsWith("250")) {
+                        return line;
+                    } else {
+                        return OK;
+                    }
+                }
+
+            }
+        }
+        try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`=?, `uid`=?, `type`='fast', `status`=?::`pingState`")) {
+            statmt.setString(1, address);
+            statmt.setString(2, "Failed to make a connection to the mail server");
+            statmt.setInt(3, forUid);
+            statmt.setString(4, "failed");
+            statmt.execute();
+        }
+        return FAIL;
+    }
+
+    private static void sortMX(String[] mxhosts) {
+        Arrays.sort(mxhosts, new Comparator<String>() {
+
+            @Override
+            public int compare(String o1, String o2) {
+                int i1 = Integer.parseInt(o1.split(" ")[0]);
+                int i2 = Integer.parseInt(o2.split(" ")[0]);
+                return Integer.compare(i1, i2);
+            }
+        });
+    }
+
 }
diff --git a/src/org/cacert/gigi/email/MailProbe.java b/src/org/cacert/gigi/email/MailProbe.java
new file mode 100644 (file)
index 0000000..41fc99a
--- /dev/null
@@ -0,0 +1,28 @@
+package org.cacert.gigi.email;
+
+import java.io.IOException;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.util.ServerConstants;
+
+public class MailProbe {
+
+    public static void sendMailProbe(Language l, String type, int id, String hash, String address) throws IOException {
+        StringBuffer body = new StringBuffer();
+        body.append(l.getTranslation("Thanks for signing up with SomeCA.org, below is the link you need to open to verify your account. Once your account is verified you will be able to start issuing certificates till your hearts' content!"));
+        body.append("\n\nhttps://");
+        body.append(ServerConstants.getWwwHostNamePortSecure());
+        body.append("/verify?type=");
+        body.append(type);
+        body.append("&id=");
+        body.append(id);
+        body.append("&hash=");
+        body.append(hash);
+        body.append("\n\n");
+        body.append(l.getTranslation("Best regards"));
+        body.append("\n");
+        body.append(l.getTranslation("SomeCA.org Support!"));
+        EmailProvider.getInstance().sendmail(address, "[SomeCA.org] " + l.getTranslation("Mail Probe"), body.toString(), "support@cacert.org", null, null, null, null, false);
+    }
+
+}
index c61494b7eb4fe96cf9923c41e687b21ea7f69660..a65b13262d659c9ba98218f6010d8f6284751d3d 100644 (file)
@@ -3,8 +3,10 @@ package org.cacert.gigi.email;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.net.Socket;
+import java.security.GeneralSecurityException;
 import java.text.SimpleDateFormat;
 import java.util.Base64;
 import java.util.Date;
@@ -12,97 +14,103 @@ import java.util.Locale;
 import java.util.Properties;
 import java.util.regex.Pattern;
 
+import org.cacert.gigi.util.PEM;
+import org.cacert.gigi.util.ServerConstants;
+
 public class Sendmail extends EmailProvider {
-       protected Sendmail(Properties props) {
-       }
-       private static final Pattern NON_ASCII = Pattern
-                       .compile("[^a-zA-Z0-9 .-\\[\\]!_@]");
 
-       @Override
-       public void sendmail(String to, String subject, String message,
-                       String from, String replyto, String toname, String fromname,
-                       String errorsto, boolean extra) throws IOException {
+    private final String targetHost;
+
+    private final int targetPort;
+
+    protected Sendmail(Properties props) {
+        targetHost = props.getProperty("emailProvider.smtpHost", "localhost");
+        targetPort = Integer.parseInt(props.getProperty("emailProvider.smtpPort", "25"));
+    }
+
+    private static final Pattern NON_ASCII = Pattern.compile("[^a-zA-Z0-9 .-\\[\\]!_@]");
+
+    @Override
+    public void sendmail(String to, String subject, String message, String from, String replyto, String toname, String fromname, String errorsto, boolean extra) throws IOException {
+
+        String[] bits = from.split(",");
 
-               String[] bits = from.split(",");
+        try (Socket smtp = new Socket(targetHost, targetPort); PrintWriter out = new PrintWriter(new OutputStreamWriter(smtp.getOutputStream(), "UTF-8")); BufferedReader in = new BufferedReader(new InputStreamReader(smtp.getInputStream(), "UTF-8"));) {
+            readSMTPResponse(in, 220);
+            out.print("HELO www.cacert.org\r\n");
+            out.flush();
+            readSMTPResponse(in, 250);
+            out.print("MAIL FROM:<returns@cacert.org>\r\n");
+            out.flush();
+            readSMTPResponse(in, 250);
+            bits = to.split(",");
+            for (String user : bits) {
+                out.print("RCPT TO:<" + user.trim() + ">\r\n");
+                out.flush();
+                readSMTPResponse(in, 250);
+            }
+            out.print("DATA\r\n");
+            out.flush();
+            readSMTPResponse(in, 250);
+            out.print("X-Mailer: SomeCA.org Website\r\n");
+            // if (array_key_exists("REMOTE_ADDR", $_SERVER)) {
+            // out.print("X-OriginatingIP: ".$_SERVER["REMOTE_ADDR"]."\r\n");
+            // }
+            // TODO
+            SimpleDateFormat emailDate = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss ZZZZ (z)", Locale.ENGLISH);
+            out.print("Date: " + emailDate.format(new Date(System.currentTimeMillis())) + "\r\n");
+            if (errorsto != null) {
+                out.print("Sender: " + errorsto + "\r\n");
+                out.print("Errors-To: " + errorsto + "\r\n");
+            }
+            if (replyto != null) {
+                out.print("Reply-To: " + replyto + "\r\n");
+            } else {
+                out.print("Reply-To: " + from + "\r\n");
+            }
+            out.print("From: support@" + ServerConstants.getWwwHostName().replaceAll("^www.", "") + "\r\n");
+            out.print("To: " + to + "\r\n");
+            if (NON_ASCII.matcher(subject).matches()) {
 
-               Socket smtp = new Socket("dogcraft.de", 25);
-               PrintWriter out = new PrintWriter(smtp.getOutputStream());
-               BufferedReader in = new BufferedReader(new InputStreamReader(
-                               smtp.getInputStream()));
-               readResponse(in);
-               out.print("HELO www.cacert.org\r\n");
-               out.flush();
-               readResponse(in);
-               out.print("MAIL FROM:<returns@cacert.org>\r\n");
-               out.flush();
-               readResponse(in);
-               bits = to.split(",");
-               for (String user : bits) {
-                       out.print("RCPT TO:<" + user.trim() + ">\r\n");
-                       out.flush();
-                       readResponse(in);
-               }
-               out.print("DATA\r\n");
-               out.flush();
-               readResponse(in);
-               out.print("X-Mailer: CAcert.org Website\r\n");
-               // if (array_key_exists("REMOTE_ADDR", $_SERVER)) {
-               // out.print("X-OriginatingIP: ".$_SERVER["REMOTE_ADDR"]."\r\n");
-               // }
-               // TODO
-               SimpleDateFormat emailDate = new SimpleDateFormat(
-                               "E, d MMM yyyy HH:mm:ss ZZZZ (z)", Locale.ENGLISH);
-               out.print("Date: "
-                               + emailDate.format(new Date(System.currentTimeMillis()))
-                               + "\r\n");
-               out.print("Sender: " + errorsto + "\r\n");
-               out.print("Errors-To: " + errorsto + "\r\n");
-               if (replyto != null) {
-                       out.print("Reply-To: " + replyto + "\r\n");
-               } else {
-                       out.print("Reply-To: " + from + "\r\n");
-               }
-               out.print("From: " + from + "\r\n");
-               out.print("To: " + to + "\r\n");
-               if (NON_ASCII.matcher(subject).matches()) {
+                out.print("Subject: =?utf-8?B?" + Base64.getEncoder().encodeToString(subject.getBytes("UTF-8")) + "?=\r\n");
+            } else {
+                out.print("Subject: " + subject + "\r\n");
+            }
+            StringBuffer headers = new StringBuffer();
+            headers.append("Content-Type: text/plain; charset=\"utf-8\"\r\n");
+            headers.append("Content-Transfer-Encoding: base64\r\n");
+            // out.print(chunk_split(base64_encode(recode("html..utf-8",
+            // $message)))."\r\n.\r\n");
+            headers.append("\r\n");
+            headers.append(PEM.formatBase64(message.getBytes("UTF-8")));
+            headers.append("\r\n");
 
-                       out.print("Subject: =?utf-8?B?"
-                                       + Base64.getEncoder().encodeToString(subject.getBytes())
-                                       + "?=\r\n");
-               } else {
-                       out.print("Subject: " + subject + "\r\n");
-               }
-               out.print("Mime-Version: 1.0\r\n");
-               if (!extra) {
-                       out.print("Content-Type: text/plain; charset=\"utf-8\"\r\n");
-                       out.print("Content-Transfer-Encoding: 8bit\r\n");
-               } else {
-                       out.print("Content-Type: text/plain; charset=\"iso-8859-1\"\r\n");
-                       out.print("Content-Transfer-Encoding: quoted-printable\r\n");
-                       out.print("Content-Disposition: inline\r\n");
-               }
-               // out.print("Content-Transfer-Encoding: BASE64\r\n");
-               out.print("\r\n");
-               // out.print(chunk_split(base64_encode(recode("html..utf-8",
-               // $message)))."\r\n.\r\n");
-               message = message + "\r\n";
+            try {
+                sendSigned(headers.toString(), out);
+                out.print("\r\n.\r\n");
+                out.flush();
+            } catch (GeneralSecurityException e) {
+                e.printStackTrace();
+                return;
+            }
+            readSMTPResponse(in, 250);
+            out.print("QUIT\n");
+            out.flush();
+            readSMTPResponse(in, 221);
+        }
+    }
 
-               String sendM = message.replace("\r", "").replace("\n.\n", "\n")
-                               .replace("\n.\n", "\n").replace("\n", "\r\n")
-                               + ".\r\n";
-               out.print(sendM);
-               out.flush();
-               readResponse(in);
-               out.print("QUIT\n");
-               out.flush();
-               readResponse(in);
-               smtp.close();
-       }
-       private static void readResponse(BufferedReader in) throws IOException {
-               String line;
-               while ((line = in.readLine()) != null && line.matches("\\d+-")) {
-               }
+    public static boolean readSMTPResponse(BufferedReader in, int code) throws IOException {
+        String line;
+        while ((line = in.readLine()) != null) {
+            if (line.startsWith(code + " ")) {
+                return true;
+            } else if ( !line.startsWith(code + "-")) {
+                return false;
+            }
+        }
+        return false;
 
-       }
+    }
 
 }
diff --git a/src/org/cacert/gigi/email/TestEmailProvider.java b/src/org/cacert/gigi/email/TestEmailProvider.java
deleted file mode 100644 (file)
index 0734350..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-package org.cacert.gigi.email;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.util.Properties;
-
-class TestEmailProvider extends EmailProvider {
-       ServerSocket servs;
-       Socket client;
-       DataOutputStream out;
-       DataInputStream in;
-       protected TestEmailProvider(Properties props) {
-               try {
-                       servs = new ServerSocket(Integer.parseInt(props
-                                       .getProperty("emailProvider.port")), 10,
-                                       InetAddress.getByName("127.0.0.1"));
-               } catch (IOException e) {
-                       e.printStackTrace();
-               }
-       }
-       @Override
-       public synchronized void sendmail(String to, String subject,
-                       String message, String from, String replyto, String toname,
-                       String fromname, String errorsto, boolean extra) throws IOException {
-               while (true) {
-                       assureLocalConnection();
-                       try {
-                               out.writeUTF("mail");
-                               write(to);
-                               write(subject);
-                               write(message);
-                               write(from);
-                               write(replyto);
-                               out.flush();
-                               return;
-                       } catch (IOException e) {
-                               client = null;
-                       }
-               }
-       }
-       private void assureLocalConnection() throws IOException {
-               if (out != null) {
-                       try {
-                               out.writeUTF("ping");
-                       } catch (IOException e) {
-                               client = null;
-                       }
-               }
-               if (client == null || client.isClosed()) {
-                       client = servs.accept();
-                       out = new DataOutputStream(client.getOutputStream());
-                       in = new DataInputStream(client.getInputStream());
-               }
-       }
-       @Override
-       public synchronized String checkEmailServer(int forUid, String address)
-                       throws IOException {
-               while (true) {
-                       assureLocalConnection();
-                       try {
-                               out.writeUTF("challengeAddrBox");
-                               out.writeUTF(address);
-                               return in.readUTF();
-                       } catch (IOException e) {
-                               client = null;
-                       }
-               }
-       }
-
-       private void write(String to) throws IOException {
-               if (to == null) {
-                       out.writeUTF("<null>");
-               } else {
-                       out.writeUTF(to);
-               }
-       }
-
-}
diff --git a/src/org/cacert/gigi/localisation/Language.java b/src/org/cacert/gigi/localisation/Language.java
new file mode 100644 (file)
index 0000000..9597112
--- /dev/null
@@ -0,0 +1,138 @@
+package org.cacert.gigi.localisation;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Locale;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+public class Language {
+
+    public static final String SESSION_ATTRIB_NAME = "lang";
+
+    private static Locale[] supportedLocales;
+
+    static {
+        LinkedList<Locale> supported = new LinkedList<>();
+        File locales = new File("locale");
+        File[] listFiles = locales.listFiles();
+        if (listFiles != null) {
+            for (File f : listFiles) {
+                if ( !f.getName().endsWith(".xml")) {
+                    continue;
+                }
+                String language = f.getName().split("\\.", 2)[0];
+                supported.add(getLocaleFromString(language));
+            }
+        }
+        Collections.sort(supported, new Comparator<Locale>() {
+
+            @Override
+            public int compare(Locale o1, Locale o2) {
+                return o1.toString().compareTo(o2.toString());
+            }
+
+        });
+        supportedLocales = supported.toArray(new Locale[supported.size()]);
+    }
+
+    public static Locale getLocaleFromString(String language) {
+        if (language.contains("_")) {
+            String[] parts = language.split("_", 2);
+            return new Locale(parts[0], parts[1]);
+
+        } else {
+            return new Locale(language);
+        }
+    }
+
+    public static Locale[] getSupportedLocales() {
+        return supportedLocales;
+    }
+
+    private static HashMap<String, Language> langs = new HashMap<String, Language>();
+
+    private HashMap<String, String> translations = new HashMap<String, String>();
+
+    private Locale locale;
+
+    private static Locale project(Locale locale) {
+        if (locale == null) {
+            return Locale.getDefault();
+        }
+        File file = new File("locale", locale.toString() + ".xml");
+        if ( !file.exists()) {
+            return new Locale(locale.getLanguage());
+        }
+        return locale;
+    }
+
+    protected Language(Locale locale) throws ParserConfigurationException, IOException, SAXException {
+        File file = new File("locale", locale.toString() + ".xml");
+        this.locale = locale;
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        DocumentBuilder db = dbf.newDocumentBuilder();
+        Document d = db.parse(new FileInputStream(file));
+        NodeList nl = d.getDocumentElement().getChildNodes();
+        for (int i = 0; i < nl.getLength(); i++) {
+            if ( !(nl.item(i) instanceof Element)) {
+                continue;
+            }
+            Element e = (Element) nl.item(i);
+            Element id = (Element) e.getElementsByTagName("id").item(0);
+            Element msg = (Element) e.getElementsByTagName("msg").item(0);
+            translations.put(id.getTextContent(), msg.getTextContent());
+        }
+        System.out.println(translations.size() + " strings loaded.");
+    }
+
+    public String getTranslation(String text) {
+        String string = translations.get(text);
+        if (string == null || string.equals("")) {
+            return text;
+        }
+        return string;
+    }
+
+    public static Language getInstance(Locale locale) {
+        locale = project(locale);
+        File file = new File("locale", locale.toString() + ".xml");
+        if ( !file.exists()) {
+            return null;
+        }
+        synchronized (Language.class) {
+            Language lang = langs.get(locale.toString());
+            if (lang != null) {
+                return lang;
+            }
+            try {
+                lang = new Language(locale);
+                langs.put(locale.toString(), lang);
+            } catch (ParserConfigurationException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            } catch (SAXException e) {
+                e.printStackTrace();
+            }
+            return lang;
+        }
+    }
+
+    public Locale getLocale() {
+        return locale;
+    }
+
+}
index e6b0f7cb95901d57dd3a88b18cfbf42b5d65285d..a4a5d8595eaf71f64ea6a31eed4ee5efb2fe929c 100644 (file)
@@ -6,31 +6,32 @@ import java.io.File;
  * Native to use privileged ports on unix-like hosts.
  * 
  * @author janis
- * 
  */
 public class SetUID {
-       static {
-               System.load(new File("natives/libsetuid.so").getAbsolutePath());
-       }
 
-       public native Status setUid(int uid, int gid);
+    static {
+        System.load(new File("natives/libsetuid.so").getAbsolutePath());
+    }
+
+    public native Status setUid(int uid, int gid);
+
+    public static class Status {
 
-       public static class Status {
+        private boolean success;
 
-               private boolean success;
-               private String message;
+        private String message;
 
-               public Status(boolean success, String message) {
-                       this.success = success;
-                       this.message = message;
-               }
+        public Status(boolean success, String message) {
+            this.success = success;
+            this.message = message;
+        }
 
-               public boolean getSuccess() {
-                       return success;
-               }
+        public boolean getSuccess() {
+            return success;
+        }
 
-               public String getMessage() {
-                       return message;
-               }
-       }
+        public String getMessage() {
+            return message;
+        }
+    }
 }
diff --git a/src/org/cacert/gigi/output/AssurancesDisplay.java b/src/org/cacert/gigi/output/AssurancesDisplay.java
new file mode 100644 (file)
index 0000000..639197c
--- /dev/null
@@ -0,0 +1,67 @@
+package org.cacert.gigi.output;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import org.cacert.gigi.dbObjects.Assurance;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.Template;
+
+public class AssurancesDisplay implements Outputable {
+
+    private static Template template;
+
+    private boolean assurer;
+
+    public String assuranceArray;
+
+    static {
+        template = new Template(AssurancesDisplay.class.getResource("AssurancesDisplay.templ"));
+    }
+
+    public AssurancesDisplay(String assuranceArray, boolean assurer) {
+        this.assuranceArray = assuranceArray;
+        this.assurer = assurer;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        final Assurance[] assurances = (Assurance[]) vars.get(assuranceArray);
+        if (assurer) {
+            vars.put("verb", l.getTranslation("To"));
+        } else {
+            vars.put("verb", l.getTranslation("From"));
+        }
+
+        IterableDataset assuranceGroup = new IterableDataset() {
+
+            private int i = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (i >= assurances.length) {
+                    return false;
+                } else {
+                    Assurance assurance = assurances[i];
+                    vars.put("id", assurance.getId());
+                    vars.put("method", assurance.getMethod());
+                    if (assurer) {
+                        vars.put("verbVal", assurance.getTo().getName());
+                    } else {
+                        vars.put("verbVal", assurance.getFrom().getName());
+                    }
+                    vars.put("date", assurance.getDate());
+                    vars.put("location", assurance.getLocation());
+                    vars.put("points", assurance.getPoints());
+                    i++;
+                    return true;
+                }
+            }
+        };
+        vars.put("assurances", assuranceGroup);
+        template.output(out, l, vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/AssurancesDisplay.templ b/src/org/cacert/gigi/output/AssurancesDisplay.templ
new file mode 100644 (file)
index 0000000..4a91eef
--- /dev/null
@@ -0,0 +1,23 @@
+<table class="table">
+<tr>
+<th colspan="7"><?=_Assurances?></th>
+</tr>
+<tr>
+<td><?=_Id?></td>
+<td><?=_Date?></td>
+<td><?=$verb?></td>
+<td><?=_Points?></td>
+<td><?=_Location?></td>
+<td><?=_Method?></td>
+</tr>
+<? foreach($assurances) {?>
+<tr>
+<td><?=$id?></td>
+<td><?=$date?></td>
+<td><?=$verbVal?></td>
+<td><?=$points?></td>
+<td><?=$location?></td>
+<td><?=$method?></td>
+</tr>
+<? } ?>
+</table>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/output/CertificateIterable.java b/src/org/cacert/gigi/output/CertificateIterable.java
new file mode 100644 (file)
index 0000000..f24d778
--- /dev/null
@@ -0,0 +1,57 @@
+package org.cacert.gigi.output;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.cert.X509Certificate;
+import java.util.Map;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.IterableDataset;
+
+public class CertificateIterable implements IterableDataset {
+
+    private Certificate[] certificates;
+
+    public CertificateIterable(Certificate[] certificates) {
+        this.certificates = certificates;
+    }
+
+    private int i = 0;
+
+    @Override
+    public boolean next(Language l, Map<String, Object> vars) {
+        if (i >= certificates.length) {
+            return false;
+        }
+        Certificate c = certificates[i++];
+        vars.put("state", c.getStatus());
+        vars.put("CN", c.getDistinguishedName());
+        vars.put("serial", c.getSerial());
+        vars.put("digest", c.getMessageDigest());
+        vars.put("profile", c.getProfile().getVisibleName());
+        try {
+            CertificateStatus st = c.getStatus();
+            vars.put("revokable", st != CertificateStatus.REVOKED && st == CertificateStatus.ISSUED);
+            if (st == CertificateStatus.ISSUED || st == CertificateStatus.REVOKED) {
+                X509Certificate cert = c.cert();
+                vars.put("issued", cert.getNotBefore());
+                vars.put("expire", cert.getNotAfter());
+            } else {
+                vars.put("issued", l.getTranslation("N/A"));
+                vars.put("expire", l.getTranslation("N/A"));
+            }
+            if (st == CertificateStatus.REVOKED) {
+                vars.put("revoked", c.getRevocationDate());
+            } else {
+                vars.put("revoked", "n/a");
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (GeneralSecurityException e) {
+            e.printStackTrace();
+        }
+        return true;
+    }
+}
diff --git a/src/org/cacert/gigi/output/CertificateTable.java b/src/org/cacert/gigi/output/CertificateTable.java
deleted file mode 100644 (file)
index 836f344..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.cacert.gigi.output;
-
-import java.io.PrintWriter;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.LinkedList;
-import java.util.Map;
-
-import org.cacert.gigi.Language;
-import org.cacert.gigi.output.DataTable.Cell;
-
-public class CertificateTable implements Outputable {
-       String resultSet;
-       public CertificateTable(String resultSet) {
-               this.resultSet = resultSet;
-       }
-
-       @Override
-       public void output(PrintWriter out, Language l, Map<String, Object> vars) {
-               ResultSet rs = (ResultSet) vars.get(resultSet);
-               try {
-                       out.println("<form method=\"post\" action=\"account.php\">");
-                       final LinkedList<Cell> cells = new LinkedList<>();
-                       cells.add(new Cell("Renew/Revoke/Delete", true));
-                       cells.add(new Cell("Status", true));
-                       cells.add(new Cell("Email Address", true));
-                       cells.add(new Cell("SerialNumber", true));
-                       cells.add(new Cell("Revoked", true));
-                       cells.add(new Cell("Expires", true));
-                       cells.add(new Cell("Login", true));
-                       cells.add(new Cell("Comment *", true, 2));
-                       rs.beforeFirst();
-                       while (rs.next()) {
-                               // out.println(rs.getString("id"));
-                               cells.add(new Cell());
-                               cells.add(new Cell("State", false));
-                               cells.add(new Cell(rs.getString("CN"), false));
-                               cells.add(new Cell(rs.getString("serial"), false));
-                               if (rs.getString("revoked") == null) {
-                                       cells.add(new Cell("N/A", false));
-                               } else {
-                                       cells.add(new Cell(rs.getString("revoked"), false));
-                               }
-                               cells.add(new Cell(rs.getString("expire"), false));
-                               cells.add(new Cell(rs.getString("a"), false));
-                               cells.add(new Cell(rs.getString("a"), false));
-                       }
-                       DataTable t = new DataTable(9, cells);
-                       t.output(out, l, vars);
-                       out.println("</form>");
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-
-       }
-}
diff --git a/src/org/cacert/gigi/output/CertificateTable.templ b/src/org/cacert/gigi/output/CertificateTable.templ
new file mode 100644 (file)
index 0000000..5a732ea
--- /dev/null
@@ -0,0 +1,32 @@
+<table class="table">
+<thead><tr>
+<th><?=_Renew/Revoke/Delete?></th>
+<th><?=_Status?></th>
+<th><?=_Email Address?></th>
+<th><?=_SerialNumber?></th>
+<th><?=_Digest?></th>
+<th><?=_Profile?></th>
+<th><?=_Issued?></th>
+<th><?=_Revoked?></th>
+<th><?=_Expires?></th>
+<th><?=_Login?></th>
+</tr></thead>
+<tbody>
+<? foreach($certs) {?>
+<tr>
+       <td>
+       <? if($revokable) { ?><input type='checkbox' name='certs[]' value='<?=$serial?>'><? } ?>
+       </td>
+       <td><?=$state?></td>
+       <td><?=$CN?></td>
+       <td><a href='/account/certs/<?=$serial?>'><?=$serial?></a></td>
+       <td><?=$digest?></td>
+       <td><?=$profile?></td>
+       <td><?=$issued?></td>
+       <td><?=$revoked?></td>
+       <td><?=$expire?></td>
+       <td>a</td>
+</tr>
+<? } ?>
+</tbody>
+</table>
diff --git a/src/org/cacert/gigi/output/CertificateValiditySelector.java b/src/org/cacert/gigi/output/CertificateValiditySelector.java
new file mode 100644 (file)
index 0000000..b8601de
--- /dev/null
@@ -0,0 +1,136 @@
+package org.cacert.gigi.output;
+
+import java.io.PrintWriter;
+import java.sql.Date;
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.util.HTMLEncoder;
+
+public class CertificateValiditySelector implements Outputable {
+
+    private static final long DAY = 1000 * 60 * 60 * 24;
+
+    private Date from;
+
+    private String val = "2y";
+
+    public CertificateValiditySelector() {
+
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.print("<select name='validFrom'><option value='now'");
+        if (from == null) {
+            out.print(" selected='selected'");
+        }
+        out.print(">");
+        out.print(l.getTranslation("now"));
+        out.print("</option>");
+        long base = getCurrentDayBase();
+        for (int i = 0; i < 14; i++) {
+            long date = base + DAY * i;
+            String d = DateSelector.getDateFormat().format(new Date(date));
+            out.print("<option value='");
+            out.print(d);
+            out.print("'");
+            if (from != null && from.getTime() == date) {
+                out.print(" selected='selected'");
+            }
+            out.print(">");
+            out.print(d);
+            out.println("</option>");
+        }
+        out.println("</select>");
+
+        out.print("<input type='text' name='validity' value='");
+        out.print(HTMLEncoder.encodeHTML(val));
+        out.println("'>");
+
+        if (from == null) {
+            return;
+        }
+
+    }
+
+    private long getCurrentDayBase() {
+        long base = System.currentTimeMillis();
+        base -= base % DAY;
+        base += DAY;
+        return base;
+    }
+
+    public void update(HttpServletRequest r) throws GigiApiException {
+        String from = r.getParameter("validFrom");
+
+        GigiApiException gae = new GigiApiException();
+        try {
+            saveStartDate(from);
+        } catch (GigiApiException e) {
+            gae.mergeInto(e);
+        }
+        try {
+            String validity = r.getParameter("validity");
+            if (validity != null) {
+                checkValidityLength(validity);
+                val = validity;
+            }
+        } catch (GigiApiException e) {
+            gae.mergeInto(e);
+        }
+        if ( !gae.isEmpty()) {
+            throw gae;
+        }
+
+    }
+
+    public static void checkValidityLength(String newval) throws GigiApiException {
+        if (newval.endsWith("y") || newval.endsWith("m")) {
+            if (newval.length() > 10) { // for database
+                throw new GigiApiException("The validity interval entered is invalid.");
+            }
+            String num = newval.substring(0, newval.length() - 1);
+            try {
+                int len = Integer.parseInt(num);
+                if (len <= 0) {
+                    throw new GigiApiException("The validity interval entered is invalid.");
+                }
+            } catch (NumberFormatException e) {
+                throw new GigiApiException("The validity interval entered is invalid.");
+            }
+        } else {
+            try {
+                DateSelector.getDateFormat().parse(newval);
+            } catch (ParseException e) {
+                throw new GigiApiException("The validity interval entered is invalid.");
+            }
+        }
+    }
+
+    private void saveStartDate(String from) throws GigiApiException {
+        if (from == null || "now".equals(from)) {
+            this.from = null;
+        } else {
+            try {
+                this.from = new Date(DateSelector.getDateFormat().parse(from).getTime());
+            } catch (ParseException e) {
+                throw new GigiApiException("The validity start date entered is invalid.");
+            }
+        }
+    }
+
+    public Date getFrom() {
+        return from;
+    }
+
+    public String getTo() {
+        return val;
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/ClientCSRGenerate.java b/src/org/cacert/gigi/output/ClientCSRGenerate.java
new file mode 100644 (file)
index 0000000..440d2b0
--- /dev/null
@@ -0,0 +1,33 @@
+package org.cacert.gigi.output;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.ServerConstants;
+
+public class ClientCSRGenerate {
+
+    private static Template normal;
+
+    static {
+        normal = new Template(ClientCSRGenerate.class.getResource("ClientCSRGenerate.templ"));
+    }
+
+    public static void output(HttpServletRequest req, HttpServletResponse resp) {
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        vars.put("minsize", "2048");
+        vars.put("normalhost", "https://" + ServerConstants.getWwwHostNamePortSecure());
+        vars.put("securehost", "https://" + ServerConstants.getSecureHostNamePort());
+        vars.put("statichost", "https://" + ServerConstants.getStaticHostNamePortSecure());
+        try {
+            normal.output(resp.getWriter(), Page.getLanguage(req), vars);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/output/ClientCSRGenerate.templ b/src/org/cacert/gigi/output/ClientCSRGenerate.templ
new file mode 100644 (file)
index 0000000..a35e3f8
--- /dev/null
@@ -0,0 +1,9 @@
+
+       <p>
+               <form method="post" action="account.php">
+                       <input type="hidden" name="keytype" value="NS">
+                       <?=_Keysize:?> <keygen name="SPKAC" challenge="<?=$spkac_hash?>">
+
+                       <input type="submit" name="submit" value="<?=_Create Certificate Request?>">
+               </form>
+       </p>
diff --git a/src/org/cacert/gigi/output/DataTable.java b/src/org/cacert/gigi/output/DataTable.java
deleted file mode 100644 (file)
index 0a30180..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-package org.cacert.gigi.output;
-
-import java.io.PrintWriter;
-import java.util.LinkedList;
-import java.util.Map;
-
-import org.cacert.gigi.Language;
-
-public class DataTable implements Outputable {
-       private LinkedList<Cell> cells;
-       private int columnCount;
-
-       public DataTable(int coloumnCount, LinkedList<Cell> content) {
-               this.columnCount = coloumnCount;
-               this.cells = content;
-       }
-
-       public void output(PrintWriter out, Language l) {
-               float mesCells = cells.size();
-               for (Cell c : cells) {
-                       if (c.getColSpan() > 1) {
-                               mesCells += c.getColSpan();
-                       }
-               }
-               out.println("<table align=\"center\" valign=\"middle\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"wrapper\">");
-               int cellsRendered = 0;
-               for (int i = 0; i < Math.ceil(mesCells / columnCount) - 1; i++) {
-                       out.println("<tr>");
-                       for (int j = 0; j < columnCount;) {
-                               Cell current = cells.get(cellsRendered++);
-                               j += current.getColSpan();
-                               out.println("<td ");
-                               out.print(current.getHtmlAttribs());
-                               out.print(" >");
-                               out.print(current.shouldTranslate() ? l.getTranslation(current
-                                               .getText()) : current.getText());
-                               out.print("</td>");
-                       }
-                       out.println("</tr>");
-               }
-               out.println("</table>");
-       }
-
-       public static class Cell {
-               private String text, htmlAttribs;
-               private boolean translate;
-               private int colSpan;
-
-               public Cell() {
-                       this("&nbsp;", false);
-               }
-
-               public Cell(String text, boolean translate, int colSpan,
-                               String htmlAttribs) {
-                       this.text = text;
-                       this.translate = translate;
-                       this.htmlAttribs = htmlAttribs;
-                       if (colSpan > 1) {
-                               this.htmlAttribs += " colspan=\"" + colSpan + "\"";
-                       }
-                       this.colSpan = colSpan;
-               }
-
-               public Cell(String text, boolean translate) {
-                       this(text, translate, 1, "class=\"DataTD\"");
-               }
-
-               public Cell(String text, boolean translate, int colSpan) {
-                       this(text, translate, colSpan, "class=\"DataTD\"");
-               }
-
-               public boolean shouldTranslate() {
-                       return translate;
-               }
-
-               public String getText() {
-                       return text;
-               }
-
-               public int getColSpan() {
-                       return colSpan;
-               }
-
-               public String getHtmlAttribs() {
-                       return htmlAttribs;
-               }
-
-       }
-
-       @Override
-       public void output(PrintWriter out, Language l, Map<String, Object> vars) {
-               output(out, l);
-       }
-
-}
index a21e38bd9b0f9731aa1cfedbefd3c9ca0060ba99..c4af9b03886a34dbc9d1e6dfc5ec34ab61896a13 100644 (file)
@@ -4,99 +4,146 @@ import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.Map;
+import java.util.TimeZone;
 
 import javax.servlet.http.HttpServletRequest;
 
-import org.cacert.gigi.Language;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.util.DayDate;
+import org.cacert.gigi.util.CalendarUtil;
+import org.cacert.gigi.util.HTMLEncoder;
 
 public class DateSelector implements Outputable {
-       String[] names;
-       public DateSelector(String day, String month, String year) {
-               this.names = new String[]{day, month, year};
-       }
-       int day;
-       int month;
-       int year;
-       @Override
-       public void output(PrintWriter out, Language l, Map<String, Object> vars) {
-               out.print("<nobr><select name=\"");
-               out.print(names[0]);
-               out.println("\">");
-               for (int i = 1; i <= 31; i++) {
-                       out.print("<option");
-                       if (i == day) {
-                               out.print(" selected=\"selected\"");
-                       }
-                       out.println(">" + i + "</option>");
-               }
-               out.println("</select>");
-               SimpleDateFormat sdf = new SimpleDateFormat("MMMM", l.getLocale());
-               out.print("<select name=\"");
-               out.print(names[1]);
-               out.println("\">");
-               Calendar c = sdf.getCalendar();
-               for (int i = 1; i <= 12; i++) {
-                       c.set(Calendar.MONTH, i - 1);
-                       out.print("<option value='" + i + "'");
-                       if (i == month) {
-                               out.print(" selected=\"selected\"");
-                       }
-                       out.println(">" + sdf.format(c.getTime()) + " (" + i + ")</option>");
-               }
-               out.println("</select>");
-               out.print("<input type=\"text\" name=\"");
-               out.print(names[2]);
-               out.print("\" value=\"");
-               if (year != 0) {
-                       out.print(year);
-               }
-               out.print("\" size=\"4\" autocomplete=\"off\"></nobr>");
-       }
-
-       public void update(HttpServletRequest r) {
-               String dayS = r.getParameter(names[0]);
-               if (dayS != null) {
-                       day = parseIntSafe(dayS);
-               }
-
-               String monthS = r.getParameter(names[1]);
-               if (monthS != null) {
-                       month = parseIntSafe(monthS);
-               }
-
-               String yearS = r.getParameter(names[2]);
-               if (yearS != null) {
-                       year = parseIntSafe(yearS);
-               }
-       }
-       private int parseIntSafe(String dayS) {
-               try {
-                       return Integer.parseInt(dayS);
-               } catch (NumberFormatException e) {
-
-               }
-               return 0;
-       }
-       public boolean isValid() {
-               if (!(1900 < year && 1 <= month && month <= 12 && 1 <= day && day <= 32)) {
-                       return false;
-               }
-               return true; // TODO checkdate
-       }
-
-       @Override
-       public String toString() {
-               return "DateSelector [names=" + Arrays.toString(names) + ", day=" + day
-                               + ", month=" + month + ", year=" + year + "]";
-       }
-
-       public Date getDate() {
-               Calendar gc = GregorianCalendar.getInstance();
-               gc.set(year, month - 1, day);
-               return gc.getTime();
-       }
+
+    private String[] names;
+
+    public DateSelector(String day, String month, String year, DayDate date) {
+        this(day, month, year);
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTF"));
+        cal.setTimeInMillis(date.getTime());
+        this.day = cal.get(Calendar.DAY_OF_MONTH);
+        this.month = cal.get(Calendar.MONTH) + 1;
+        this.year = cal.get(Calendar.YEAR);
+    }
+
+    public DateSelector(String day, String month, String year) {
+        this.names = new String[] {
+                HTMLEncoder.encodeHTML(day), HTMLEncoder.encodeHTML(month), HTMLEncoder.encodeHTML(year)
+        };
+    }
+
+    private int day;
+
+    private int month;
+
+    private int year;
+
+    private static ThreadLocal<SimpleDateFormat> fmt = new ThreadLocal<>();
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.print("<nobr>");
+        outputYear(out);
+        outputMonth(out, l);
+        outputDay(out);
+        out.print("</nobr>");
+    }
+
+    private void outputDay(PrintWriter out) {
+        out.print("<select name=\"");
+        out.print(names[0]);
+        out.println("\">");
+        for (int i = 1; i <= 31; i++) {
+            out.print("<option");
+            if (i == day) {
+                out.print(" selected=\"selected\"");
+            }
+            out.println(">" + i + "</option>");
+        }
+        out.println("</select>");
+    }
+
+    private void outputMonth(PrintWriter out, Language l) {
+        SimpleDateFormat sdf = new SimpleDateFormat("MMMM", l.getLocale());
+        out.print("<select name=\"");
+        out.print(names[1]);
+        out.println("\">");
+        Calendar c = sdf.getCalendar();
+        for (int i = 1; i <= 12; i++) {
+            c.set(Calendar.MONTH, i - 1);
+            out.print("<option value='" + i + "'");
+            if (i == month) {
+                out.print(" selected=\"selected\"");
+            }
+            out.println(">" + sdf.format(c.getTime()) + " (" + i + ")</option>");
+        }
+        out.println("</select>");
+    }
+
+    private void outputYear(PrintWriter out) {
+        out.print("<input type=\"text\" name=\"");
+        out.print(names[2]);
+        out.print("\" value=\"");
+        if (year != 0) {
+            out.print(year);
+        }
+        out.print("\" size=\"4\" autocomplete=\"off\">");
+    }
+
+    public void update(HttpServletRequest r) throws GigiApiException {
+        try {
+            String dayS = r.getParameter(names[0]);
+            if (dayS != null) {
+                day = Integer.parseInt(dayS);
+            }
+
+            String monthS = r.getParameter(names[1]);
+            if (monthS != null) {
+                month = Integer.parseInt(monthS);
+            }
+
+            String yearS = r.getParameter(names[2]);
+            if (yearS != null) {
+                year = Integer.parseInt(yearS);
+            }
+        } catch (NumberFormatException e) {
+            throw new GigiApiException("Unparsable date.");
+        }
+    }
+
+    public boolean isValid() {
+        if ( !(1900 < year && 1 <= month && month <= 12 && 1 <= day && day <= 32)) {
+            return false;
+        }
+
+        if ( !CalendarUtil.isDateValid(year, month, day)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "DateSelector [names=" + Arrays.toString(names) + ", day=" + day + ", month=" + month + ", year=" + year + "]";
+    }
+
+    public DayDate getDate() {
+        return CalendarUtil.getDateFromComponents(year, month, day);
+    }
+
+    public static SimpleDateFormat getDateFormat() {
+        SimpleDateFormat local = fmt.get();
+        if (local == null) {
+            local = new SimpleDateFormat("yyyy-MM-dd");
+            local.setLenient(false);
+            local.setTimeZone(TimeZone.getTimeZone("UTC"));
+            fmt.set(local);
+        }
+        return local;
+    }
 
 }
diff --git a/src/org/cacert/gigi/output/Form.java b/src/org/cacert/gigi/output/Form.java
deleted file mode 100644 (file)
index 9a27127..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.cacert.gigi.output;
-
-import java.io.PrintWriter;
-
-import javax.servlet.ServletRequest;
-import javax.servlet.http.HttpServletRequest;
-
-import org.cacert.gigi.pages.Page;
-
-public abstract class Form implements Outputable {
-       public abstract boolean submit(PrintWriter out, HttpServletRequest req);
-
-       protected void outputError(PrintWriter out, ServletRequest req, String text) {
-               out.print("<div>");
-               out.print(Page.translate(req, text));
-               out.println("</div>");
-       }
-
-}
diff --git a/src/org/cacert/gigi/output/GroupSelector.java b/src/org/cacert/gigi/output/GroupSelector.java
new file mode 100644 (file)
index 0000000..a26be9b
--- /dev/null
@@ -0,0 +1,50 @@
+package org.cacert.gigi.output;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.util.HTMLEncoder;
+
+public class GroupSelector implements Outputable {
+
+    String name;
+
+    Group value = null;
+
+    public GroupSelector(String name) {
+        this.name = HTMLEncoder.encodeHTML(name);
+    }
+
+    public void update(HttpServletRequest r) throws GigiApiException {
+        String vS = r.getParameter(name);
+        value = null;
+        for (Group g : Group.values()) {
+            if (g.getDatabaseName().equals(vS)) {
+                value = g;
+            }
+        }
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.println("<select name='" + name + "'>");
+        for (Group g : Group.values()) {
+            out.print("<option name='" + g.getDatabaseName());
+            if (g.equals(value)) {
+                out.print(" selected");
+            }
+            out.println("'>" + g.getDatabaseName() + "</option>");
+        }
+        out.println("</select>");
+    }
+
+    public Group getGroup() {
+        return value;
+    }
+}
diff --git a/src/org/cacert/gigi/output/HashAlgorithms.java b/src/org/cacert/gigi/output/HashAlgorithms.java
new file mode 100644 (file)
index 0000000..fe29d62
--- /dev/null
@@ -0,0 +1,32 @@
+package org.cacert.gigi.output;
+
+import java.util.Map;
+
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.IterableDataset;
+
+public class HashAlgorithms implements IterableDataset {
+
+    private int i = 0;
+
+    private Digest selected;
+
+    public HashAlgorithms(Digest selected) {
+        this.selected = selected;
+    }
+
+    @Override
+    public boolean next(Language l, Map<String, Object> vars) {
+        Digest[] length = Digest.values();
+        if (i >= length.length) {
+            return false;
+        }
+        Digest d = length[i++];
+        vars.put("algorithm", d.toString());
+        vars.put("name", d.toString());
+        vars.put("info", d.getExp());
+        vars.put("checked", selected == d ? " checked='checked'" : "");
+        return true;
+    }
+}
diff --git a/src/org/cacert/gigi/output/IMenuItem.java b/src/org/cacert/gigi/output/IMenuItem.java
new file mode 100644 (file)
index 0000000..5e85f62
--- /dev/null
@@ -0,0 +1,12 @@
+package org.cacert.gigi.output;
+
+import org.cacert.gigi.PermissionCheckable;
+import org.cacert.gigi.output.template.Outputable;
+
+/**
+ * Markerinterface for an {@link Outputable} speicially used in a {@link Menu}.
+ * 
+ * @author janis
+ */
+public interface IMenuItem extends Outputable, PermissionCheckable {
+}
diff --git a/src/org/cacert/gigi/output/MailTable.java b/src/org/cacert/gigi/output/MailTable.java
deleted file mode 100644 (file)
index 5e310b7..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.cacert.gigi.output;
-
-import java.io.PrintWriter;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.LinkedList;
-import java.util.Map;
-
-import org.cacert.gigi.Language;
-import org.cacert.gigi.output.DataTable.Cell;
-
-public class MailTable implements Outputable {
-       private String resultSet, userMail;
-
-       public MailTable(String key, String userMail) {
-               this.resultSet = key;
-               this.userMail = userMail;
-       }
-
-       @Override
-       public void output(PrintWriter out, Language l, Map<String, Object> vars) {
-               ResultSet rs = (ResultSet) vars.get(resultSet);
-               String userMail = (String) vars.get(this.userMail);
-               LinkedList<Cell> cells = new LinkedList<>();
-               cells.add(new Cell("Email Accounts", true, 4, "class=\"title\""));
-               cells.add(new Cell("Default", true));
-               cells.add(new Cell("Status", true));
-               cells.add(new Cell("Delete", true));
-               cells.add(new Cell("Address", true));
-               try {
-                       rs.beforeFirst();
-                       while (rs.next()) {
-                               cells.add(new Cell());
-                               cells.add(new Cell(
-                                               rs.getString("hash").trim().isEmpty() ? "Verified"
-                                                               : "Unverified", true));
-                               if (rs.getString("email").equals(userMail)) {
-                                       cells.add(new Cell(
-                                                       "N/A"
-                                                                       , true));
-                               } else {
-                                       cells.add(new Cell("<input type=\"checkbox\" name=\"delid[]\" value=\""
-                                                                                       + rs.getInt("id") + "\">", false));
-                               }
-                               cells.add(new Cell(rs.getString("email"), false));
-                       }
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-               String trans = l.getTranslation("Make Default");
-               cells.add(new Cell(
-                               "<input type=\"submit\" name=\"makedefault\" value=\"" + trans
-                                               + "\">", false, 2));
-               trans = l.getTranslation("Delete");
-               cells.add(new Cell("<input type=\"submit\" name=\"process\" value=\""
-                               + trans + "\">", false, 2));
-               DataTable t = new DataTable(4, cells);
-               t.output(out, l, vars);
-       }
-
-}
diff --git a/src/org/cacert/gigi/output/Menu.java b/src/org/cacert/gigi/output/Menu.java
new file mode 100644 (file)
index 0000000..f351b73
--- /dev/null
@@ -0,0 +1,80 @@
+package org.cacert.gigi.output;
+
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class Menu implements IMenuItem {
+
+    public static final String AUTH_VALUE = "ac";
+
+    private String menuName;
+
+    private IMenuItem[] content;
+
+    private LinkedList<IMenuItem> prepare = new LinkedList<IMenuItem>();
+
+    public Menu(String menuName) {
+        this.menuName = menuName;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        boolean visible = false;
+        AuthorizationContext u = (AuthorizationContext) vars.get(AUTH_VALUE);
+        for (IMenuItem mi : getContent()) {
+            if (mi.isPermitted(u)) {
+                if ( !visible) {
+                    visible = true;
+                    out.print("<li class=\"dropdown\"><a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\" role=\"button\" aria-haspopup=\"true\" aria-expanded=\"false\">");
+                    out.print(l.getTranslation(menuName));
+                    out.print("<span class=\"caret\"></span></a><ul class=\"dropdown-menu\">");
+                }
+                mi.output(out, l, vars);
+            }
+        }
+        if (visible) {
+            out.println("</ul></li>");
+        }
+    }
+
+    public void addItem(IMenuItem item) {
+        prepare.add(item);
+    }
+
+    public void prepare() {
+        content = new IMenuItem[prepare.size()];
+        content = prepare.toArray(content);
+        prepare = null;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof Menu) {
+            return menuName.equals(((Menu) obj).getMenuName());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return menuName.hashCode();
+    }
+
+    public String getMenuName() {
+        return menuName;
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return true;
+    }
+
+    public IMenuItem[] getContent() {
+        return content;
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/MenuCollector.java b/src/org/cacert/gigi/output/MenuCollector.java
new file mode 100644 (file)
index 0000000..7a5a059
--- /dev/null
@@ -0,0 +1,31 @@
+package org.cacert.gigi.output;
+
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class MenuCollector implements IMenuItem {
+
+    private LinkedList<Menu> items = new LinkedList<Menu>();
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        for (Menu menu : items) {
+            menu.output(out, l, vars);
+        }
+
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext u) {
+        return true;
+    }
+
+    public void put(Menu menu) {
+        items.add(menu);
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/Outputable.java b/src/org/cacert/gigi/output/Outputable.java
deleted file mode 100644 (file)
index 4d5978e..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.cacert.gigi.output;
-
-import java.io.PrintWriter;
-import java.util.Map;
-
-import org.cacert.gigi.Language;
-
-public interface Outputable {
-       public void output(PrintWriter out, Language l, Map<String, Object> vars);
-}
diff --git a/src/org/cacert/gigi/output/PageMenuItem.java b/src/org/cacert/gigi/output/PageMenuItem.java
new file mode 100644 (file)
index 0000000..f331e3c
--- /dev/null
@@ -0,0 +1,20 @@
+package org.cacert.gigi.output;
+
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class PageMenuItem extends SimpleMenuItem {
+
+    private Page p;
+
+    public PageMenuItem(Page p, String path) {
+        // "https://" + ServerConstants.getWwwHostNamePortSecure() +
+        super(path, p.getTitle());
+        this.p = p;
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return p.isPermitted(ac);
+    }
+}
diff --git a/src/org/cacert/gigi/output/SimpleMenuItem.java b/src/org/cacert/gigi/output/SimpleMenuItem.java
new file mode 100644 (file)
index 0000000..961a288
--- /dev/null
@@ -0,0 +1,34 @@
+package org.cacert.gigi.output;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class SimpleMenuItem implements IMenuItem {
+
+    private final String href;
+
+    private final String name;
+
+    public SimpleMenuItem(String href, String name) {
+        this.href = href;
+        this.name = name;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.print("<li><a href=\"");
+        out.print(href);
+        out.print("\">");
+        out.print(l.getTranslation(name));
+        out.print("</a></li>");
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return true;
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/Template.java b/src/org/cacert/gigi/output/Template.java
deleted file mode 100644 (file)
index 225560c..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-package org.cacert.gigi.output;
-
-import java.io.PrintWriter;
-import java.io.Reader;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Scanner;
-import java.util.regex.Pattern;
-
-import org.cacert.gigi.Language;
-
-public class Template implements Outputable {
-       String[] contents;
-       Outputable[] vars;
-
-       public Template(Reader r) {
-               LinkedList<String> splitted = new LinkedList<String>();
-               LinkedList<Outputable> commands = new LinkedList<Outputable>();
-               Scanner sc = new Scanner(r);
-               Pattern p1 = Pattern.compile("([^<]|<[^?])*<\\?");
-               Pattern p2 = Pattern.compile("([^<]|<[^?])*\\?>");
-               while (true) {
-                       String s1 = sc.findWithinHorizon(p1, 0);
-                       if (s1 == null) {
-                               break;
-                       }
-                       s1 = s1.substring(0, s1.length() - 2);
-                       splitted.add(s1);
-                       String s2 = sc.findWithinHorizon(p2, 0);
-                       s2 = s2.substring(0, s2.length() - 2);
-                       commands.add(parseCommand(s2));
-               }
-               sc.useDelimiter("\0");
-               if (sc.hasNext()) {
-                       splitted.add(sc.next());
-               }
-               sc.close();
-               contents = splitted.toArray(new String[splitted.size()]);
-               vars = commands.toArray(new Outputable[commands.size()]);
-       }
-       private Outputable parseCommand(String s2) {
-               if (s2.startsWith("=_")) {
-                       final String raw = s2.substring(2);
-                       return new Outputable() {
-
-                               @Override
-                               public void output(PrintWriter out, Language l,
-                                               Map<String, Object> vars) {
-                                       out.print(l.getTranslation(raw));
-                               }
-                       };
-               } else if (s2.startsWith("=$")) {
-                       final String raw = s2.substring(2);
-                       return new Outputable() {
-
-                               @Override
-                               public void output(PrintWriter out, Language l,
-                                               Map<String, Object> vars) {
-                                       outputVar(out, l, vars, raw);
-                               }
-                       };
-               } else if (s2.startsWith("=s,")) {
-                       String command = s2.substring(3);
-                       final LinkedList<String> store = new LinkedList<String>();
-                       while (command.startsWith("$")) {
-                               int idx = command.indexOf(",");
-                               store.add(command.substring(0, idx));
-                               command = command.substring(idx + 1);
-                       }
-                       final String text = command;
-                       return new Outputable() {
-
-                               @Override
-                               public void output(PrintWriter out, Language l,
-                                               Map<String, Object> vars) {
-                                       String[] parts = l.getTranslation(text).split("%s");
-                                       String[] myvars = store.toArray(new String[store.size()]);
-                                       out.print(parts[0]);
-                                       for (int j = 1; j < parts.length; j++) {
-                                               outputVar(out, l, vars, myvars[j - 1].substring(1));
-                                               out.print(parts[j]);
-                                       }
-                               }
-                       };
-               } else {
-                       System.out.println("Unknown processing instruction: " + s2);
-               }
-               return null;
-       }
-       public void output(PrintWriter out, Language l, Map<String, Object> vars) {
-               for (int i = 0; i < contents.length; i++) {
-                       out.print(contents[i]);
-                       if (i < this.vars.length) {
-                               this.vars[i].output(out, l, vars);
-                       }
-               }
-       }
-       private void outputVar(PrintWriter out, Language l,
-                       Map<String, Object> vars, String varname) {
-               Object s = vars.get(varname);
-
-               if (s == null) {
-                       System.out.println("Empty variable: " + varname);
-               }
-               if (s instanceof Outputable) {
-                       ((Outputable) s).output(out, l, vars);
-               } else {
-                       out.print(s);
-               }
-       }
-}
diff --git a/src/org/cacert/gigi/output/template/ForeachStatement.java b/src/org/cacert/gigi/output/template/ForeachStatement.java
new file mode 100644 (file)
index 0000000..4c8b102
--- /dev/null
@@ -0,0 +1,49 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+/**
+ * Outputs an {@link Outputable} multiple times based on a given
+ * {@link IterableDataset}.
+ */
+public final class ForeachStatement implements Translatable {
+
+    private final String variable;
+
+    private final TemplateBlock body;
+
+    /**
+     * Creates a new {@link ForeachStatement}.
+     * 
+     * @param variable
+     *            the variable to take the {@link IterableDataset} from.
+     * @param body
+     *            the body to output multiple times.
+     */
+    public ForeachStatement(String variable, TemplateBlock body) {
+        this.variable = variable;
+        this.body = body;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        Object o = vars.get(variable);
+        if (o instanceof IterableDataset) {
+            IterableDataset id = (IterableDataset) o;
+            Map<String, Object> subcontext = new HashMap<String, Object>(vars);
+            while (id.next(l, subcontext)) {
+                body.output(out, l, subcontext);
+            }
+        }
+    }
+
+    @Override
+    public void addTranslations(Collection<String> s) {
+        body.addTranslations(s);
+    }
+}
diff --git a/src/org/cacert/gigi/output/template/Form.java b/src/org/cacert/gigi/output/template/Form.java
new file mode 100644 (file)
index 0000000..83a96f3
--- /dev/null
@@ -0,0 +1,164 @@
+package org.cacert.gigi.output.template;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.RandomToken;
+
+/**
+ * A generic HTML-form that handles CSRF-token creation.
+ */
+public abstract class Form implements Outputable {
+
+    public static final String CSRF_FIELD = "csrf";
+
+    private final String csrf;
+
+    private final String action;
+
+    /**
+     * Creates a new {@link Form}.
+     * 
+     * @param hsr
+     *            the request to register the form against.
+     */
+    public Form(HttpServletRequest hsr) {
+        this(hsr, null);
+    }
+
+    /**
+     * Creates a new {@link Form}.
+     * 
+     * @param hsr
+     *            the request to register the form against.
+     * @param action
+     *            the target path where the form should be submitted.
+     */
+    public Form(HttpServletRequest hsr, String action) {
+        csrf = RandomToken.generateToken(32);
+        this.action = action;
+        HttpSession hs = hsr.getSession();
+        hs.setAttribute("form/" + getClass().getName() + "/" + csrf, this);
+    }
+
+    /**
+     * Update the forms internal state based on submitted data.
+     * 
+     * @param out
+     *            the stream to the user.
+     * @param req
+     *            the request to take the initial data from.
+     * @return true, iff the form succeeded and the user should be redirected.
+     * @throws GigiApiException
+     *             if internal operations went wrong.
+     */
+    public abstract boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException;
+
+    protected String getCsrfFieldName() {
+        return CSRF_FIELD;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        if (action == null) {
+            out.println("<form method='POST'>");
+        } else {
+            out.println("<form method='POST' action='" + action + "'>");
+        }
+        failed = false;
+        outputContent(out, l, vars);
+        out.print("<input type='hidden' name='" + CSRF_FIELD + "' value='");
+        out.print(getCSRFToken());
+        out.println("'></form>");
+    }
+
+    /**
+     * Outputs the forms contents.
+     * 
+     * @param out
+     *            Stream to the user.
+     * @param l
+     *            {@link Language} to translate text to.
+     * @param vars
+     *            Variables supplied from the outside.
+     */
+    protected abstract void outputContent(PrintWriter out, Language l, Map<String, Object> vars);
+
+    private boolean failed;
+
+    protected void outputError(PrintWriter out, ServletRequest req, String text, Object... contents) {
+        if ( !failed) {
+            failed = true;
+            out.println("<div class='formError'>");
+        }
+        out.print("<div>");
+        if (contents.length == 0) {
+            out.print(Page.translate(req, text));
+        } else {
+            out.print(String.format(Page.translate(req, text), contents));
+        }
+        out.println("</div>");
+    }
+
+    protected void outputErrorPlain(PrintWriter out, String text) {
+        if ( !failed) {
+            failed = true;
+            out.println("<div class='formError'>");
+        }
+        out.print("<div>");
+        out.print(text);
+        out.println("</div>");
+    }
+
+    public boolean isFailed(PrintWriter out) {
+        if (failed) {
+            out.println("</div>");
+        }
+        return failed;
+    }
+
+    protected String getCSRFToken() {
+        return csrf;
+    }
+
+    /**
+     * Re-fetches a form e.g. when a Post-request is received.
+     * 
+     * @param req
+     *            the request that is directed to the form.
+     * @param target
+     *            the {@link Class} of the expected form.
+     * @return the form where this request is directed to.
+     * @throws CSRFException
+     *             if no CSRF-token is found or the token is wrong.
+     */
+    public static <T extends Form> T getForm(HttpServletRequest req, Class<T> target) throws CSRFException {
+        String csrf = req.getParameter(CSRF_FIELD);
+        if (csrf == null) {
+            throw new CSRFException();
+        }
+        HttpSession hs = req.getSession();
+        if (hs == null) {
+            throw new CSRFException();
+        }
+        Form f = (Form) hs.getAttribute("form/" + target.getName() + "/" + csrf);
+        if (f == null) {
+            throw new CSRFException();
+        }
+        return (T) f;
+    }
+
+    public static class CSRFException extends IOException {
+
+        private static final long serialVersionUID = 59708247477988362L;
+
+    }
+}
diff --git a/src/org/cacert/gigi/output/template/IfStatement.java b/src/org/cacert/gigi/output/template/IfStatement.java
new file mode 100644 (file)
index 0000000..1b49f3b
--- /dev/null
@@ -0,0 +1,70 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+/**
+ * One ore two {@link Outputable}s that are emitted conditionally if a given
+ * variable is neither <code>null</code> nor {@link Boolean#FALSE}.
+ */
+public final class IfStatement implements Translatable {
+
+    private final String variable;
+
+    private final TemplateBlock iftrue;
+
+    private final TemplateBlock iffalse;
+
+    /**
+     * Creates a new {@link IfStatement} with an empty else-part.
+     * 
+     * @param variable
+     *            the variable to check.
+     * @param body
+     *            the body to emit conditionally.
+     */
+    public IfStatement(String variable, TemplateBlock body) {
+        this.variable = variable;
+        this.iftrue = body;
+        this.iffalse = null;
+    }
+
+    /**
+     * Creates a new {@link IfStatement} with an else-block.
+     * 
+     * @param variable
+     *            the variable to check.
+     * @param iftrue
+     *            the block to emit if the check succeeds.
+     * @param iffalse
+     *            the block to emit if the check fails.
+     */
+    public IfStatement(String variable, TemplateBlock iftrue, TemplateBlock iffalse) {
+        this.variable = variable;
+        this.iftrue = iftrue;
+        this.iffalse = iffalse;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        Object o = vars.get(variable);
+
+        if ( !(o == null || Boolean.FALSE.equals(o))) {
+            iftrue.output(out, l, vars);
+        } else if (iffalse != null) {
+            iffalse.output(out, l, vars);
+        }
+    }
+
+    @Override
+    public void addTranslations(Collection<String> s) {
+        iftrue.addTranslations(s);
+        if (iffalse != null) {
+            iffalse.addTranslations(s);
+        }
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/template/IterableDataset.java b/src/org/cacert/gigi/output/template/IterableDataset.java
new file mode 100644 (file)
index 0000000..26405a5
--- /dev/null
@@ -0,0 +1,25 @@
+package org.cacert.gigi.output.template;
+
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+/**
+ * Represents some kind of data, that may be iterated over in a template using
+ * the <code>foreach</code> statement.
+ */
+public interface IterableDataset {
+
+    /**
+     * Moves to the next Dataset.
+     * 
+     * @param l
+     *            the language for l10n-ed strings.
+     * @param vars
+     *            the variables used in this template. They need to be updated
+     *            for each line.
+     * @return true, iff there was a data-line "installed". False of this set is
+     *         already empty.
+     */
+    public boolean next(Language l, Map<String, Object> vars);
+}
diff --git a/src/org/cacert/gigi/output/template/OutputVariableCommand.java b/src/org/cacert/gigi/output/template/OutputVariableCommand.java
new file mode 100644 (file)
index 0000000..5dce3ae
--- /dev/null
@@ -0,0 +1,42 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+/**
+ * Emits a variable.
+ */
+public final class OutputVariableCommand implements Translatable {
+
+    private final String raw;
+
+    private final boolean unescaped;
+
+    /**
+     * Creates a new OutputVariableCommand.
+     * 
+     * @param raw
+     *            the variable to emit. If starting with <code>!</code> the
+     *            variable is emitted non-HTML-escaped.
+     */
+    public OutputVariableCommand(String raw) {
+        if (raw.charAt(0) == '!') {
+            unescaped = true;
+            this.raw = raw.substring(1);
+        } else {
+            unescaped = false;
+            this.raw = raw;
+        }
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        Template.outputVar(out, l, vars, raw, unescaped);
+    }
+
+    @Override
+    public void addTranslations(Collection<String> s) {}
+}
diff --git a/src/org/cacert/gigi/output/template/Outputable.java b/src/org/cacert/gigi/output/template/Outputable.java
new file mode 100644 (file)
index 0000000..02fea41
--- /dev/null
@@ -0,0 +1,24 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+/**
+ * An object that is outputable to the user normally in an HTML-page.
+ */
+public interface Outputable {
+
+    /**
+     * Writes this object's content to the given output stream.
+     * 
+     * @param out
+     *            the PrintWriter to the user.
+     * @param l
+     *            the {@link Language} to translate localizable strings to.
+     * @param vars
+     *            a map of variable assignments for this template.
+     */
+    public void output(PrintWriter out, Language l, Map<String, Object> vars);
+}
diff --git a/src/org/cacert/gigi/output/template/OutputableArrayIterable.java b/src/org/cacert/gigi/output/template/OutputableArrayIterable.java
new file mode 100644 (file)
index 0000000..4989540
--- /dev/null
@@ -0,0 +1,43 @@
+package org.cacert.gigi.output.template;
+
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+/**
+ * Generic implementation of {@link IterableDataset} that is fed by an array.
+ */
+public class OutputableArrayIterable implements IterableDataset {
+
+    private Object[] content;
+
+    private String targetName;
+
+    private int index = 0;
+
+    /**
+     * Creates a new {@link OutputableArrayIterable}.
+     * 
+     * @param content
+     *            the objects to be iterated over.
+     * @param targetName
+     *            the variable where the contents of the array to be put in the
+     *            loop.
+     */
+    public OutputableArrayIterable(Object[] content, String targetName) {
+        this.content = content;
+        this.targetName = targetName;
+    }
+
+    @Override
+    public boolean next(Language l, Map<String, Object> vars) {
+        if (index >= content.length) {
+            return false;
+        }
+        vars.put(targetName, content[index]);
+        vars.put("i", index);
+        index++;
+        return true;
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/template/Scope.java b/src/org/cacert/gigi/output/template/Scope.java
new file mode 100644 (file)
index 0000000..700ccfe
--- /dev/null
@@ -0,0 +1,40 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+/**
+ * Builds a variable scope around another {@link Outputable}, statically filling
+ * variables.
+ */
+public class Scope implements Outputable {
+
+    private Map<String, Object> vars;
+
+    private Outputable out;
+
+    /**
+     * Creates a new {@link Scope}.
+     * 
+     * @param out
+     *            the enclosed {@link Outputable}.
+     * @param vars
+     *            the variables to assign in the inner scope.
+     */
+    public Scope(Outputable out, Map<String, Object> vars) {
+        this.out = out;
+        this.vars = vars;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        HashMap<String, Object> map = new HashMap<>();
+        map.putAll(vars);
+        map.putAll(this.vars);
+        this.out.output(out, l, map);
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/template/SprintfCommand.java b/src/org/cacert/gigi/output/template/SprintfCommand.java
new file mode 100644 (file)
index 0000000..51ea9cf
--- /dev/null
@@ -0,0 +1,123 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.util.HTMLEncoder;
+
+/**
+ * A pattern that is to be translated before variables are inserted.
+ */
+public final class SprintfCommand implements Translatable {
+
+    private final String text;
+
+    private final String[] store;
+
+    /**
+     * Creates a new SprintfCommand based on its pre-parsed contents.
+     * 
+     * @param text
+     *            a string with <code>{0},{1},..</code> as placeholders.
+     * @param store
+     *            the data to put into the placeholders: ${var}, $!{var},
+     *            !'plain'.
+     */
+    public SprintfCommand(String text, List<String> store) {
+        this.text = text;
+        this.store = store.toArray(new String[store.size()]);
+    }
+
+    private static final String VARIABLE = "\\$!?\\{[a-zA-Z0-9_-]+\\}";
+
+    private static final Pattern processingInstruction = Pattern.compile("(" + VARIABLE + ")|(!'[^{}'\\$]*)'");
+
+    /**
+     * Creates a new SprintfCommand that is parsed as from template source.
+     * 
+     * @param content
+     *            the part from the template that is to be parsed.
+     */
+    protected SprintfCommand(String content) {
+        StringBuffer raw = new StringBuffer();
+        List<String> var = new LinkedList<String>();
+        int counter = 0;
+        Matcher m = processingInstruction.matcher(content);
+        int last = 0;
+        while (m.find()) {
+            raw.append(content.substring(last, m.start()));
+            String group = null;
+            if ((group = m.group(1)) != null) {
+                var.add(group);
+            } else if ((group = m.group(2)) != null) {
+                var.add(group);
+            } else {
+                throw new Error("Regex is broken??");
+            }
+            last = m.end();
+            raw.append("{" + (counter++) + "}");
+        }
+        raw.append(content.substring(last));
+        text = raw.toString();
+        store = var.toArray(new String[var.size()]);
+    }
+
+    private static final Pattern replacant = Pattern.compile("\\{([0-9]+)\\}");
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        String parts = l.getTranslation(text);
+        Matcher m = replacant.matcher(parts);
+        int pos = 0;
+        while (m.find()) {
+            out.print(HTMLEncoder.encodeHTML(parts.substring(pos, m.start())));
+            String var = store[Integer.parseInt(m.group(1))];
+            if (var.startsWith("$!")) {
+                Template.outputVar(out, l, vars, var.substring(3, var.length() - 1), true);
+            } else if (var.startsWith("!'")) {
+                out.print(var.substring(2));
+            } else if (var.startsWith("$")) {
+                Template.outputVar(out, l, vars, var.substring(2, var.length() - 1), false);
+            } else {
+                throw new Error("Processing error in template.");
+            }
+            pos = m.end();
+
+        }
+        out.print(HTMLEncoder.encodeHTML(parts.substring(pos)));
+    }
+
+    @Override
+    public void addTranslations(Collection<String> s) {
+        s.add(text);
+    }
+
+    /**
+     * Creates a simple {@link SprintfCommand} wrapped in a {@link Scope} to fit
+     * in now constant variables into this template.
+     * 
+     * @param msg
+     *            the message (to be translated) with <code>{0},{1},...</code>
+     *            as placeholders.
+     * @param vars
+     *            the variables to put into the placeholders.
+     * @return the constructed {@link Outputable}.
+     */
+    public static Outputable createSimple(String msg, Object... vars) {
+        HashMap<String, Object> scope = new HashMap<>();
+        String[] store = new String[vars.length];
+        for (int i = 0; i < vars.length; i++) {
+            scope.put("autoVar" + i, vars[i]);
+            store[i] = "${autoVar" + i + "}";
+        }
+        return new Scope(new SprintfCommand(msg, Arrays.asList(store)), scope);
+    }
+}
diff --git a/src/org/cacert/gigi/output/template/Template.java b/src/org/cacert/gigi/output/template/Template.java
new file mode 100644 (file)
index 0000000..15f3408
--- /dev/null
@@ -0,0 +1,229 @@
+package org.cacert.gigi.output.template;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.util.DayDate;
+import org.cacert.gigi.util.HTMLEncoder;
+
+/**
+ * Represents a loaded template file.
+ */
+public class Template implements Outputable {
+
+    private static class ParseResult {
+
+        TemplateBlock block;
+
+        String endType;
+
+        public ParseResult(TemplateBlock block, String endType) {
+            this.block = block;
+            this.endType = endType;
+        }
+
+        public String getEndType() {
+            return endType;
+        }
+
+        public TemplateBlock getBlock(String reqType) {
+            if (endType == null && reqType == null) {
+                return block;
+            }
+            if (endType == null || reqType == null) {
+                throw new Error("Invalid block type: " + endType);
+            }
+            if (endType.equals(reqType)) {
+                return block;
+            }
+            throw new Error("Invalid block type: " + endType);
+        }
+    }
+
+    private TemplateBlock data;
+
+    private long lastLoaded;
+
+    private File source;
+
+    private static final Pattern CONTROL_PATTERN = Pattern.compile(" ?([a-zA-Z]+)\\(\\$([^)]+)\\) ?\\{ ?");
+
+    private static final Pattern ELSE_PATTERN = Pattern.compile(" ?\\} ?else ?\\{ ?");
+
+    /**
+     * Creates a new template by parsing the contents from the given URL. This
+     * constructor will fail on syntax error. When the URL points to a file,
+     * {@link File#lastModified()} is monitored for changes of the template.
+     * 
+     * @param u
+     *            the URL to load the template from. UTF-8 is chosen as charset.
+     */
+    public Template(URL u) {
+        try {
+            Reader r = new InputStreamReader(u.openStream(), "UTF-8");
+            try {
+                if (u.getProtocol().equals("file")) {
+                    source = new File(u.toURI());
+                    lastLoaded = source.lastModified() + 1000;
+                }
+            } catch (URISyntaxException e) {
+                e.printStackTrace();
+            }
+            data = parse(r).getBlock(null);
+            r.close();
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
+    /**
+     * Creates a new template by parsing the contents from the given reader.
+     * This constructor will fail on syntax error.
+     * 
+     * @param r
+     *            the Reader containing the data.
+     */
+    public Template(Reader r) {
+        try {
+            data = parse(r).getBlock(null);
+            r.close();
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
+    private ParseResult parse(Reader r) throws IOException {
+        LinkedList<String> splitted = new LinkedList<String>();
+        LinkedList<Translatable> commands = new LinkedList<Translatable>();
+        StringBuffer buf = new StringBuffer();
+        String blockType = null;
+        outer:
+        while (true) {
+            while ( !endsWith(buf, "<?")) {
+                int ch = r.read();
+                if (ch == -1) {
+                    break outer;
+                }
+                buf.append((char) ch);
+            }
+            buf.delete(buf.length() - 2, buf.length());
+            splitted.add(buf.toString());
+            buf.delete(0, buf.length());
+            while ( !endsWith(buf, "?>")) {
+                int ch = r.read();
+                if (ch == -1) {
+                    throw new EOFException();
+                }
+                buf.append((char) ch);
+            }
+            buf.delete(buf.length() - 2, buf.length());
+            String com = buf.toString().replace("\n", "");
+            buf.delete(0, buf.length());
+            Matcher m = CONTROL_PATTERN.matcher(com);
+            if (m.matches()) {
+                String type = m.group(1);
+                String variable = m.group(2);
+                ParseResult body = parse(r);
+                if (type.equals("if")) {
+                    if ("else".equals(body.getEndType())) {
+                        commands.add(new IfStatement(variable, body.getBlock("else"), parse(r).getBlock("}")));
+                    } else {
+                        commands.add(new IfStatement(variable, body.getBlock("}")));
+                    }
+                } else if (type.equals("foreach")) {
+                    commands.add(new ForeachStatement(variable, body.getBlock("}")));
+                } else {
+                    throw new IOException("Syntax error: unknown control structure: " + type);
+                }
+                continue;
+            } else if ((m = ELSE_PATTERN.matcher(com)).matches()) {
+                blockType = "else";
+                break;
+            } else if (com.matches(" ?\\} ?")) {
+                blockType = "}";
+                break;
+            } else {
+                commands.add(parseCommand(com));
+            }
+        }
+        splitted.add(buf.toString());
+        return new ParseResult(new TemplateBlock(splitted.toArray(new String[splitted.size()]), commands.toArray(new Translatable[commands.size()])), blockType);
+    }
+
+    private boolean endsWith(StringBuffer buf, String string) {
+        return buf.length() >= string.length() && buf.substring(buf.length() - string.length(), buf.length()).equals(string);
+    }
+
+    private Translatable parseCommand(String s2) {
+        if (s2.startsWith("=_")) {
+            final String raw = s2.substring(2);
+            if ( !s2.contains("$") && !s2.contains("!'")) {
+                return new TranslateCommand(raw);
+            } else {
+                return new SprintfCommand(raw);
+            }
+        } else if (s2.startsWith("=$")) {
+            final String raw = s2.substring(2);
+            return new OutputVariableCommand(raw);
+        } else {
+            throw new Error("Unknown processing instruction: " + s2);
+        }
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        if (source != null && lastLoaded < source.lastModified()) {
+            try {
+                System.out.println("Reloading template.... " + source);
+                InputStreamReader r = new InputStreamReader(new FileInputStream(source), "UTF-8");
+                data = parse(r).getBlock(null);
+                r.close();
+                lastLoaded = source.lastModified() + 1000;
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        data.output(out, l, vars);
+    }
+
+    protected static void outputVar(PrintWriter out, Language l, Map<String, Object> vars, String varname, boolean unescaped) {
+        Object s = vars.get(varname);
+
+        if (s == null) {
+            System.out.println("Empty variable: " + varname);
+        }
+        if (s instanceof Outputable) {
+            ((Outputable) s).output(out, l, vars);
+        } else if (s instanceof DayDate) {
+            out.print(DateSelector.getDateFormat().format(((DayDate) s).toDate()));
+        } else if (s instanceof Date) {
+            SimpleDateFormat sdfUI = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+            out.print("<time datetime=\"" + sdf.format(s) + "\">");
+            out.print(sdfUI.format(s));
+            out.print(" UTC</time>");
+        } else {
+            out.print(s == null ? "null" : (unescaped ? s.toString() : HTMLEncoder.encodeHTML(s.toString())));
+        }
+    }
+
+    public void addTranslations(Collection<String> s) {
+        data.addTranslations(s);
+    }
+}
diff --git a/src/org/cacert/gigi/output/template/TemplateBlock.java b/src/org/cacert/gigi/output/template/TemplateBlock.java
new file mode 100644 (file)
index 0000000..1e3aac0
--- /dev/null
@@ -0,0 +1,36 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+
+class TemplateBlock implements Translatable {
+
+    private String[] contents;
+
+    private Translatable[] vars;
+
+    public TemplateBlock(String[] contents, Translatable[] vars) {
+        this.contents = contents;
+        this.vars = vars;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        for (int i = 0; i < contents.length; i++) {
+            out.print(contents[i]);
+            if (i < this.vars.length) {
+                this.vars[i].output(out, l, vars);
+            }
+        }
+    }
+
+    public void addTranslations(Collection<String> s) {
+        for (Translatable t : vars) {
+            t.addTranslations(s);
+        }
+    }
+
+}
diff --git a/src/org/cacert/gigi/output/template/Translatable.java b/src/org/cacert/gigi/output/template/Translatable.java
new file mode 100644 (file)
index 0000000..4df1147
--- /dev/null
@@ -0,0 +1,18 @@
+package org.cacert.gigi.output.template;
+
+import java.util.Collection;
+
+/**
+ * An {@link Outputable} that wants to give static strings to translation
+ * collection.
+ */
+public interface Translatable extends Outputable {
+
+    /**
+     * Adds all static translation Strings to the given {@link Collection}.
+     * 
+     * @param s
+     *            the {@link Collection} to add the Strings to.
+     */
+    public void addTranslations(Collection<String> s);
+}
diff --git a/src/org/cacert/gigi/output/template/TranslateCommand.java b/src/org/cacert/gigi/output/template/TranslateCommand.java
new file mode 100644 (file)
index 0000000..d291e9d
--- /dev/null
@@ -0,0 +1,45 @@
+package org.cacert.gigi.output.template;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Map;
+
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.util.HTMLEncoder;
+
+/**
+ * Wraps a String that needs to be translated before it is printed to the user.
+ */
+public final class TranslateCommand implements Translatable {
+
+    private final String raw;
+
+    /**
+     * Creates a new TranslateCommand that wraps the given String.
+     * 
+     * @param raw
+     *            the String to be translated.
+     */
+    public TranslateCommand(String raw) {
+        this.raw = raw;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.print(HTMLEncoder.encodeHTML(l.getTranslation(raw)));
+    }
+
+    /**
+     * Gets the raw, untranslated String.
+     * 
+     * @return the raw, untranslated String.
+     */
+    public String getRaw() {
+        return raw;
+    }
+
+    @Override
+    public void addTranslations(Collection<String> s) {
+        s.add(raw);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/AboutPage.java b/src/org/cacert/gigi/pages/AboutPage.java
new file mode 100644 (file)
index 0000000..0ab2011
--- /dev/null
@@ -0,0 +1,30 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class AboutPage extends Page {
+
+    public AboutPage() {
+        super("About");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PrintWriter out = resp.getWriter();
+        HashMap<String, Object> o = new HashMap<>();
+        o.put("version", Package.getPackage("org.cacert.gigi").getImplementationVersion());
+        getDefaultTemplate().output(out, getLanguage(req), o);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return true;
+    }
+}
diff --git a/src/org/cacert/gigi/pages/AboutPage.templ b/src/org/cacert/gigi/pages/AboutPage.templ
new file mode 100644 (file)
index 0000000..eed6eb3
--- /dev/null
@@ -0,0 +1 @@
+<?=_This page is generated by gigi version ${version}.?>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/HandlesMixedRequest.java b/src/org/cacert/gigi/pages/HandlesMixedRequest.java
new file mode 100644 (file)
index 0000000..5e15d38
--- /dev/null
@@ -0,0 +1,12 @@
+package org.cacert.gigi.pages;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Marks a {@link Page} as beeing able to handle
+ * {@link HttpServletRequest#getQueryString()} in
+ * {@link HttpServletRequest#getMethod()}<code>== "POST"</code>
+ */
+public interface HandlesMixedRequest {
+
+}
index acfc8f51ed310153e9f1d041402167599d91e35d..141c6ca18cc6466ffae0248458347b10511228f7 100644 (file)
 package org.cacert.gigi.pages;
 
-import static org.cacert.gigi.Gigi.LOGGEDIN;
-import static org.cacert.gigi.Gigi.USER;
+import static org.cacert.gigi.Gigi.*;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.security.cert.X509Certificate;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
-import org.cacert.gigi.User;
-import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.TranslateCommand;
+import org.cacert.gigi.pages.main.RegisterPage;
+import org.cacert.gigi.util.AuthorizationContext;
 import org.cacert.gigi.util.PasswordHash;
+import org.cacert.gigi.util.RateLimit;
+import org.cacert.gigi.util.ServerConstants;
 
 public class LoginPage extends Page {
-       public static final String LOGIN_RETURNPATH = "login-returnpath";
-
-       public LoginPage(String title) {
-               super(title);
-       }
-
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               resp.getWriter()
-                               .println(
-                                               "<form method='POST' action='/login'>"
-                                                               + "<input type='text' name='username'>"
-                                                               + "<input type='password' name='password'> <input type='submit' value='login'></form>");
-       }
-
-       @Override
-       public boolean beforeTemplate(HttpServletRequest req,
-                       HttpServletResponse resp) throws IOException {
-               if (req.getSession().getAttribute("loggedin") == null) {
-                       X509Certificate[] cert = (X509Certificate[]) req
-                                       .getAttribute("javax.servlet.request.X509Certificate");
-                       if (cert != null && cert[0] != null) {
-                               tryAuthWithCertificate(req, cert[0]);
-                       }
-                       if (req.getMethod().equals("POST")) {
-                               tryAuthWithUnpw(req);
-                       }
-               }
-
-               if (req.getSession().getAttribute("loggedin") != null) {
-                       String s = (String) req.getSession().getAttribute(LOGIN_RETURNPATH);
-                       if (s != null) {
-                               if (!s.startsWith("/")) {
-                                       s = "/" + s;
-                               }
-                               resp.sendRedirect(s);
-                       } else {
-                               resp.sendRedirect("/");
-                       }
-                       return true;
-               }
-               return false;
-       }
-       @Override
-       public boolean needsLogin() {
-               return false;
-       }
-       private void tryAuthWithUnpw(HttpServletRequest req) {
-               String un = req.getParameter("username");
-               String pw = req.getParameter("password");
-               try {
-                       PreparedStatement ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "SELECT `password`, `id` FROM `users` WHERE `email`=? AND locked='0' AND verified='1'");
-                       ps.setString(1, un);
-                       ResultSet rs = ps.executeQuery();
-                       if (rs.next()) {
-                               if (PasswordHash.verifyHash(pw, rs.getString(1))) {
-                                       req.getSession().invalidate();
-                                       HttpSession hs = req.getSession();
-                                       hs.setAttribute(LOGGEDIN, true);
-                                       hs.setAttribute(USER, new User(rs.getInt(2)));
-                               }
-                       }
-                       rs.close();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-       }
-       public static User getUser(HttpServletRequest req) {
-               return (User) req.getSession().getAttribute(USER);
-       }
-       private void tryAuthWithCertificate(HttpServletRequest req,
-                       X509Certificate x509Certificate) {
-               String serial = x509Certificate.getSerialNumber().toString(16)
-                               .toUpperCase();
-               try {
-                       PreparedStatement ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "SELECT `memid` FROM `emailcerts` WHERE `serial`=? AND `disablelogin`='0' AND `revoked` = "
-                                                                       + "'0000-00-00 00:00:00'");
-                       ps.setString(1, serial);
-                       ResultSet rs = ps.executeQuery();
-                       if (rs.next()) {
-                               req.getSession().invalidate();
-                               HttpSession hs = req.getSession();
-                               hs.setAttribute(LOGGEDIN, true);
-                               hs.setAttribute(USER, new User(rs.getInt(1)));
-                       }
-                       rs.close();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-       }
+
+    public static final RateLimit RATE_LIMIT = new RateLimit(10, 5 * 60 * 1000);
+
+    public class LoginForm extends Form {
+
+        public LoginForm(HttpServletRequest hsr) {
+            super(hsr);
+        }
+
+        @Override
+        public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+            if (RegisterPage.RATE_LIMIT.isLimitExceeded(req.getRemoteAddr())) {
+                outputError(out, req, "Rate Limit Exceeded");
+                return false;
+            }
+            tryAuthWithUnpw(req);
+            return false;
+        }
+
+        @Override
+        protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+            getDefaultTemplate().output(out, l, vars);
+        }
+
+    }
+
+    public static final String LOGIN_RETURNPATH = "login-returnpath";
+
+    public LoginPage() {
+        super("Password Login");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (req.getHeader("Host").equals(ServerConstants.getSecureHostNamePort())) {
+            resp.getWriter().println(getLanguage(req).getTranslation("Authentication with certificate failed. Try another certificate or use a password."));
+        } else {
+            new LoginForm(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        }
+    }
+
+    @Override
+    public boolean beforeTemplate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        String redir = (String) req.getSession().getAttribute(LOGIN_RETURNPATH);
+        if (req.getSession().getAttribute("loggedin") == null) {
+            X509Certificate cert = getCertificateFromRequest(req);
+            if (cert != null) {
+                tryAuthWithCertificate(req, cert);
+            }
+            if (req.getMethod().equals("POST")) {
+                try {
+                    Form.getForm(req, LoginForm.class).submit(resp.getWriter(), req);
+                } catch (GigiApiException e) {
+                }
+            }
+        }
+
+        if (req.getSession().getAttribute("loggedin") != null) {
+            String s = redir;
+            if (s != null) {
+                if ( !s.startsWith("/")) {
+                    s = "/" + s;
+                }
+                resp.sendRedirect(s);
+            } else {
+                resp.sendRedirect("/");
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+    private void tryAuthWithUnpw(HttpServletRequest req) {
+        String un = req.getParameter("username");
+        String pw = req.getParameter("password");
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `password`, `id` FROM `users` WHERE `email`=? AND verified='1'")) {
+            ps.setString(1, un);
+            GigiResultSet rs = ps.executeQuery();
+            if (rs.next()) {
+                String dbHash = rs.getString(1);
+                String hash = PasswordHash.verifyHash(pw, dbHash);
+                if (hash != null) {
+                    if ( !hash.equals(dbHash)) {
+                        try (GigiPreparedStatement gps = new GigiPreparedStatement("UPDATE `users` SET `password`=? WHERE `email`=?")) {
+                            gps.setString(1, hash);
+                            gps.setString(2, un);
+                            gps.executeUpdate();
+                        }
+                    }
+                    loginSession(req, User.getById(rs.getInt(2)));
+                    req.getSession().setAttribute(LOGIN_METHOD, new TranslateCommand("Password"));
+                }
+            }
+        }
+    }
+
+    public static User getUser(HttpServletRequest req) {
+        AuthorizationContext ac = getAuthorizationContext(req);
+        if (ac == null) {
+            return null;
+        }
+        return ac.getActor();
+    }
+
+    public static AuthorizationContext getAuthorizationContext(HttpServletRequest req) {
+        return ((AuthorizationContext) req.getSession().getAttribute(AUTH_CONTEXT));
+    }
+
+    private void tryAuthWithCertificate(HttpServletRequest req, X509Certificate x509Certificate) {
+        String serial = extractSerialFormCert(x509Certificate);
+        User user = fetchUserBySerial(serial);
+        if (user == null) {
+            return;
+        }
+        loginSession(req, user);
+        req.getSession().setAttribute(CERT_SERIAL, serial);
+        req.getSession().setAttribute(CERT_ISSUER, x509Certificate.getIssuerDN());
+        req.getSession().setAttribute(LOGIN_METHOD, new TranslateCommand("Certificate"));
+    }
+
+    public static String extractSerialFormCert(X509Certificate x509Certificate) {
+        return x509Certificate.getSerialNumber().toString(16).toUpperCase();
+    }
+
+    public static User fetchUserBySerial(String serial) {
+        if ( !serial.matches("[A-Fa-f0-9]+")) {
+            throw new Error("serial malformed.");
+        }
+
+        CertificateOwner o = CertificateOwner.getByEnabledSerial(serial);
+        if (o == null || !(o instanceof User)) {
+            return null;
+        }
+        return (User) o;
+    }
+
+    public static X509Certificate getCertificateFromRequest(HttpServletRequest req) {
+        X509Certificate[] cert = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
+        X509Certificate uc = null;
+        if (cert != null && cert[0] != null) {
+            uc = cert[0];
+        }
+        return uc;
+    }
+
+    private static final Group LOGIN_BLOCKED = Group.getByString("blockedlogin");
+
+    private void loginSession(HttpServletRequest req, User user) {
+        if (user.isInGroup(LOGIN_BLOCKED)) {
+            return;
+        }
+        req.getSession().invalidate();
+        HttpSession hs = req.getSession();
+        hs.setAttribute(LOGGEDIN, true);
+        hs.setAttribute(Language.SESSION_ATTRIB_NAME, user.getPreferredLocale());
+        hs.setAttribute(AUTH_CONTEXT, new AuthorizationContext(user, user));
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac == null;
+    }
 }
diff --git a/src/org/cacert/gigi/pages/LoginPage.templ b/src/org/cacert/gigi/pages/LoginPage.templ
new file mode 100644 (file)
index 0000000..46af15e
--- /dev/null
@@ -0,0 +1,11 @@
+<div class='loginbox'>
+<h1><?=_Login?></h1>
+<p class='smalltext'><?=_Warning! This site requires cookies to be enabled to ensure your privacy and security. This site uses session cookies to store temporary values to prevent people from copying and pasting the session ID to someone else exposing their account, personal details and identity theft as a result.?></p>
+<label for="username"><?=_Email Address?>:</label><input class="form-control" type='text' name="username"/><br />
+<label for="password"><?=_Pass Phrase?>:</label><input class="form-control" type='password' name='password'/><br />
+<input type='submit' name="process" value="<?=_Login?>" /><br /><br />
+<a href='https://blah/index.php?id=4'><?=_Password Login?></a> -<!-- TODO -->
+<a href='https://blah/index.php?id=5'><?=_Lost Password?></a> -
+<a href='https://blah/index.php?id=4&amp;noauto=1'><?=_Net Cafe Login?></a><br />
+<p class='smalltext'><?=_If you are having trouble with your username or password, please visit our !'<a href="http://wiki.cacert.org/wiki/FAQ/LostPasswordOrAccount" target="_new">'wiki page!'</a>' for more information?></p>
+</div>
diff --git a/src/org/cacert/gigi/pages/LogoutPage.java b/src/org/cacert/gigi/pages/LogoutPage.java
new file mode 100644 (file)
index 0000000..6f568f1
--- /dev/null
@@ -0,0 +1,38 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.cacert.gigi.Gigi;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class LogoutPage extends Page {
+
+    public static final String PATH = "/logout";
+
+    public LogoutPage() {
+        super("Logout");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        HttpSession hs = req.getSession();
+        if (req.getPathInfo() != null && req.getPathInfo().equals("/logout")) {
+            if (hs != null) {
+                hs.setAttribute(Gigi.LOGGEDIN, null);
+                hs.invalidate();
+            }
+            resp.sendRedirect("/");
+            return;
+        }
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null;
+    }
+
+}
index adeaa8024fbb5fb4319bf56f12b1b188a71dd2b7..a24188a5ad6d381edac7da6a20bd1f48cb0ef964 100644 (file)
@@ -1,22 +1,32 @@
 package org.cacert.gigi.pages;
 
 import java.io.IOException;
+import java.util.HashMap;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.cacert.gigi.output.template.Template;
+
 public class MainPage extends Page {
-       public MainPage(String title) {
-               super(title);
-       }
-
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               resp.getWriter().println("Access granted.");
-       }
-       @Override
-       public boolean needsLogin() {
-               return false;
-       }
+
+    Template notLog = new Template(MainPage.class.getResource("MainPageNotLogin.templ"));
+
+    public MainPage() {
+        super("Home");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (LoginPage.getUser(req) != null) {
+            getDefaultTemplate().output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        } else {
+            notLog.output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        }
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
 }
diff --git a/src/org/cacert/gigi/pages/MainPage.templ b/src/org/cacert/gigi/pages/MainPage.templ
new file mode 100644 (file)
index 0000000..8ab6329
--- /dev/null
@@ -0,0 +1,14 @@
+<p><?=_Welcome to your account section of the website. Below is a description of the different sections and what they're for.?></p>
+<H4><?=_ET?></H4>
+<p><?=_If you would like to view news items or change languages you can click the logout or go home links. Go home doesn't log you out of the system, just returns you to the front of the website. Logout logs you out of the system.?></p>
+<H4><?=_My Details?></H4>
+<p><?=_In this section you will be able to edit your personal information (if you haven't been assured), update your pass phrase, and lost pass phrase questions. You will also be able to set your location for the Web of Trust, it also effects the email announcement settings which among other things can be set to notify you if you're within 200km of a planned assurance event. You'll also be able to set additional contact information when you become fully trusted, so others can contact you to meet up outside official events.?></p>
+<h4><?=_Email Accounts and Client Certificates?></h4>
+<p><?=_The email account section is for adding/updating/removing email accounts which can be used to issue client certificates against. The client certificate section steps you through generating a certificate signing request for one or more emails you've registered in the email account section.?></p>
+<h4><?=_Domains and Server Certificates.?></h4>
+<p><?=_Before you can start issuing certificates for your website, irc server, smtp server, pop3, imap etc you will need to add domains to your account under the domain menu. You can also remove domains from here as well. Once you've added a domain you are free then to go into the Server Certificate section and start pasting CSR into the website and have the website return you a valid certificate for up to 2 years if you have 50 trust points, or 6 months for no trust points.?></p>
+<h4><?=_Org Client and Server Certificates?></h4>
+<p><?=_Once you have verified your company you will see these menu options. They allow you to issue as many certificates as you like without proving individual email accounts as you like, further more you are able to get your company details on the certificate.?></p>
+<h4><?=_ET Web of Trust?></h4>
+<p><?=_The Web of Trust system ET uses is similar to that many involved with GPG/PGP use, they hold face to face meetings to verify each others photo identities match their GPG/PGP key information. ET differs however in that we have modified things to work within the PKI framework, for you to gain trust in the system you must first locate someone already trusted. The trust person depending how many people they've trusted or meet before will determine how many points they can issue to you (the number of points they can issue is listed in the locate assurer section). Once you've met up you can show your ID and you will need to fill out a CAP form which the person assuring your details must retain for verification reasons.?></p>
+<p><b><?=_For information about the TTP-assisted-assurance program please read !'<a href="//wiki.cacert.org/TTP/TTPuser">https://wiki.cacert.org/TTP/TTPuser</a>' and !'<a href="//wiki.cacert.org/TTP/TTPAL"> https://wiki.cacert.org/TTP/TTPAL</a>'.?></b></p>
diff --git a/src/org/cacert/gigi/pages/MainPageNotLogin.templ b/src/org/cacert/gigi/pages/MainPageNotLogin.templ
new file mode 100644 (file)
index 0000000..91afad2
--- /dev/null
@@ -0,0 +1,21 @@
+<h3><?=_Are you new to SomeCA??></h3>
+
+<p><?=_SomeCA.org is a community-driven Certificate Authority that issues certificates to the public at large for free.?></p>
+
+<p><?=_SomeCA's goal is to promote awareness and education on computer security through the use of encryption, specifically by providing cryptographic certificates. These certificates can be used to digitally sign and encrypt email, authenticate and authorize users connecting to websites and secure data transmission over the internet. Any application that supports the Secure Socket Layer Protocol (SSL or TLS) can make use of certificates signed by SomeCA, as can any application that uses X.509 certificates, e.g. for encryption or code signing and document signatures.?></p>
+
+<p><?=_If you want to have free certificates issued to you, !'<a href="/register">'join the SomeCA Community!'</a>'.?></p>
+
+<p><?=_If you want to use certificates issued by SomeCA, read the SomeCA !'<a href="/policy/RootDistributionLicense.html">'Root Distribution License!'</a>'.?>
+<?=_This license applies to using the SomeCA !'<a href="/roots">'root keys!'</a>'.?></p>
+
+<hr/>
+
+<h3><?=_For SomeCA Community Members?></h3>
+
+<p><?=_Have you passed the SomeCA !'<a href="http://wiki.cacert.org/wiki/AssurerChallenge">'Assurer Challenge!'</a>' yet??></p>
+
+<p><?=_Have you read the !'<a href="/policy/TermsofService.html">'Terms of Service!'</a>' yet??></p>
+
+<p><?=_For general documentation and help, please visit the SomeCA !'<a href="http://wiki.SomeCA.org">'Wiki Documentation site!'</a>'.?>
+<?=_For specific policies, see the SomeCA !'<a href="/policy/">'Approved Policies page!'</a>'.?></p>
diff --git a/src/org/cacert/gigi/pages/OneFormPage.java b/src/org/cacert/gigi/pages/OneFormPage.java
new file mode 100644 (file)
index 0000000..c1e075c
--- /dev/null
@@ -0,0 +1,45 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.output.template.Form;
+
+public abstract class OneFormPage extends Page {
+
+    Class<? extends Form> c;
+
+    public OneFormPage(String title, Class<? extends Form> t) {
+        super(title);
+        c = t;
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            Form form = Form.getForm(req, c);
+            if (form.submit(resp.getWriter(), req)) {
+                resp.sendRedirect(getSuccessPath(form));
+            }
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+            doGet(req, resp);
+        }
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            c.getConstructor(HttpServletRequest.class).newInstance(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        } catch (ReflectiveOperationException e) {
+            new GigiApiException().format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    public abstract String getSuccessPath(Form f);
+
+}
index 04f1dc67292176643ad43848cee92635be030775..8d64d94f93cb884bfd7a94836c18debea283ee36 100644 (file)
 package org.cacert.gigi.pages;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.Locale;
 
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 
-import org.cacert.gigi.Language;
-import org.cacert.gigi.output.Template;
+import org.cacert.gigi.PermissionCheckable;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.util.AuthorizationContext;
 
 /**
  * This class encapsulates a sub page of Gigi. A template residing nearby this
  * class with name &lt;className&gt;.templ will be loaded automatically.
  */
-public abstract class Page {
-       private String title;
-       private Template defaultTemplate;
-
-       public Page(String title) {
-               this.title = title;
-               try {
-                       InputStream resource = getClass().getResourceAsStream(
-                                       getClass().getSimpleName() + ".templ");
-                       if (resource != null) {
-                               defaultTemplate = new Template(new InputStreamReader(resource,
-                                               "UTF-8"));
-                       }
-               } catch (UnsupportedEncodingException e) {
-                       e.printStackTrace();
-               }
-       }
-
-       /**
-        * Retrives the default template (&lt;className&gt;.templ) which has already
-        * been loaded.
-        * 
-        * @return the default template.
-        */
-       public Template getDefaultTemplate() {
-               return defaultTemplate;
-       }
-
-       /**
-        * This method can be overridden to execute code and do stuff before the
-        * default template is applied.
-        * 
-        * @param req
-        *            the request to handle.
-        * @param resp
-        *            the response to write to
-        * @return true, iff the request is consumed and the default template should
-        *         not be applied.
-        * @throws IOException
-        *             if output goes wrong.
-        */
-       public boolean beforeTemplate(HttpServletRequest req,
-                       HttpServletResponse resp) throws IOException {
-               return false;
-       }
-
-       /**
-        * This method is called to generate the content inside the default
-        * template.
-        * 
-        * @param req
-        *            the request to handle.
-        * @param resp
-        *            the response to write to
-        * @throws IOException
-        *             if output goes wrong.
-        */
-       public abstract void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException;
-
-       /**
-        * Same as {@link #doGet(HttpServletRequest, HttpServletResponse)} but for
-        * POST requests. By default they are redirected to
-        * {@link #doGet(HttpServletRequest, HttpServletResponse)};
-        * 
-        * @param req
-        *            the request to handle.
-        * @param resp
-        *            the response to write to
-        * @throws IOException
-        *             if output goes wrong.
-        */
-       public void doPost(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               doGet(req, resp);
-       }
-
-       /**
-        * Returns true, iff this page requires login. Default is <code>true</code>
-        * 
-        * @return iff the page needs login.
-        */
-       public boolean needsLogin() {
-               return true;
-       }
-
-       public String getTitle() {
-               return title;
-       }
-
-       public void setTitle(String title) {
-               this.title = title;
-       }
-       public static Language getLanguage(ServletRequest req) {
-               return Language.getInstance("de");
-       }
-
-       public static String translate(ServletRequest req, String string) {
-               Language l = getLanguage(req);
-               return l.getTranslation(string);
-       }
+public abstract class Page implements PermissionCheckable {
+
+    private String title;
+
+    private Template defaultTemplate;
+
+    public Page(String title) {
+        this.title = title;
+        URL resource = getClass().getResource(getClass().getSimpleName() + ".templ");
+        if (resource != null) {
+            defaultTemplate = new Template(resource);
+        }
+    }
+
+    /**
+     * Retrieves the default template (&lt;className&gt;.templ) which has
+     * already been loaded.
+     * 
+     * @return the default template.
+     */
+    public Template getDefaultTemplate() {
+        return defaultTemplate;
+    }
+
+    /**
+     * This method can be overridden to execute code and do stuff before the
+     * default template is applied.
+     * 
+     * @param req
+     *            the request to handle.
+     * @param resp
+     *            the response to write to
+     * @return true, if the request is consumed and the default template should
+     *         not be applied.
+     * @throws IOException
+     *             if output goes wrong.
+     */
+    public boolean beforeTemplate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        return false;
+    }
+
+    /**
+     * This method is called to generate the content inside the default
+     * template.
+     * 
+     * @param req
+     *            the request to handle.
+     * @param resp
+     *            the response to write to
+     * @throws IOException
+     *             if output goes wrong.
+     */
+    public abstract void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException;
+
+    /**
+     * Same as {@link #doGet(HttpServletRequest, HttpServletResponse)} but for
+     * POST requests. By default they are redirected to
+     * {@link #doGet(HttpServletRequest, HttpServletResponse)};
+     * 
+     * @param req
+     *            the request to handle.
+     * @param resp
+     *            the response to write to
+     * @throws IOException
+     *             if output goes wrong.
+     */
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        doGet(req, resp);
+    }
+
+    /**
+     * Returns true, if this page requires login. Default is <code>true</code>
+     * 
+     * @return if the page needs login.
+     */
+    public boolean needsLogin() {
+        return true;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public static Language getLanguage(ServletRequest req) {
+        HttpSession session = ((HttpServletRequest) req).getSession();
+        synchronized (session) {
+
+            Locale sessval = (Locale) session.getAttribute(Language.SESSION_ATTRIB_NAME);
+            if (sessval != null) {
+                Language l = Language.getInstance(sessval);
+                if (l != null) {
+                    return l;
+                }
+            }
+            Enumeration<Locale> langs = req.getLocales();
+            while (langs.hasMoreElements()) {
+                Locale c = langs.nextElement();
+                Language l = Language.getInstance(c);
+                if (l != null) {
+                    session.setAttribute(Language.SESSION_ATTRIB_NAME, l.getLocale());
+                    return l;
+                }
+            }
+            session.setAttribute(Language.SESSION_ATTRIB_NAME, Locale.ENGLISH);
+            return Language.getInstance(Locale.ENGLISH);
+        }
+    }
+
+    public static String translate(ServletRequest req, String string) {
+        Language l = getLanguage(req);
+        return l.getTranslation(string);
+    }
+
+    public static User getUser(HttpServletRequest req) {
+        return LoginPage.getUser(req);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return !needsLogin() || ac != null;
+    }
 
 }
diff --git a/src/org/cacert/gigi/pages/PasswordResetForm.templ b/src/org/cacert/gigi/pages/PasswordResetForm.templ
new file mode 100644 (file)
index 0000000..61e1c2b
--- /dev/null
@@ -0,0 +1,27 @@
+<table class="table">
+  <thead>
+  <tr>
+    <th colspan="2" class="title"><?=_Change Pass Phrase?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_Password reset token (A-Word)?>: </td>
+    <td><input type="password" name="private_token"></td>
+  </tr>
+  <tr>
+    <td><?=_New Pass Phrase?><span class="formMandatory">*</span>: </td>
+    <td><input type="password" name="pword1"></td>
+  </tr>
+  <tr>
+    <td><?=_Pass Phrase Again?><span class="formMandatory">*</span>: </td>
+    <td><input type="password" name="pword2"></td>
+  </tr>
+  <tr>
+    <td colspan="2"><span class="formMandatory">*</span><?=_Please note, in the interests of good security, the pass phrase must be made up of an upper case letter, lower case letter, number and symbol (all white spaces at the beginning and end are removed).?></td>
+  </tr>
+  <tr>
+    <td colspan="2"><input type="submit" name="process" value="<?=_Update Pass Phrase?>"></td>
+  </tr>
+  </tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/PasswordResetPage.java b/src/org/cacert/gigi/pages/PasswordResetPage.java
new file mode 100644 (file)
index 0000000..496c0e1
--- /dev/null
@@ -0,0 +1,147 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.email.Sendmail;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.SprintfCommand;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.RandomToken;
+import org.cacert.gigi.util.ServerConstants;
+
+public class PasswordResetPage extends Page {
+
+    public static final int HOUR_MAX = 96;
+
+    public static final String PATH = "/passwordReset";
+
+    public PasswordResetPage() {
+        super("Password Reset");
+    }
+
+    public static class PasswordResetForm extends Form {
+
+        private static Template t = new Template(PasswordResetForm.class.getResource("PasswordResetForm.templ"));
+
+        private User u;
+
+        private int id;
+
+        public PasswordResetForm(HttpServletRequest hsr) throws GigiApiException {
+            super(hsr, PATH);
+            String idS = hsr.getParameter("id");
+            String tokS = hsr.getParameter("token");
+            if (idS == null || tokS == null) {
+                throw new GigiApiException("requires id and token");
+            }
+            try {
+                id = Integer.parseInt(idS);
+            } catch (NumberFormatException e) {
+                throw new GigiApiException("requires id to be integer");
+            }
+            u = User.getResetWithToken(id, tokS);
+            if (u == null) {
+                throw new GigiApiException("User missing or token invalid");
+            }
+
+        }
+
+        @Override
+        public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+            try (GigiPreparedStatement passwordReset = new GigiPreparedStatement("UPDATE `passwordResetTickets` SET `used` = CURRENT_TIMESTAMP WHERE `used` IS NULL AND `created` < CURRENT_TIMESTAMP - interval '1 hours' * ?;")) {
+                passwordReset.setInt(1, HOUR_MAX);
+                passwordReset.execute();
+            }
+
+            String p1 = req.getParameter("pword1");
+            String p2 = req.getParameter("pword2");
+            String tok = req.getParameter("private_token");
+            if (p1 == null || p2 == null || tok == null) {
+                throw new GigiApiException("Missing form parameter.");
+            }
+            if ( !p1.equals(p2)) {
+                throw new GigiApiException("New passwords differ.");
+            }
+            u.consumePasswordResetTicket(id, tok, p1);
+            return true;
+        }
+
+        @Override
+        protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+
+            t.output(out, l, vars);
+        }
+
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PasswordResetForm form = Form.getForm(req, PasswordResetForm.class);
+        try {
+            form.submit(resp.getWriter(), req);
+            resp.getWriter().println(getLanguage(req).getTranslation("Password reset successful."));
+            return;
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+        form.output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            new PasswordResetForm(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return true;
+    }
+
+    public static void initPasswordResetProcess(PrintWriter out, User targetUser, HttpServletRequest req, String aword, Language l, String method, String subject) {
+        String ptok = RandomToken.generateToken(32);
+        int id = targetUser.generatePasswordResetTicket(Page.getUser(req), ptok, aword);
+        try {
+            StringWriter sw = new StringWriter();
+            PrintWriter outMail = new PrintWriter(sw);
+            outMail.print(l.getTranslation("Hi,") + "\n\n");
+            outMail.print(method);
+            outMail.print("\n\nhttps://");
+            outMail.print(ServerConstants.getWwwHostNamePortSecure() + PasswordResetPage.PATH);
+            outMail.print("?id=");
+            outMail.print(id);
+            outMail.print("&token=");
+            outMail.print(URLEncoder.encode(ptok, "UTF-8"));
+            outMail.print("\n");
+            outMail.print("\n");
+            SprintfCommand.createSimple("This process will expire in {0} hours.", Integer.toString(HOUR_MAX)).output(outMail, l, new HashMap<String, Object>());
+            outMail.print("\n");
+            outMail.print("\n");
+            outMail.print(l.getTranslation("Best regards"));
+            outMail.print("\n");
+            outMail.print(l.getTranslation("SomeCA.org Support!"));
+            outMail.close();
+            Sendmail.getInstance().sendmail(Page.getUser(req).getEmail(), "[SomeCA.org] " + subject, sw.toString(), "support@cacert.org", null, null, null, null, false);
+            out.println(Page.getLanguage(req).getTranslation("Password reset successful."));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+    }
+}
diff --git a/src/org/cacert/gigi/pages/PolicyIndex.java b/src/org/cacert/gigi/pages/PolicyIndex.java
new file mode 100644 (file)
index 0000000..30c5885
--- /dev/null
@@ -0,0 +1,48 @@
+package org.cacert.gigi.pages;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class PolicyIndex extends Page {
+
+    public PolicyIndex() {
+        super("SomeCA.org Policies");
+    }
+
+    File root = new File("static/www/policy");
+
+    public static final String DEFAULT_PATH = "/policy";
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PrintWriter out = resp.getWriter();
+        out.println("<ul>");
+        File[] files = root.listFiles();
+        if (files != null) {
+            for (File f : files) {
+                String name = f.getName();
+                if ( !name.endsWith(".html")) {
+                    continue;
+                }
+                String display = name.replaceFirst("\\.html$", "");
+
+                out.print("<li><a href='");
+                out.print(name);
+                out.print("'>");
+                out.print(display);
+                out.println("</a></li>");
+            }
+        }
+        out.println("</ul>");
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/RootCertPage.java b/src/org/cacert/gigi/pages/RootCertPage.java
new file mode 100644 (file)
index 0000000..d714af9
--- /dev/null
@@ -0,0 +1,64 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.HashMap;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.util.PEM;
+
+public class RootCertPage extends Page {
+
+    private Certificate root;
+
+    public RootCertPage(KeyStore ks) {
+        super("Root Certificates");
+        try {
+            root = ks.getCertificate("root");
+        } catch (KeyStoreException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public boolean beforeTemplate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (req.getParameter("pem") != null && root != null) {
+            resp.setContentType("application/x-x509-ca-cert");
+            ServletOutputStream out = resp.getOutputStream();
+            try {
+                out.println(PEM.encode("CERTIFICATE", root.getEncoded()));
+            } catch (CertificateEncodingException e) {
+                e.printStackTrace();
+            }
+            return true;
+        } else if (req.getParameter("cer") != null && root != null) {
+            resp.setContentType("application/x-x509-ca-cert");
+            ServletOutputStream out = resp.getOutputStream();
+            try {
+                out.write(root.getEncoded());
+            } catch (CertificateEncodingException e) {
+                e.printStackTrace();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        getDefaultTemplate().output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/RootCertPage.templ b/src/org/cacert/gigi/pages/RootCertPage.templ
new file mode 100644 (file)
index 0000000..cd65445
--- /dev/null
@@ -0,0 +1,2 @@
+<?=_The Root certificates are available for download here. Choose your preferred format:?><br/>
+<a href="?pem">PEM</a> <a href="?cer">CER</a>
diff --git a/src/org/cacert/gigi/pages/StaticPage.java b/src/org/cacert/gigi/pages/StaticPage.java
new file mode 100644 (file)
index 0000000..be2e75b
--- /dev/null
@@ -0,0 +1,29 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.output.template.Template;
+
+public class StaticPage extends Page {
+
+    private Template content;
+
+    public StaticPage(String title, InputStream content) throws UnsupportedEncodingException {
+        super(title);
+        this.content = new Template(new InputStreamReader(content, "UTF-8"));
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        content.output(resp.getWriter(), getLanguage(req), vars);
+    }
+
+}
index e3d2f3cbf2c3f79a3c6176a8dfed46a2f51a8e6f..7a3b76e3af649c2409e9176a05f2af3b1e93ebda 100644 (file)
@@ -7,14 +7,18 @@ import javax.servlet.http.HttpServletResponse;
 
 public class TestSecure extends Page {
 
-       public TestSecure() {
-               super("Secure testpage");
-       }
+    public TestSecure() {
+        super("Secure testpage");
+    }
 
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               resp.getWriter().println("This page is secure.");
-       }
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        resp.getWriter().println("This page is secure.");
+        Object attribute = req.getAttribute("javax.servlet.request.X509Certificate");
+        resp.getWriter().println(attribute);
+        if (attribute != null) {
+            resp.getWriter().println(((Object[]) attribute).length);
+        }
+    }
 
 }
index 8d292667df203c07f99907dee14c862f4a750207..0f88fe4048cd467b208fe477b8d92bce4bae2cfe 100644 (file)
@@ -2,71 +2,121 @@ package org.cacert.gigi.pages;
 
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.Verifyable;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.SprintfCommand;
 
 public class Verify extends Page {
-       public static final String PATH = "/verify";
-       public Verify() {
-               super("Verify email");
-       }
-       @Override
-       public boolean needsLogin() {
-               return false;
-       }
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               PrintWriter out = resp.getWriter();
-               String hash = req.getParameter("hash");
-               String type = req.getParameter("type");
-               String id = req.getParameter("id");
-               if ("email".equals(type)) {
-                       try {
-                               PreparedStatement ps = DatabaseConnection
-                                               .getInstance()
-                                               .prepare(
-                                                               "select email, memid from `email` where `id`=? and `hash`=? and `hash` != '' and `deleted` = 0");
-                               ps.setString(1, id);
-                               ps.setString(2, hash);
-                               ResultSet rs = ps.executeQuery();
-                               rs.last();
-                               if (rs.getRow() == 1) {
-                                       PreparedStatement ps1 = DatabaseConnection
-                                                       .getInstance()
-                                                       .prepare(
-                                                                       "update `email` set `hash`='', `modified`=NOW() where `id`=?");
-                                       ps1.setString(1, id);
-                                       ps1.execute();
-                                       PreparedStatement ps2 = DatabaseConnection
-                                                       .getInstance()
-                                                       .prepare(
-                                                                       "update `users` set `verified`='1' where `id`=? and `email`=? and `verified`='0'");
-                                       ps2.setString(1, rs.getString(2));
-                                       ps2.setString(2, rs.getString(1));
-                                       ps2.execute();
-                                       out.println("Your email is good.");
-                               } else {
-                                       out.println("Your request is invalid");
-                               }
-                       } catch (SQLException e) {
-                               e.printStackTrace();
-                       }
-               }
-       }
-       @Override
-       public void doPost(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               String hash = req.getParameter("hash");
-               String type = req.getParameter("type");
-               if ("email".equals(type)) {
-
-               }
-       }
+
+    private static final SprintfCommand emailAddressVerified = new SprintfCommand("Email address {0} verified", Arrays.asList("${subject}"));
+
+    private static final SprintfCommand domainVerified = new SprintfCommand("Domain {0} verified", Arrays.asList("${subject}"));
+
+    private class VerificationForm extends Form {
+
+        private String hash;
+
+        private String type;
+
+        private String id;
+
+        private Verifyable target;
+
+        String subject;
+
+        public VerificationForm(HttpServletRequest hsr) {
+            super(hsr, PATH);
+            hash = hsr.getParameter("hash");
+            type = hsr.getParameter("type");
+            id = hsr.getParameter("id");
+            if ("email".equals(type)) {
+                EmailAddress addr = EmailAddress.getById(Integer.parseInt(id));
+                subject = addr.getAddress();
+                target = addr;
+            } else if ("domain".equals(type)) {
+                Domain domain = Domain.getById(Integer.parseInt(id));
+                subject = domain.getSuffix();
+                target = domain;
+            }
+        }
+
+        @Override
+        public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+            HashMap<String, Object> data = new HashMap<>();
+            data.put("subject", subject);
+            if ("email".equals(type)) {
+                try {
+                    target.verify(hash);
+                    emailAddressVerified.output(out, getLanguage(req), data);
+                } catch (IllegalArgumentException e) {
+                    out.println(translate(req, "The email address is invalid."));
+                } catch (GigiApiException e) {
+                    e.format(out, getLanguage(req));
+                }
+            } else if ("domain".equals(type)) {
+                try {
+                    target.verify(hash);
+                    domainVerified.output(out, getLanguage(req), data);
+                } catch (IllegalArgumentException e) {
+                    out.println(translate(req, "The domain is invalid."));
+                } catch (GigiApiException e) {
+                    e.format(out, getLanguage(req));
+                }
+            }
+            return true;
+        }
+
+        @Override
+        protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+            vars.put("hash", hash);
+            vars.put("id", id);
+            vars.put("type", type);
+
+            vars.put("subject", subject);
+            getDefaultTemplate().output(out, l, vars);
+        }
+    }
+
+    public static final String PATH = "/verify";
+
+    public Verify() {
+        super("Verify email");
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            if (Form.getForm(req, VerificationForm.class).submit(resp.getWriter(), req)) {
+            }
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            new VerificationForm(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        } catch (IllegalArgumentException e) {
+            resp.getWriter().println(translate(req, "The object to verify is invalid."));
+
+        }
+    }
+
 }
diff --git a/src/org/cacert/gigi/pages/Verify.templ b/src/org/cacert/gigi/pages/Verify.templ
new file mode 100644 (file)
index 0000000..1101f91
--- /dev/null
@@ -0,0 +1,5 @@
+<?=_Verify this element: ${subject}?>
+<input type="hidden" name="hash" value="<?=$hash?>"/>
+<input type="hidden" name="type" value="<?=$type?>"/>
+<input type="hidden" name="id" value="<?=$id?>"/>
+<input type="submit" value="OK"/>
diff --git a/src/org/cacert/gigi/pages/account/ChangeForm.java b/src/org/cacert/gigi/pages/account/ChangeForm.java
new file mode 100644 (file)
index 0000000..8e73d3f
--- /dev/null
@@ -0,0 +1,60 @@
+package org.cacert.gigi.pages.account;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+
+public class ChangeForm extends Form {
+
+    private User target;
+
+    public ChangeForm(HttpServletRequest hsr, User target) {
+        super(hsr);
+        this.target = target;
+    }
+
+    private static Template t;
+    static {
+        t = new Template(ChangePasswordPage.class.getResource("ChangePasswordForm.templ"));
+    }
+
+    @Override
+    public void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        t.output(out, l, vars);
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        String oldpassword = req.getParameter("oldpassword");
+        String p1 = req.getParameter("pword1");
+        String p2 = req.getParameter("pword2");
+        GigiApiException error = new GigiApiException();
+        if (oldpassword == null || p1 == null || p2 == null) {
+            new GigiApiException("All fields are required.").format(out, Page.getLanguage(req));
+            return false;
+        }
+        if ( !p1.equals(p2)) {
+            new GigiApiException("New passwords do not match.").format(out, Page.getLanguage(req));
+            return false;
+        }
+        try {
+            target.changePassword(oldpassword, p1);
+        } catch (GigiApiException e) {
+            error.mergeInto(e);
+        }
+        if ( !error.isEmpty()) {
+            error.format(out, Page.getLanguage(req));
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/account/ChangePasswordForm.templ b/src/org/cacert/gigi/pages/account/ChangePasswordForm.templ
new file mode 100644 (file)
index 0000000..d458898
--- /dev/null
@@ -0,0 +1,27 @@
+<table class="table">
+  <thead>
+  <tr>
+    <th colspan="2" class="title"><?=_Change Pass Phrase?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_Old Pass Phrase?>: </td>
+    <td><input class="form-control" type="password" name="oldpassword"></td>
+  </tr>
+  <tr>
+    <td><?=_New Pass Phrase?><span class="formMandatory">*</span>: </td>
+    <td><input class="form-control" type="password" name="pword1"></td>
+  </tr>
+  <tr>
+    <td><?=_Pass Phrase Again?><span class="formMandatory">*</span>: </td>
+    <td><input class="form-control" type="password" name="pword2"></td>
+  </tr>
+  <tr>
+    <td colspan="2"><span class="formMandatory">*</span><?=_Please note, in the interests of good security, the pass phrase must be made up of an upper case letter, lower case letter, number and symbol (all white spaces at the beginning and end are removed).?></td>
+  </tr>
+  <tr>
+    <td colspan="2"><input type="submit" name="process" value="<?=_Update Pass Phrase?>"></td>
+  </tr>
+  </tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/account/ChangePasswordPage.java b/src/org/cacert/gigi/pages/account/ChangePasswordPage.java
new file mode 100644 (file)
index 0000000..8432f02
--- /dev/null
@@ -0,0 +1,37 @@
+package org.cacert.gigi.pages.account;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class ChangePasswordPage extends Page {
+
+    public static final String PATH = "/account/password";
+
+    public ChangePasswordPage() {
+        super("Change Password");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        new ChangeForm(req, getUser(req)).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        ChangeForm f = Form.getForm(req, ChangeForm.class);
+        f.submit(resp.getWriter(), req);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.getTarget() instanceof User;
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/History.java b/src/org/cacert/gigi/pages/account/History.java
new file mode 100644 (file)
index 0000000..520e3a5
--- /dev/null
@@ -0,0 +1,55 @@
+package org.cacert.gigi.pages.account;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.OutputableArrayIterable;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class History extends Page {
+
+    public static final String SUPPORT_PATH = "/support/user/*/history";
+
+    public static final String PATH = "/account/history";
+
+    private static final int intStart = SUPPORT_PATH.indexOf('*');
+
+    private boolean support;
+
+    public History(boolean support) {
+        super(support ? "Support History" : "History");
+        this.support = support;
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        CertificateOwner u;
+        if (support) {
+            String info = req.getPathInfo();
+            int id = Integer.parseInt(info.substring(intStart, info.length() - SUPPORT_PATH.length() + intStart + 1));
+            u = User.getById(id);
+            if (u == null) {
+                resp.sendError(404);
+                return;
+            }
+        } else {
+            u = LoginPage.getAuthorizationContext(req).getTarget();
+        }
+        String[] adminLog = u.getAdminLog();
+        HashMap<String, Object> vars = new HashMap<>();
+        vars.put("entries", new OutputableArrayIterable(adminLog, "entry"));
+        getDefaultTemplate().output(resp.getWriter(), getLanguage(req), vars);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ( !support || ac.canSupport());
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/History.templ b/src/org/cacert/gigi/pages/account/History.templ
new file mode 100644 (file)
index 0000000..d6fe85a
--- /dev/null
@@ -0,0 +1,9 @@
+<table class="table">
+<tbody>
+<tr><th><?=_Support actions?></th></tr>
+<? foreach($entries) { ?>
+<tr><td><?=$entry?></td></tr>
+<? } ?>
+</tbody>
+</table>
+
diff --git a/src/org/cacert/gigi/pages/account/MailAdd.java b/src/org/cacert/gigi/pages/account/MailAdd.java
deleted file mode 100644 (file)
index e311cbc..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.cacert.gigi.pages.account;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.LinkedList;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.cacert.gigi.Language;
-import org.cacert.gigi.output.DataTable;
-import org.cacert.gigi.output.DataTable.Cell;
-import org.cacert.gigi.pages.Page;
-
-public class MailAdd extends Page{
-       public static final String DEFAULT_PATH = "/account/mails/add";
-       public MailAdd(String title) {
-               super(title);
-       }
-
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               LinkedList<Cell> cells = new LinkedList<>();
-               cells.add(new Cell("Add Email", true, 2, "class=\"title\""));
-               cells.add(new Cell("Email Address", true));
-               cells.add(new Cell("<input type=\"text\" name=\"newemail\">", false));
-               Language language = getLanguage(req);
-               String trans = language.getTranslation("I own or am authorised to control this email address");
-               cells.add(new Cell("<input type=\"submit\" name=\"process\" value=\""
-                               + trans + "\">", false, 2));
-               DataTable dt = new DataTable(2, cells);
-               dt.output(resp.getWriter(), language);
-               PrintWriter out = resp.getWriter();
-               out.println("<p>");
-               out.println(language
-                               .getTranslation(
-                                               "Currently we only issue certificates for Punycode domains if the person requesting them has code signing attributes attached to their account, as these have potentially slightly higher security risk."));
-               out.println("</p>");
-       }
-
-}
diff --git a/src/org/cacert/gigi/pages/account/MailCertificates.java b/src/org/cacert/gigi/pages/account/MailCertificates.java
deleted file mode 100644 (file)
index 72952d4..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.cacert.gigi.pages.account;
-
-import java.io.IOException;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.HashMap;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.cacert.gigi.User;
-import org.cacert.gigi.database.DatabaseConnection;
-import org.cacert.gigi.output.CertificateTable;
-import org.cacert.gigi.pages.LoginPage;
-import org.cacert.gigi.pages.Page;
-
-public class MailCertificates extends Page {
-       CertificateTable myTable = new CertificateTable("mailcerts");
-       public static final String PATH = "/account/certs/email";
-
-       public MailCertificates() {
-               super("Email Certificates");
-       }
-
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               HashMap<String, Object> vars = new HashMap<String, Object>();
-               User us = LoginPage.getUser(req);
-               try {
-                       PreparedStatement ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "SELECT `id`, `CN`, `serial`, `revoked`, `expire`, `disablelogin` FROM `emailcerts` WHERE `memid`=?");
-                       ps.setInt(1, us.getId());
-                       ResultSet rs = ps.executeQuery();
-                       vars.put("mailcerts", rs);
-                       myTable.output(resp.getWriter(), getLanguage(req), vars);
-                       rs.close();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-       }
-
-}
diff --git a/src/org/cacert/gigi/pages/account/MailOverview.java b/src/org/cacert/gigi/pages/account/MailOverview.java
deleted file mode 100644 (file)
index e574438..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.cacert.gigi.pages.account;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.HashMap;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.cacert.gigi.Language;
-import org.cacert.gigi.User;
-import org.cacert.gigi.database.DatabaseConnection;
-import org.cacert.gigi.output.MailTable;
-import org.cacert.gigi.pages.LoginPage;
-import org.cacert.gigi.pages.Page;
-
-public class MailOverview extends Page {
-       public static final String DEFAULT_PATH = "/account/mails";
-       private MailTable table = new MailTable("mails", "userMail");
-       public MailOverview(String title) {
-               super(title);
-       }
-
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               HashMap<String, Object> vars = new HashMap<String, Object>();
-               User us = LoginPage.getUser(req);
-               int id = us.getId();
-               try {
-                       PreparedStatement ps = DatabaseConnection.getInstance().prepare(
-                                       "SELECT * from `email` WHERE `memid`=? AND `deleted`=0");
-                       ps.setInt(1, id);
-                       ResultSet rs = ps.executeQuery();
-                       vars.put("mails", rs);
-                       vars.put("userMail", us.getEmail());
-
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-               Language language = getLanguage(req);
-               table.output(resp.getWriter(), language, vars);
-               PrintWriter wri = resp.getWriter();
-               wri.println("<p>");
-               wri.println(language
-                               .getTranslation("Please Note: You can not set an unverified account as a default account, and you can not remove a default account. To remove the default account you must set another verified account as the default."));
-               wri.println("</p>");
-       }
-
-}
index 6bce47b8cac5e1e7a7271dc3896f13c686359fc0..4b0aa992fe6025bac5ed46c834a96fef62958acc 100644 (file)
@@ -1,7 +1,5 @@
 package org.cacert.gigi.pages.account;
 
-import static org.cacert.gigi.Gigi.USER;
-
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -9,39 +7,46 @@ import java.util.HashMap;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.cacert.gigi.User;
-import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.LoginPage;
 import org.cacert.gigi.pages.Page;
-import org.cacert.gigi.util.HTMLEncoder;
 
 public class MyDetails extends Page {
 
-       public MyDetails() {
-               super("My Details");
-       }
-
-       public static final String PATH = "/account/details";
-
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               User u = (User) req.getSession().getAttribute(USER);
-
-               PrintWriter out = resp.getWriter();
-               HashMap<String, Object> map = new HashMap<String, Object>();
-               map.put("fname", HTMLEncoder.encodeHTML(u.getFname()));
-               map.put("mname",
-                               u.getMname() == null
-                                               ? ""
-                                               : HTMLEncoder.encodeHTML(u.getMname()));
-               map.put("lname", HTMLEncoder.encodeHTML(u.getLname()));
-               map.put("suffix",
-                               u.getSuffix() == null ? "" : HTMLEncoder.encodeHTML(u
-                                               .getSuffix()));
-               DateSelector ds = new DateSelector("day", "month", "year");
-               map.put("DoB", ds);
-               map.put("details", "");
-               getDefaultTemplate().output(out, getLanguage(req), map);
-
-       }
+    public MyDetails() {
+        super("My Details");
+    }
+
+    public static final String PATH = "/account/details";
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PrintWriter out = resp.getWriter();
+        HashMap<String, Object> map = new HashMap<String, Object>();
+        MyDetailsForm form = new MyDetailsForm(req, getUser(req));
+        map.put("detailsForm", form);
+        if (LoginPage.getUser(req).getOrganisations().size() != 0) {
+            map.put("orgaForm", new MyOrganisationsForm(req));
+        }
+        getDefaultTemplate().output(out, getLanguage(req), map);
+    }
+
+    @Override
+    public boolean beforeTemplate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (req.getParameter("orgaForm") != null) {
+            Form.getForm(req, MyOrganisationsForm.class).submit(resp.getWriter(), req);
+        } else {
+            return false;
+        }
+        resp.sendRedirect(PATH);
+        return true;
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (req.getParameter("processDetails") != null) {
+            Form.getForm(req, MyDetailsForm.class).submit(resp.getWriter(), req);
+        }
+        super.doPost(req, resp);
+    }
 }
index 4a7584301624fe1cdf9b5884e60bf26f0d867965..db8d4f4e7d5e1573f253ff378f0f1abb4185c522 100644 (file)
@@ -1,40 +1,4 @@
-<form method="post" action="/account/myDetails">
-<table class="wrapper" width="400">
-  <tr>
-    <td colspan="2" class="title"><?=_My Details?></td>
-  </tr>
-  <tr>
-    <td class="DataTD" width="125"><?=_First Name?>: </td>
-    <td class="DataTD" width="125"><input type="text" name="fname" value="<?=$fname?>"></td>
-  </tr>
-  <tr>
-    <td class="DataTD" valign="top"><?=_Middle Name(s)?><br>
-      (<?=_optional?>)
-    </td>
-    <td class="DataTD"><input type="text" name="mname" value="<?=$mname?>"></td>
-  </tr>
-  <tr>
-    <td class="DataTD"><?=_Last Name?>: </td>
-    <td class="DataTD"><input type="text" name="lname" value="<?=$lname?>"></td>
-  </tr>
-  <tr>
-    <td class="DataTD"><?=_Suffix?><br>
-      (<?=_optional?>)</td>
-    <td class="DataTD"><input type="text" name="suffix" value="<?=$suffix?>"></td>
-  </tr>
-  <tr>
-    <td class="DataTD"><?=_Date of Birth?><br>
-           (<?=_dd/mm/yyyy?>)</td>
-    <td class="DataTD"><?=$DoB?></td>
-  </tr>
-  <tr>
-    <td colspan="2" class="title"><?=_('Show account history')?></td>
-  </tr>
-  <tr>
-    <td colspan="2" class="title"><?=_View secret question & answers and OTP phrases?></td>
-  </tr>
-  <?=$details?>
-  <tr><td class="DataTD" colspan="2"><input type="submit" name="process" value="<?=_Update?>"></td>
-  </tr>
-</table>
-</form>
+<?=$detailsForm?>
+<? if($orgaForm) { ?>
+<?=$orgaForm?>
+<? } ?>
diff --git a/src/org/cacert/gigi/pages/account/MyDetailsForm.java b/src/org/cacert/gigi/pages/account/MyDetailsForm.java
new file mode 100644 (file)
index 0000000..9ea66b3
--- /dev/null
@@ -0,0 +1,85 @@
+package org.cacert.gigi.pages.account;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.HTMLEncoder;
+
+public class MyDetailsForm extends Form {
+
+    private static Template assured = new Template(MyDetails.class.getResource("MyDetailsFormAssured.templ"));
+
+    private static Template templ;
+    static {
+        templ = new Template(MyDetailsForm.class.getResource("MyDetailsForm.templ"));
+    }
+
+    private User target;
+
+    private DateSelector ds;
+
+    public MyDetailsForm(HttpServletRequest hsr, User target) {
+        super(hsr);
+        this.target = target;
+        this.ds = new DateSelector("day", "month", "year", target.getDoB());
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        try {
+            synchronized (target) {
+                if (target.getAssurancePoints() == 0) {
+                    String newFname = req.getParameter("fname").trim();
+                    String newLname = req.getParameter("lname").trim();
+                    String newMname = req.getParameter("mname").trim();
+                    String newSuffix = req.getParameter("suffix").trim();
+                    if (newLname.isEmpty()) {
+                        throw new GigiApiException("Last name cannot be empty.");
+                    }
+
+                    target.setName(new Name(newFname, newLname, newMname, newSuffix));
+                    ds.update(req);
+                    target.setDoB(ds.getDate());
+                    target.updateUserData();
+                } else {
+                    throw new GigiApiException("No change after assurance allowed.");
+                }
+            }
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+            return false;
+        } catch (NumberFormatException e) {
+            new GigiApiException("Invalid value.").format(out, Page.getLanguage(req));
+            return false;
+        }
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        Name name = target.getName();
+        vars.put("fname", HTMLEncoder.encodeHTML(name.getFname()));
+        vars.put("mname", name.getMname() == null ? "" : HTMLEncoder.encodeHTML(name.getMname()));
+        vars.put("lname", HTMLEncoder.encodeHTML(name.getLname()));
+        vars.put("suffix", name.getSuffix() == null ? "" : HTMLEncoder.encodeHTML(name.getSuffix()));
+        vars.put("details", "");
+        if (target.getAssurancePoints() == 0) {
+            vars.put("DoB", ds);
+            templ.output(out, l, vars);
+        } else {
+            vars.put("DoB", target.getDoB());
+            assured.output(out, l, vars);
+        }
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/account/MyDetailsForm.templ b/src/org/cacert/gigi/pages/account/MyDetailsForm.templ
new file mode 100644 (file)
index 0000000..d4d3c7e
--- /dev/null
@@ -0,0 +1,42 @@
+<table class="table">
+<thead>
+  <tr>
+    <th colspan="2"><?=_My Details?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_First Name?>: </td>
+    <td><input class="form-control" type="text" name="fname" value="<?=$fname?>"></td>
+  </tr>
+  <tr>
+    <td valign="top"><?=_Middle Name(s)?><br>
+      (<?=_optional?>)
+    </td>
+    <td><input class="form-control" type="text" name="mname" value="<?=$mname?>"></td>
+  </tr>
+  <tr>
+    <td><?=_Last Name?>: </td>
+    <td><input class="form-control" type="text" name="lname" value="<?=$lname?>"></td>
+  </tr>
+  <tr>
+    <td><?=_Suffix?><br>
+      (<?=_optional?>)</td>
+    <td><input class="form-control" type="text" name="suffix" value="<?=$suffix?>"></td>
+  </tr>
+  <tr>
+    <td><?=_Date of Birth?><br>
+           (<?=_yyyy-mm-dd?>)</td>
+    <td><?=$DoB?></td>
+  </tr>
+  <tr>
+    <td colspan="2" class="title"><a href="/account/history"><?=_Show account history?></a></td>
+  </tr>
+  <tr>
+    <td colspan="2" class="title"><?=_View secret question & answers and OTP phrases?></td>
+  </tr>
+  <?=$details?>
+  <tr><td colspan="2"><input type="submit" name="processDetails" value="<?=_Update?>"></td>
+  </tr>
+  </tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/account/MyDetailsFormAssured.templ b/src/org/cacert/gigi/pages/account/MyDetailsFormAssured.templ
new file mode 100644 (file)
index 0000000..8cfb770
--- /dev/null
@@ -0,0 +1,40 @@
+<table class="table">
+<thead>
+  <tr>
+    <th colspan="2"><?=_My Details?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_First Name?>: </td>
+    <td><?=$fname?></td>
+  </tr>
+  <tr>
+    <td valign="top"><?=_Middle Name(s)?><br>
+      (<?=_optional?>)
+    </td>
+    <td><?=$mname?></td>
+  </tr>
+  <tr>
+    <td><?=_Last Name?>: </td>
+    <td><?=$lname?></td>
+  </tr>
+  <tr>
+    <td><?=_Suffix?><br>
+      (<?=_optional?>)</td>
+    <td><?=$suffix?></td>
+  </tr>
+  <tr>
+    <td><?=_Date of Birth?><br>
+           (<?=_yyyy-mm-dd?>)</td>
+    <td><?=$DoB?></td>
+  </tr>
+  <tr>
+    <td colspan="2" class="title"><a href="/account/history"><?=_Show account history?></a></td>
+  </tr>
+  <tr>
+    <td colspan="2" class="title"><?=_View secret question & answers and OTP phrases?></td>
+  </tr>
+  <?=$details?>
+  </tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/account/MyOrganisationsForm.java b/src/org/cacert/gigi/pages/account/MyOrganisationsForm.java
new file mode 100644 (file)
index 0000000..443a9ef
--- /dev/null
@@ -0,0 +1,91 @@
+package org.cacert.gigi.pages.account;
+
+import java.io.PrintWriter;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.Gigi;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class MyOrganisationsForm extends Form {
+
+    private AuthorizationContext target;
+
+    public MyOrganisationsForm(HttpServletRequest hsr) {
+        super(hsr);
+        target = LoginPage.getAuthorizationContext(hsr);
+    }
+
+    private static Template template;
+
+    static {
+        template = new Template(MyOrganisationsForm.class.getResource("MyOrganisationsForm.templ"));
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        if (req.getParameter("org-leave") != null) {
+            req.getSession().setAttribute(Gigi.AUTH_CONTEXT, new AuthorizationContext(target.getActor(), target.getActor()));
+            return true;
+        }
+        Enumeration<String> i = req.getParameterNames();
+        int orgId = -1;
+        while (i.hasMoreElements()) {
+            String s = i.nextElement();
+            if (s.startsWith("org:")) {
+                int id = Integer.parseInt(s.substring(4));
+                if (orgId == -1) {
+                    orgId = id;
+                } else {
+                    out.println(LoginPage.getLanguage(req).getTranslation("Error: invalid parameter."));
+                    return false;
+                }
+            }
+        }
+        for (Organisation org : target.getActor().getOrganisations()) {
+            if (org.getId() == orgId) {
+
+                req.getSession().setAttribute(Gigi.AUTH_CONTEXT, new AuthorizationContext(org, target.getActor()));
+                return true;
+            }
+        }
+        System.out.println("Switch fialed");
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        final List<Organisation> o = target.getActor().getOrganisations();
+        if (target.getTarget() != target.getActor()) {
+            vars.put("personal", target.getTarget() != target.getActor());
+        }
+        vars.put("orgas", new IterableDataset() {
+
+            Iterator<Organisation> it = o.iterator();
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if ( !it.hasNext()) {
+                    return false;
+                }
+                Organisation o = it.next();
+                vars.put("orgName", o.getName());
+                vars.put("orgID", o.getId());
+                return true;
+            }
+        });
+        template.output(out, l, vars);
+
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/account/MyOrganisationsForm.templ b/src/org/cacert/gigi/pages/account/MyOrganisationsForm.templ
new file mode 100644 (file)
index 0000000..c0eb939
--- /dev/null
@@ -0,0 +1,10 @@
+<input type='hidden' name='orgaForm' value='orga'/>
+<h2><?=_My Organisations?></h2>
+<table class="table">
+<? foreach($orgas) { ?>
+<tr><td><?=$orgName?></td><td><?=$orgID?></td><td><input type='submit' value='<?=_switch to this organisation?>' name='org:<?=$orgID?>'/></td></tr>
+<? } ?>
+</table>
+<? if($personal) { ?>
+<input type='submit' value='<?=_switch back to personal use?>' name='org-leave'/>
+<? } ?>
diff --git a/src/org/cacert/gigi/pages/account/UserTrainings.java b/src/org/cacert/gigi/pages/account/UserTrainings.java
new file mode 100644 (file)
index 0000000..3a0a450
--- /dev/null
@@ -0,0 +1,59 @@
+package org.cacert.gigi.pages.account;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.OutputableArrayIterable;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class UserTrainings extends Page {
+
+    public static final String SUPPORT_PATH = "/support/user/*/trainings";
+
+    public static final String PATH = "/account/trainings";
+
+    private static final int intStart = SUPPORT_PATH.indexOf('*');
+
+    private boolean support;
+
+    public UserTrainings(boolean support) {
+        super(support ? "Support User Trainings" : "Trainings");
+        this.support = support;
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        User u;
+        if (support) {
+            String info = req.getPathInfo();
+            int id = Integer.parseInt(info.substring(intStart, info.length() - SUPPORT_PATH.length() + intStart + 1));
+            u = User.getById(id);
+            if (u == null) {
+                resp.sendError(404);
+                return;
+            }
+        } else {
+            u = getUser(req);
+        }
+        HashMap<String, Object> vars = new HashMap<>();
+        vars.put("entries", new OutputableArrayIterable(u.getTrainings(), "entry"));
+        getDefaultTemplate().output(resp.getWriter(), getLanguage(req), vars);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        if (ac == null) {
+            return false;
+        }
+        if (support) {
+            return ac.canSupport();
+        } else {
+            return ac.getTarget() instanceof User;
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/UserTrainings.templ b/src/org/cacert/gigi/pages/account/UserTrainings.templ
new file mode 100644 (file)
index 0000000..0e2667a
--- /dev/null
@@ -0,0 +1,9 @@
+<table class="table">
+<tbody>
+<tr><th><?=_Trainings?></th></tr>
+<? foreach($entries) { ?>
+<tr><td><?=$entry?></td></tr>
+<? } ?>
+</tbody>
+</table>
+
diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateAdd.java b/src/org/cacert/gigi/pages/account/certs/CertificateAdd.java
new file mode 100644 (file)
index 0000000..e37b930
--- /dev/null
@@ -0,0 +1,54 @@
+package org.cacert.gigi.pages.account.certs;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class CertificateAdd extends Page {
+
+    public static final String PATH = "/account/certs/new";
+
+    public CertificateAdd() {
+        super("Create certificate");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        new CertificateIssueForm(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        CertificateIssueForm f = Form.getForm(req, CertificateIssueForm.class);
+        if (f.submit(resp.getWriter(), req)) {
+            Certificate c = f.getResult();
+            if (c.getStatus() != CertificateStatus.ISSUED) {
+                resp.getWriter().println("Timeout while waiting for certificate.");
+                return;
+            }
+            String ser = c.getSerial();
+            if (ser.isEmpty()) {
+                resp.getWriter().println("Timeout while waiting for certificate.");
+                return;
+            }
+            resp.sendRedirect(Certificates.PATH + "/" + ser);
+        }
+        f.output(resp.getWriter(), getLanguage(req), Collections.<String, Object>emptyMap());
+
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return super.isPermitted(ac) && !ac.isInGroup(Group.BLOCKEDCERT);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateDisplay.templ b/src/org/cacert/gigi/pages/account/certs/CertificateDisplay.templ
new file mode 100644 (file)
index 0000000..0d559cd
--- /dev/null
@@ -0,0 +1,12 @@
+<a href='<?=$serial?>.crt'><?=_PEM encoded Certificate?></a>
+<? foreach($trustchain) { ?>
+ <?=_issued by?> <a href='<?=$link?>'><?=$name?></a>
+<? } ?><br/>
+<a href='<?=$serial?>.crt?chain'><?=_PEM encoded Certificate Cain?></a><br/>
+<a href='<?=$serial?>.crt?chain&noAnchor'><?=_PEM encoded Certificate Cain (Excluding Anchor)?></a><br/>
+<a href='<?=$serial?>.cer'><?=_DER encoded Certificate?></a><br/>
+<a href='<?=$serial?>.cer?install&chain'><?=_Install into browser.?></a><br/>
+<a href='<?=$serial?>.cer?install'><?=_Install into browser. (Chrome)?></a>. <?=_Please ensure that the intermediate certificates listed above are installed prior to installing the certificate.?><br/>
+<pre>
+<?=$cert?>
+</pre>
diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateIssueForm.java b/src/org/cacert/gigi/pages/account/certs/CertificateIssueForm.java
new file mode 100644 (file)
index 0000000..a3e4b80
--- /dev/null
@@ -0,0 +1,183 @@
+package org.cacert.gigi.pages.account.certs;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.SubjectAlternateName;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.CertificateValiditySelector;
+import org.cacert.gigi.output.HashAlgorithms;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.RandomToken;
+
+/**
+ * This class represents a form that is used for issuing certificates. This
+ * class uses "sun.security" and therefore needs "-XDignore.symbol.file"
+ */
+public class CertificateIssueForm extends Form {
+
+    private final static Template t = new Template(CertificateIssueForm.class.getResource("CertificateIssueForm.templ"));
+
+    private final static Template tIni = new Template(CertificateAdd.class.getResource("RequestCertificate.templ"));
+
+    private AuthorizationContext c;
+
+    private String spkacChallenge;
+
+    private boolean login;
+
+    public CertificateIssueForm(HttpServletRequest hsr) {
+        super(hsr);
+        c = LoginPage.getAuthorizationContext(hsr);
+        spkacChallenge = RandomToken.generateToken(16);
+    }
+
+    private Certificate result;
+
+    public Certificate getResult() {
+        return result;
+    }
+
+    private CertificateRequest cr;
+
+    CertificateValiditySelector issueDate = new CertificateValiditySelector();
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        String csr = req.getParameter("CSR");
+        String spkac = req.getParameter("SPKAC");
+        try {
+            try {
+                if (csr != null) {
+                    cr = new CertificateRequest(c, csr);
+                    cr.checkKeyStrength(out);
+                } else if (spkac != null) {
+                    cr = new CertificateRequest(c, spkac, spkacChallenge);
+                    cr.checkKeyStrength(out);
+                } else if (cr != null) {
+                    login = "1".equals(req.getParameter("login"));
+                    issueDate.update(req);
+                    GigiApiException error = new GigiApiException();
+
+                    try {
+                        cr.update(req.getParameter("CN"), req.getParameter("hash_alg"), req.getParameter("profile"), //
+                                req.getParameter("org"), req.getParameter("OU"), req.getParameter("SANs"), out, req);
+                    } catch (GigiApiException e) {
+                        error.mergeInto(e);
+                    }
+                    if (req.getParameter("tos_agree") == null) {
+                        error.mergeInto(new GigiApiException("You need to accept the ToS."));
+                    }
+                    Certificate result = null;
+                    try {
+                        result = cr.draft();
+                    } catch (GigiApiException e) {
+                        error.mergeInto(e);
+                    }
+                    if ( !error.isEmpty() || result == null) {
+                        error.format(out, Page.getLanguage(req));
+                        return false;
+                    }
+                    result.issue(issueDate.getFrom(), issueDate.getTo(), c.getActor()).waitFor(60000);
+                    this.result = result;
+                    return true;
+                } else {
+                    throw new GigiApiException("Error no action.");
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            } catch (IllegalArgumentException e) {
+                e.printStackTrace();
+                throw new GigiApiException("Certificate Request format is invalid.");
+            } catch (GeneralSecurityException e) {
+                e.printStackTrace();
+                throw new GigiApiException("Certificate Request format is invalid.");
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+        }
+        return false;
+    }
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        if (cr == null) {
+            HashMap<String, Object> vars2 = new HashMap<String, Object>(vars);
+            vars2.put("csrf", getCSRFToken());
+            vars2.put("csrf_name", getCsrfFieldName());
+            vars2.put("spkacChallenge", spkacChallenge);
+            tIni.output(out, l, vars2);
+            return;
+        } else {
+            super.output(out, l, vars);
+        }
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        HashMap<String, Object> vars2 = new HashMap<String, Object>(vars);
+        vars2.put("ToS", "<a href='/policy/TermsOfService.html'>ToS</a>");
+
+        StringBuffer content = new StringBuffer();
+        for (SubjectAlternateName SAN : cr.getSANs()) {
+            content.append(SAN.getType().toString().toLowerCase());
+            content.append(':');
+            content.append(SAN.getName());
+            content.append('\n');
+        }
+
+        vars2.put("CN", cr.getName());
+        if (c.getTarget() instanceof Organisation) {
+            vars2.put("orga", "true");
+            vars2.put("department", cr.getOu());
+        }
+        vars2.put("validity", issueDate);
+        vars2.put("emails", content.toString());
+        vars2.put("hashs", new HashAlgorithms(cr.getSelectedDigest()));
+        vars2.put("profiles", new IterableDataset() {
+
+            CertificateProfile[] cps = CertificateProfile.getAll();
+
+            int i = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                CertificateProfile cp;
+                do {
+                    if (i >= cps.length) {
+                        return false;
+                    }
+                    cp = cps[i];
+                    i++;
+                } while ( !cp.canBeIssuedBy(c.getTarget(), c.getActor()));
+
+                if (cp.getId() == cr.getProfile().getId()) {
+                    vars.put("selected", " selected");
+                } else {
+                    vars.put("selected", "");
+                }
+                vars.put("key", cp.getKeyName());
+                vars.put("name", cp.getVisibleName());
+                return true;
+            }
+        });
+
+        t.output(out, l, vars2);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateIssueForm.templ b/src/org/cacert/gigi/pages/account/certs/CertificateIssueForm.templ
new file mode 100644 (file)
index 0000000..7b64dc7
--- /dev/null
@@ -0,0 +1,98 @@
+<h3><?=_CAcert Certificate Acceptable Use Policy?></h3>
+<p><?=_I hereby represent that I am fully authorized by the owner of the information contained in the CSR sent to SomeCA Inc. to apply for an Digital Certificate for secure and authenticated electronic transactions. I understand that a digital certificate serves to identify the Subscriber for the purposes of electronic communication and that the management of the private keys associated with such certificates is the responsibility of the subscriber's technical staff and/or contractors.?></p>
+
+<p><?=_CAcert Inc.'s public certification services are governed by a CPS as amended from time to time which is incorporated into this Agreement by reference. The Subscriber will use the SSL Server Certificate in accordance with SomeCA Inc.'s CPS and supporting documentation published at?> <a href="http://www.cacert.org/cps.php">http://www.cacert.org/cps.php</a></p>
+
+<p><?=_If the Subscriber's name and/or domain name registration change the subscriber will immediately inform SomeCA Inc. who shall revoke the digital certificate. When the Digital Certificate expires or is revoked the company will permanently remove the certificate from the server on which it is installed and will not use it for any purpose thereafter. The person responsible for key management and security is fully authorized to install and utilize the certificate to represent this organization's electronic presence.?></p>
+
+<table class="table">
+  <thead>
+  <tr>
+    <th colspan="2" class="title"><?=_New Certificate?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td>
+    <label for='profile'><?=_Key type?></label>
+    </td>
+    <td>
+    <select name="profile" id='profile'>
+    <? foreach($profiles) { ?>
+      <option value="<?=$key?>"<?=$!selected?>><?=$name?></option>
+    <? } ?>
+    </select>
+    </td>
+  </tr>
+  <tr>
+    <td>
+    <label for='CN'><?=_Your name?></label>
+    </td>
+    <td><input class="form-control" type='text' id='CN' name='CN' value='<?=$CN?>'/></td>
+  </tr>
+  <tr>
+    <td>SANs</td>
+    <td align="left"><textarea class="form-control" rows='5' name='SANs' placeholder="dns:my.domain.example.com, dns:*.example.com, email:my.email@example.com (or newline separated)"><?=$emails?></textarea></td>
+  </tr>
+  <? if($orga) { ?>
+  <tr>
+    <td><?=_Departement?></td>
+    <td align="left"><input type='text' name='OU' value='<?=$department?>'/></td>
+  </tr>
+  <? } ?>
+  <tr class="expertoff">
+    <td class='check'>
+      <input type="checkbox" id="expertbox" name="expertbox"/>
+    </td>
+    <td align="left">
+      <label for="expertbox"><?=_Show advanced options?></label>
+    </td>
+  </tr>
+
+  <tr class="expert">
+       <td><?=_Hash algorithm for signing?></td>
+    <td class='radio'>
+      <? foreach($hashs) { ?>
+        <input type="radio" id="hash_alg_<?=$algorithm?>" name="hash_alg" value="<?=$algorithm?>"<?=$!checked?>/>
+        <label for="hash_alg_<?=$algorithm?>"><span class='name'><?=$name?></span><? if($info) { ?> <span class='addinfo'> <?=$info?></span><? } ?></label><div class='elements'></div>
+      <? } ?>
+    </td>
+  </tr>
+  <tr class="expert">
+    <td><?=_Valid period?></td>
+    <td>
+        <?=$validity?>
+    </td>
+  </tr>
+    <tr>
+    <td class='check'>
+      <input type="checkbox" id="tos_agree" name="tos_agree" />
+    </td>
+    <td align="left">
+      <label for="tos_agree"><strong><?=_I accept the Terms of Service ($!{ToS}).?> </strong><br />
+      <?=_Please note: You need to accept the ToS to proceed.?></label>
+    </td>
+  </tr>
+  <tr><td colspan='2'>&nbsp;</td></tr>
+
+  <tr>
+    <td class='check'>
+      <input type="checkbox" id="login" name="login" value="1" checked="checked" />
+    </td>
+    <td align="left">
+      <label for="login"><?=_Enable certificate login with this certificate?><br />
+      <?=_By allowing certificate login, this certificate can be used to login into this account at https://secure.cacert.org/ .?></label>
+    </td>
+  </tr>
+  <tr>
+    <td colspan="2">
+      <label for="description"><?=_Optional comment, only used in the certificate overview?></label><br />
+      <input class="form-control" type="text" id="description" name="description" maxlength="100" size="100" />
+    </td>
+  </tr>
+
+  <tr>
+    <td colspan="2"><input type="submit" name="process" value="<?=_Issue Certificate?>" /></td>
+  </tr>
+  </tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateModificationForm.java b/src/org/cacert/gigi/pages/account/certs/CertificateModificationForm.java
new file mode 100644 (file)
index 0000000..6553a77
--- /dev/null
@@ -0,0 +1,73 @@
+package org.cacert.gigi.pages.account.certs;
+
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Job;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.CertificateIterable;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+
+public class CertificateModificationForm extends Form {
+
+    CertificateOwner target;
+
+    final boolean withRevoked;
+
+    public CertificateModificationForm(HttpServletRequest hsr, boolean withRevoked) {
+        super(hsr);
+        this.withRevoked = withRevoked;
+        target = LoginPage.getAuthorizationContext(hsr).getTarget();
+    }
+
+    private static final Template certTable = new Template(CertificateIterable.class.getResource("CertificateTable.templ"));
+
+    private static final Template myTemplate = new Template(CertificateModificationForm.class.getResource("CertificateModificationForm.templ"));
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        String[] certs = req.getParameterValues("certs[]");
+        if (certs == null) {
+            // nothing to do
+            return false;
+        }
+        LinkedList<Job> revokes = new LinkedList<Job>();
+        for (String serial : certs) {
+            Certificate c = Certificate.getBySerial(serial);
+            if (c == null || c.getOwner() != target) {
+                continue;
+            }
+            revokes.add(c.revoke());
+        }
+        long start = System.currentTimeMillis();
+        for (Job job : revokes) {
+            try {
+                int toWait = (int) (60000 + start - System.currentTimeMillis());
+                if (toWait > 0) {
+                    job.waitFor(toWait);
+                } else {
+                    break; // canceled... waited too log
+                }
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        vars.put("certs", new CertificateIterable(target.getCertificates(withRevoked)));
+        vars.put("certTable", certTable);
+        myTemplate.output(out, l, vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateModificationForm.templ b/src/org/cacert/gigi/pages/account/certs/CertificateModificationForm.templ
new file mode 100644 (file)
index 0000000..585513d
--- /dev/null
@@ -0,0 +1,2 @@
+<?=$certTable?>
+<input type="submit" value="<?=_Revoke Certificates?>"/>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/account/certs/CertificateRequest.java b/src/org/cacert/gigi/pages/account/certs/CertificateRequest.java
new file mode 100644 (file)
index 0000000..aafd869
--- /dev/null
@@ -0,0 +1,563 @@
+package org.cacert.gigi.pages.account.certs;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.crypto.SPKAC;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.Certificate.SANType;
+import org.cacert.gigi.dbObjects.Certificate.SubjectAlternateName;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.CertificateProfile.PropertyTemplate;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.SprintfCommand;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.PEM;
+import org.cacert.gigi.util.RateLimit;
+
+import sun.security.pkcs.PKCS9Attribute;
+import sun.security.pkcs10.PKCS10;
+import sun.security.pkcs10.PKCS10Attribute;
+import sun.security.pkcs10.PKCS10Attributes;
+import sun.security.util.DerInputStream;
+import sun.security.util.DerValue;
+import sun.security.util.ObjectIdentifier;
+import sun.security.x509.AVA;
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.CertificateExtensions;
+import sun.security.x509.DNSName;
+import sun.security.x509.ExtendedKeyUsageExtension;
+import sun.security.x509.Extension;
+import sun.security.x509.GeneralName;
+import sun.security.x509.GeneralNameInterface;
+import sun.security.x509.GeneralNames;
+import sun.security.x509.PKIXExtensions;
+import sun.security.x509.RDN;
+import sun.security.x509.RFC822Name;
+import sun.security.x509.SubjectAlternativeNameExtension;
+import sun.security.x509.X500Name;
+
+public class CertificateRequest {
+
+    public static final String DEFAULT_CN = "CAcert WoT User";
+
+    public static final ObjectIdentifier OID_KEY_USAGE_SSL_SERVER = ObjectIdentifier.newInternal(new int[] {
+            1, 3, 6, 1, 5, 5, 7, 3, 1
+    });
+
+    public static final ObjectIdentifier OID_KEY_USAGE_SSL_CLIENT = ObjectIdentifier.newInternal(new int[] {
+            1, 3, 6, 1, 5, 5, 7, 3, 2
+    });
+
+    public static final ObjectIdentifier OID_KEY_USAGE_CODESIGN = ObjectIdentifier.newInternal(new int[] {
+            1, 3, 6, 1, 5, 5, 7, 3, 3
+    });
+
+    public static final ObjectIdentifier OID_KEY_USAGE_EMAIL_PROTECTION = ObjectIdentifier.newInternal(new int[] {
+            1, 3, 6, 1, 5, 5, 7, 3, 4
+    });
+
+    public static final ObjectIdentifier OID_KEY_USAGE_TIMESTAMP = ObjectIdentifier.newInternal(new int[] {
+            1, 3, 6, 1, 5, 5, 7, 3, 8
+    });
+
+    public static final ObjectIdentifier OID_KEY_USAGE_OCSP = ObjectIdentifier.newInternal(new int[] {
+            1, 3, 6, 1, 5, 5, 7, 3, 9
+    });
+
+    private CSRType csrType;
+
+    private final PublicKey pk;
+
+    private String csr;
+
+    public String name = DEFAULT_CN;
+
+    private Set<SubjectAlternateName> SANs;
+
+    private Digest selectedDigest = Digest.getDefault();
+
+    private CertificateProfile profile = CertificateProfile.getById(1);
+
+    private String ou = "";
+
+    private AuthorizationContext ctx;
+
+    private String pDNS, pMail;
+
+    public CertificateRequest(AuthorizationContext c, String csr) throws IOException, GeneralSecurityException, GigiApiException {
+        this(c, csr, (CertificateProfile) null);
+    }
+
+    public CertificateRequest(AuthorizationContext ctx, String csr, CertificateProfile cp) throws GeneralSecurityException, IOException, IOException {
+        this.ctx = ctx;
+        if (cp != null) {
+            profile = cp;
+        } else if (ctx.getActor().getAssurancePoints() > 50) {
+            profile = CertificateProfile.getByName("client-a");
+        }
+        byte[] data = PEM.decode("(NEW )?CERTIFICATE REQUEST", csr);
+        PKCS10 parsed = new PKCS10(data);
+        PKCS10Attributes atts = parsed.getAttributes();
+
+        TreeSet<SubjectAlternateName> SANs = new TreeSet<>();
+        for (RDN r : parsed.getSubjectName().rdns()) {
+            for (AVA a : r.avas()) {
+                if (a.getObjectIdentifier().equals((Object) PKCS9Attribute.EMAIL_ADDRESS_OID)) {
+                    SANs.add(new SubjectAlternateName(SANType.EMAIL, a.getValueString()));
+                } else if (a.getObjectIdentifier().equals((Object) X500Name.commonName_oid)) {
+                    String value = a.getValueString();
+                    if (value.contains(".") && !value.contains(" ")) {
+                        SANs.add(new SubjectAlternateName(SANType.DNS, value));
+                    } else {
+                        name = value;
+                    }
+                } else if (a.getObjectIdentifier().equals((Object) PKIXExtensions.SubjectAlternativeName_Id)) {
+                    // TODO? parse invalid SANs
+                }
+            }
+        }
+
+        for (PKCS10Attribute b : atts.getAttributes()) {
+
+            if ( !b.getAttributeId().equals((Object) PKCS9Attribute.EXTENSION_REQUEST_OID)) {
+                // unknown attrib
+                continue;
+            }
+
+            for (Extension c : ((CertificateExtensions) b.getAttributeValue()).getAllExtensions()) {
+                if (c instanceof SubjectAlternativeNameExtension) {
+
+                    SubjectAlternativeNameExtension san = (SubjectAlternativeNameExtension) c;
+                    GeneralNames obj = san.get(SubjectAlternativeNameExtension.SUBJECT_NAME);
+                    for (int i = 0; i < obj.size(); i++) {
+                        GeneralName generalName = obj.get(i);
+                        GeneralNameInterface peeled = generalName.getName();
+                        if (peeled instanceof DNSName) {
+                            SANs.add(new SubjectAlternateName(SANType.DNS, ((DNSName) peeled).getName()));
+                        } else if (peeled instanceof RFC822Name) {
+                            SANs.add(new SubjectAlternateName(SANType.EMAIL, ((RFC822Name) peeled).getName()));
+                        }
+                    }
+                } else if (c instanceof ExtendedKeyUsageExtension) {
+                    ExtendedKeyUsageExtension ekue = (ExtendedKeyUsageExtension) c;
+                    String appendix = "";
+                    if (ctx.getActor().getAssurancePoints() >= 50) {
+                        appendix = "-a";
+                    }
+                    for (String s : ekue.getExtendedKeyUsage()) {
+                        if (s.equals(OID_KEY_USAGE_SSL_SERVER.toString())) {
+                            // server
+                            profile = CertificateProfile.getByName("server" + appendix);
+                        } else if (s.equals(OID_KEY_USAGE_SSL_CLIENT.toString())) {
+                            // client
+                            profile = CertificateProfile.getByName("client" + appendix);
+                        } else if (s.equals(OID_KEY_USAGE_CODESIGN.toString())) {
+                            // code sign
+                        } else if (s.equals(OID_KEY_USAGE_EMAIL_PROTECTION.toString())) {
+                            // emailProtection
+                            profile = CertificateProfile.getByName("mail" + appendix);
+                        } else if (s.equals(OID_KEY_USAGE_TIMESTAMP.toString())) {
+                            // timestamp
+                        } else if (s.equals(OID_KEY_USAGE_OCSP.toString())) {
+                            // OCSP
+                        }
+                    }
+                } else {
+                    // Unknown requested extension
+                }
+            }
+
+        }
+        this.SANs = SANs;
+        pk = parsed.getSubjectPublicKeyInfo();
+        String sign = getSignatureAlgorithm(data);
+        guessDigest(sign);
+
+        this.csr = csr;
+        this.csrType = CSRType.CSR;
+    }
+
+    public CertificateRequest(AuthorizationContext ctx, String spkac, String spkacChallenge) throws IOException, GigiApiException, GeneralSecurityException {
+        this.ctx = ctx;
+        String cleanedSPKAC = spkac.replaceAll("[\r\n]", "");
+        byte[] data = Base64.getDecoder().decode(cleanedSPKAC);
+        SPKAC parsed = new SPKAC(data);
+        if ( !parsed.getChallenge().equals(spkacChallenge)) {
+            throw new GigiApiException("Challenge mismatch");
+        }
+        pk = parsed.getPubkey();
+        String sign = getSignatureAlgorithm(data);
+        guessDigest(sign);
+        this.SANs = new HashSet<>();
+        this.csr = "SPKAC=" + cleanedSPKAC;
+        this.csrType = CSRType.SPKAC;
+
+    }
+
+    private static String getSignatureAlgorithm(byte[] data) throws IOException {
+        DerInputStream in = new DerInputStream(data);
+        DerValue[] seq = in.getSequence(3);
+        return AlgorithmId.parse(seq[1]).getName();
+    }
+
+    private void guessDigest(String sign) {
+        if (sign.toLowerCase().startsWith("sha512")) {
+            selectedDigest = Digest.SHA512;
+        } else if (sign.toLowerCase().startsWith("sha384")) {
+            selectedDigest = Digest.SHA384;
+        }
+    }
+
+    public void checkKeyStrength(PrintWriter out) {
+        out.println("Type: " + pk.getAlgorithm() + "<br/>");
+        if (pk instanceof RSAPublicKey) {
+            out.println("Exponent: " + ((RSAPublicKey) pk).getPublicExponent() + "<br/>");
+            out.println("Length: " + ((RSAPublicKey) pk).getModulus().bitLength());
+        } else if (pk instanceof DSAPublicKey) {
+            DSAPublicKey dpk = (DSAPublicKey) pk;
+            out.println("Length: " + dpk.getY().bitLength() + "<br/>");
+            out.println(dpk.getParams());
+        } else if (pk instanceof ECPublicKey) {
+            ECPublicKey epk = (ECPublicKey) pk;
+            out.println("Length-x: " + epk.getW().getAffineX().bitLength() + "<br/>");
+            out.println("Length-y: " + epk.getW().getAffineY().bitLength() + "<br/>");
+            out.println(epk.getParams().getCurve());
+        }
+    }
+
+    private Set<SubjectAlternateName> parseSANBox(String SANs) {
+        String[] SANparts = SANs.split("[\r\n]+|, *");
+        Set<SubjectAlternateName> parsedNames = new LinkedHashSet<>();
+        for (String SANline : SANparts) {
+            String[] parts = SANline.split(":", 2);
+            if (parts.length == 1) {
+                if (parts[0].trim().equals("")) {
+                    continue;
+                }
+                if (parts[0].contains("@")) {
+                    parsedNames.add(new SubjectAlternateName(SANType.EMAIL, parts[0]));
+                } else {
+                    parsedNames.add(new SubjectAlternateName(SANType.DNS, parts[0]));
+                }
+                continue;
+            }
+            try {
+                SANType t = Certificate.SANType.valueOf(parts[0].toUpperCase().trim());
+                if (t == null) {
+                    continue;
+                }
+                parsedNames.add(new SubjectAlternateName(t, parts[1].trim()));
+            } catch (IllegalArgumentException e) {
+                // invalid enum type
+                continue;
+            }
+        }
+        return parsedNames;
+    }
+
+    public Set<SubjectAlternateName> getSANs() {
+        return SANs;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public synchronized String getOu() {
+        if (ctx.getTarget() instanceof Organisation) {
+            return ou;
+        }
+        throw new IllegalStateException();
+    }
+
+    public Digest getSelectedDigest() {
+        return selectedDigest;
+    }
+
+    public CertificateProfile getProfile() {
+        return profile;
+    }
+
+    public synchronized boolean update(String nameIn, String hashAlg, String profileStr, String newOrgStr, String ou, String SANsStr, PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        GigiApiException error = new GigiApiException();
+        this.name = nameIn;
+        if (hashAlg != null) {
+            selectedDigest = Digest.valueOf(hashAlg);
+        }
+        this.profile = CertificateProfile.getByName(profileStr);
+        if (ctx.getTarget() instanceof Organisation) {
+            this.ou = ou;
+        }
+
+        if ( !this.profile.canBeIssuedBy(ctx.getTarget(), ctx.getActor())) {
+            this.profile = CertificateProfile.getById(1);
+            error.mergeInto(new GigiApiException("Certificate Profile is invalid."));
+            throw error;
+        }
+
+        verifySANs(error, profile, parseSANBox(SANsStr), ctx.getTarget());
+
+        if ( !error.isEmpty()) {
+            throw error;
+        }
+        return true;
+    }
+
+    private void verifySANs(GigiApiException error, CertificateProfile p, Set<SubjectAlternateName> sANs2, CertificateOwner owner) {
+        Set<SubjectAlternateName> filteredSANs = new LinkedHashSet<>();
+        PropertyTemplate domainTemp = p.getTemplates().get("domain");
+        PropertyTemplate emailTemp = p.getTemplates().get("email");
+        pDNS = null;
+        pMail = null;
+        for (SubjectAlternateName san : sANs2) {
+            if (san.getType() == SANType.DNS) {
+                if (domainTemp != null && owner.isValidDomain(san.getName())) {
+                    if (pDNS != null && !domainTemp.isMultiple()) {
+                        // remove
+                    } else {
+                        if (pDNS == null) {
+                            pDNS = san.getName();
+                        }
+                        filteredSANs.add(san);
+                        continue;
+                    }
+                }
+            } else if (san.getType() == SANType.EMAIL) {
+                if (emailTemp != null && owner.isValidEmail(san.getName())) {
+                    if (pMail != null && !emailTemp.isMultiple()) {
+                        // remove
+                    } else {
+                        if (pMail == null) {
+                            pMail = san.getName();
+                        }
+                        filteredSANs.add(san);
+                        continue;
+                    }
+                }
+            }
+            error.mergeInto(new GigiApiException(SprintfCommand.createSimple(//
+                    "The requested Subject alternate name \"{0}\" has been removed.", san.getType().toString().toLowerCase() + ":" + san.getName())));
+        }
+        SANs = filteredSANs;
+    }
+
+    // domain email name name=WoTUser orga
+    public synchronized Certificate draft() throws GigiApiException {
+
+        GigiApiException error = new GigiApiException();
+
+        HashMap<String, String> subject = new HashMap<>();
+        PropertyTemplate domainTemp = profile.getTemplates().get("domain");
+        PropertyTemplate emailTemp = profile.getTemplates().get("email");
+        PropertyTemplate nameTemp = profile.getTemplates().get("name");
+        PropertyTemplate wotUserTemp = profile.getTemplates().get("name=WoTUser");
+        verifySANs(error, profile, SANs, ctx.getTarget());
+
+        // Ok, let's determine the CN
+        // the CN is
+        // 1. the user's "real name", iff the real name is to be included i.e.
+        // not empty (name), or to be forced to WOTUser
+
+        // 2. the user's "primary domain", iff "1." doesn't match and there is a
+        // primary domain. (domainTemp != null)
+
+        String verifiedCN = null;
+        if (ctx.getTarget() instanceof Organisation) {
+            if ( !name.equals("")) {
+                verifiedCN = name;
+            }
+        } else {
+            verifiedCN = verifyName(error, nameTemp, wotUserTemp, verifiedCN);
+        }
+        if (pDNS == null && domainTemp != null && domainTemp.isRequired()) {
+            error.mergeInto(new GigiApiException("Server Certificates require a DNS name."));
+        } else if (domainTemp != null && verifiedCN == null) {
+            // user may add domains
+            verifiedCN = pDNS;
+        }
+        if (verifiedCN != null) {
+            subject.put("CN", verifiedCN);
+        }
+
+        if (pMail != null) {
+            if (emailTemp != null) {
+                subject.put("EMAIL", pMail);
+            } else {
+                // verify SANs should prevent this
+                pMail = null;
+                error.mergeInto(new GigiApiException("You may not include an email in this certificate."));
+            }
+        } else {
+            if (emailTemp != null && emailTemp.isRequired()) {
+                error.mergeInto(new GigiApiException("You need to include an email in this certificate."));
+            }
+        }
+
+        if (ctx.getTarget() instanceof Organisation) {
+            Organisation org = (Organisation) ctx.getTarget();
+            subject.put("O", org.getName());
+            subject.put("C", org.getState());
+            subject.put("ST", org.getProvince());
+            subject.put("L", org.getCity());
+            if (ou != null) {
+                subject.put("OU", ou);
+            }
+        }
+        System.out.println(subject);
+        if ( !error.isEmpty()) {
+            throw error;
+        }
+        try {
+            if (RATE_LIMIT.isLimitExceeded(Integer.toString(ctx.getActor().getId()))) {
+                throw new GigiApiException("Rate Limit Exceeded");
+            }
+            return new Certificate(ctx.getTarget(), ctx.getActor(), subject, selectedDigest, //
+                    this.csr, this.csrType, profile, SANs.toArray(new SubjectAlternateName[SANs.size()]));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    // 100 per 10 minutes
+    public static final RateLimit RATE_LIMIT = new RateLimit(100, 10 * 60 * 1000);
+
+    private String verifyName(GigiApiException error, PropertyTemplate nameTemp, PropertyTemplate wotUserTemp, String verifiedCN) {
+        // real names,
+        // possible configurations: name {y,null,?}, name=WoTUser {y,null}
+        // semantics:
+        // y * -> real
+        // null y -> default
+        // null null -> null
+        // ? y -> real, default
+        // ? null -> real, default, null
+        boolean realIsOK = false;
+        boolean nullIsOK = false;
+        boolean defaultIsOK = false;
+        if (wotUserTemp != null && ( !wotUserTemp.isRequired() || wotUserTemp.isMultiple())) {
+            error.mergeInto(new GigiApiException("Internal configuration error detected."));
+        }
+        if (nameTemp != null && nameTemp.isRequired() && !nameTemp.isMultiple()) {
+            realIsOK = true;
+        } else if (nameTemp == null) {
+            defaultIsOK = wotUserTemp != null;
+            nullIsOK = !defaultIsOK;
+        } else if (nameTemp != null && !nameTemp.isRequired() && !nameTemp.isMultiple()) {
+            realIsOK = true;
+            defaultIsOK = true;
+            nullIsOK = wotUserTemp == null;
+        } else {
+            error.mergeInto(new GigiApiException("Internal configuration error detected."));
+        }
+        if (ctx.getTarget() instanceof User) {
+            User u = (User) ctx.getTarget();
+            if (name != null && u.isValidName(name)) {
+                if (realIsOK) {
+                    verifiedCN = name;
+                } else {
+                    error.mergeInto(new GigiApiException("Your real name is not allowed in this certificate."));
+                    if (defaultIsOK) {
+                        name = DEFAULT_CN;
+                    } else if (nullIsOK) {
+                        name = "";
+                    }
+                }
+            } else if (name != null && name.equals(DEFAULT_CN)) {
+                if (defaultIsOK) {
+                    verifiedCN = name;
+                } else {
+                    error.mergeInto(new GigiApiException("The default name is not allowed in this certificate."));
+                    if (nullIsOK) {
+                        name = "";
+                    } else if (realIsOK) {
+                        name = u.getName().toString();
+                    }
+                }
+            } else if (name == null || name.equals("")) {
+                if (nullIsOK) {
+                    verifiedCN = "";
+                } else {
+                    error.mergeInto(new GigiApiException("A name is required in this certificate."));
+                    if (defaultIsOK) {
+                        name = DEFAULT_CN;
+                    } else if (realIsOK) {
+                        name = u.getName().toString();
+                    }
+                }
+            } else {
+                error.mergeInto(new GigiApiException("The name you entered was invalid."));
+
+            }
+            if (wotUserTemp != null) {
+                if ( !wotUserTemp.isRequired() || wotUserTemp.isMultiple()) {
+                    error.mergeInto(new GigiApiException("Internal configuration error detected."));
+                }
+                if ( !name.equals(DEFAULT_CN)) {
+                    name = DEFAULT_CN;
+                    error.mergeInto(new GigiApiException("You may not change the name for this certificate type."));
+                } else {
+                    verifiedCN = DEFAULT_CN;
+                }
+
+            } else {
+                if (nameTemp != null) {
+                    if (name.equals("")) {
+                        if (nameTemp.isRequired()) {
+                            // nothing, but required
+                            name = DEFAULT_CN;
+                            error.mergeInto(new GigiApiException("No name entered, but one was required."));
+                        } else {
+                            // nothing and not required
+
+                        }
+                    } else if (u.isValidName(name)) {
+                        verifiedCN = name;
+                    } else {
+                        if (nameTemp.isRequired()) {
+                            error.mergeInto(new GigiApiException("The name entered, does not match the details in your account. You cannot issue certificates with this name. Enter a name that matches the one that has been assured in your account, because a name is required for this certificate type."));
+                        } else if (name.equals(DEFAULT_CN)) {
+                            verifiedCN = DEFAULT_CN;
+                        } else {
+                            name = DEFAULT_CN;
+                            error.mergeInto(new GigiApiException("The name entered, does not match the details in your account. You cannot issue certificates with this name. Enter a name that matches the one that has been assured in your account or keep the default name."));
+                        }
+                    }
+                } else {
+                    if ( !name.equals("")) {
+                        name = "";
+                        error.mergeInto(new GigiApiException("No real name is included in this certificate. The real name, you entered will be ignored."));
+                    }
+                }
+            }
+        } else {
+            if (realIsOK) {
+                verifiedCN = name;
+            } else {
+                verifiedCN = "";
+                name = "";
+                error.mergeInto(new GigiApiException("No real name is included in this certificate. The real name, you entered will be ignored."));
+            }
+        }
+
+        return verifiedCN;
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/certs/Certificates.java b/src/org/cacert/gigi/pages/account/certs/Certificates.java
new file mode 100644 (file)
index 0000000..d40bbac
--- /dev/null
@@ -0,0 +1,154 @@
+package org.cacert.gigi.pages.account.certs;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URLEncoder;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.CACertificate;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.HandlesMixedRequest;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.CertExporter;
+import org.cacert.gigi.util.PEM;
+
+public class Certificates extends Page implements HandlesMixedRequest {
+
+    private Template certDisplay = new Template(Certificates.class.getResource("CertificateDisplay.templ"));
+
+    public static final String PATH = "/account/certs";
+
+    static class TrustchainIterable implements IterableDataset {
+
+        CACertificate cert;
+
+        public TrustchainIterable(CACertificate cert) {
+            this.cert = cert;
+        }
+
+        @Override
+        public boolean next(Language l, Map<String, Object> vars) {
+            if (cert == null) {
+                return false;
+            }
+            vars.put("name", cert.getKeyname());
+            vars.put("link", cert.getLink());
+            if (cert.isSelfsigned()) {
+                cert = null;
+                return true;
+            }
+            cert = cert.getParent();
+            return true;
+        }
+
+    }
+
+    public Certificates() {
+        super("Certificates");
+    }
+
+    @Override
+    public boolean beforeTemplate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+
+        String pi = req.getPathInfo().substring(PATH.length());
+        if (pi.length() == 0) {
+            return false;
+        }
+        pi = pi.substring(1);
+        boolean crt = false;
+        boolean cer = false;
+        resp.setContentType("application/pkix-cert");
+        if (req.getParameter("install") != null) {
+            resp.setContentType("application/x-x509-user-cert");
+        }
+        if (pi.endsWith(".crt")) {
+            crt = true;
+            pi = pi.substring(0, pi.length() - 4);
+        } else if (pi.endsWith(".cer")) {
+            cer = true;
+            pi = pi.substring(0, pi.length() - 4);
+        }
+        String serial = pi;
+        try {
+            Certificate c = Certificate.getBySerial(serial);
+            if (c == null || LoginPage.getAuthorizationContext(req).getTarget().getId() != c.getOwner().getId()) {
+                resp.sendError(404);
+                return true;
+            }
+            if ( !crt && !cer) {
+                return false;
+            }
+            ServletOutputStream out = resp.getOutputStream();
+            boolean doChain = req.getParameter("chain") != null;
+            boolean includeAnchor = req.getParameter("noAnchor") == null;
+            if (crt) {
+                CertExporter.writeCertCrt(c, out, doChain, includeAnchor);
+            } else if (cer) {
+                CertExporter.writeCertCer(c, out, doChain, includeAnchor);
+            }
+        } catch (IllegalArgumentException e) {
+            resp.sendError(404);
+            return true;
+        } catch (GeneralSecurityException e) {
+            resp.sendError(404);
+            return true;
+        }
+
+        return true;
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (req.getQueryString() != null && !req.getQueryString().equals("") && !req.getQueryString().equals("withRevoked")) {
+            return;// Block actions by get parameters.
+        }
+        if ( !req.getPathInfo().equals(PATH)) {
+            resp.sendError(500);
+            return;
+        }
+        Form.getForm(req, CertificateModificationForm.class).submit(resp.getWriter(), req);
+        doGet(req, resp);
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PrintWriter out = resp.getWriter();
+        String pi = req.getPathInfo().substring(PATH.length());
+        if (pi.length() != 0) {
+            pi = pi.substring(1);
+
+            String serial = pi;
+            Certificate c = Certificate.getBySerial(serial);
+            if (c == null || LoginPage.getAuthorizationContext(req).getTarget().getId() != c.getOwner().getId()) {
+                resp.sendError(404);
+                return;
+            }
+            HashMap<String, Object> vars = new HashMap<>();
+            vars.put("serial", URLEncoder.encode(serial, "UTF-8"));
+            vars.put("trustchain", new TrustchainIterable(c.getParent()));
+            try {
+                vars.put("cert", PEM.encode("CERTIFICATE", c.cert().getEncoded()));
+            } catch (GeneralSecurityException e) {
+                e.printStackTrace();
+            }
+            certDisplay.output(out, getLanguage(req), vars);
+
+            return;
+        }
+
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        new CertificateModificationForm(req, req.getParameter("withRevoked") != null).output(out, getLanguage(req), vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/account/certs/RequestCertificate.templ b/src/org/cacert/gigi/pages/account/certs/RequestCertificate.templ
new file mode 100644 (file)
index 0000000..0711cdd
--- /dev/null
@@ -0,0 +1,47 @@
+<form method="post">
+<table class="table">
+  <thead>
+  <tr>
+    <th colspan="2" class="title"><?=_New Certificate from CSR?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_I have a CSR! Paste it here:?><br/>Don't know, what as CSR is and how to create one? Take a look in the <a href="https://wiki.cacert.org/FAQ/CSR">Wiki</a>!</td>
+    <td>
+      <textarea class="form-control" name="CSR" class="csr"></textarea>
+    </td>
+  </tr>
+  <tr>
+    <td colspan="2">
+      <input type="submit" name="process" value="<?=_Next?>" />
+      <input type='hidden' name='<?=$csrf_name?>' value='<?=$csrf?>'>
+    </td>
+  </tr>
+  </tbody>
+</table>
+</form>
+<br>
+<form method="post">
+<table class="table">
+  <thead>
+  <tr>
+    <th colspan="2" class="title"><?=_New Certificate from newly generatey Key (SPKAC)?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_I do not have a CSR.?></td>
+    <td align="left">
+     <keygen name="SPKAC" challenge="<?=$spkacChallenge?>"/>
+    </td>
+  </tr>
+  <tr>
+    <td colspan="2">
+     <input type="submit" name="process" value="<?=_Next?>" />
+     <input type='hidden' name='<?=$csrf_name?>' value='<?=$csrf?>'>
+    </td>
+  </tr>
+  </tbody>
+</table>
+</form>
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainAddForm.java b/src/org/cacert/gigi/pages/account/domain/DomainAddForm.java
new file mode 100644 (file)
index 0000000..a0e5685
--- /dev/null
@@ -0,0 +1,62 @@
+package org.cacert.gigi.pages.account.domain;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+
+public class DomainAddForm extends Form {
+
+    private static final Template t = new Template(DomainManagementForm.class.getResource("DomainAddForm.templ"));
+
+    private User target;
+
+    PingConfigForm pcf;
+
+    public DomainAddForm(HttpServletRequest hsr, User target) throws GigiApiException {
+        super(hsr);
+        this.target = target;
+        pcf = new PingConfigForm(hsr, null);
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        try {
+            String parameter = req.getParameter("newdomain");
+            if (parameter.trim().isEmpty()) {
+                throw new GigiApiException("No domain inserted.");
+            }
+            Domain d = new Domain(target, target, parameter);
+            pcf.setTarget(d);
+            pcf.submit(out, req);
+            return true;
+        } catch (NumberFormatException e) {
+            new GigiApiException("A number could not be parsed").format(out, Page.getLanguage(req));
+            return false;
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+            return false;
+        }
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        vars.put("pingconfig", new Outputable() {
+
+            @Override
+            public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+                pcf.outputEmbeddableContent(out, l, vars);
+            }
+        });
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainAddForm.templ b/src/org/cacert/gigi/pages/account/domain/DomainAddForm.templ
new file mode 100644 (file)
index 0000000..d16aa3e
--- /dev/null
@@ -0,0 +1,17 @@
+<table class="table">
+  <thead>
+  <tr>
+    <th colspan="2" class="title"><?=_Add Domain?></th>
+  </tr>
+  </thead>
+<tbody>
+  <tr>
+    <td><?=_Domain?> </td>
+    <td><input class="form-control" type="text" name="newdomain" value=""> (<?=_In the following:?> <span class='exampleDomainPlace'>example.org</span>)</td>
+  </tr>
+  <?=$pingconfig?>
+  <tr>
+    <td colspan="2"><input type="submit" name="adddomain" value="<?=_I own or am authorised to control this domain?>"></td>
+  </tr>
+</tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainManagementForm.java b/src/org/cacert/gigi/pages/account/domain/DomainManagementForm.java
new file mode 100644 (file)
index 0000000..4e80123
--- /dev/null
@@ -0,0 +1,80 @@
+package org.cacert.gigi.pages.account.domain;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+
+public class DomainManagementForm extends Form {
+
+    private static final Template t = new Template(DomainManagementForm.class.getResource("DomainManagementForm.templ"));
+
+    private CertificateOwner target;
+
+    private boolean foreign;
+
+    public DomainManagementForm(HttpServletRequest hsr, CertificateOwner target, boolean foreign) {
+        super(hsr);
+        this.target = target;
+        this.foreign = foreign;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        try {
+            String dels = req.getParameter("delete");
+
+            int delId = Integer.parseInt(dels);
+            Domain d = Domain.getById(delId);
+            if (d != null && d.getOwner() == target) {
+                d.delete();
+            } else {
+                throw new GigiApiException("Domain was not found.");
+            }
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+            return false;
+        }
+        return true;
+    }
+
+    public CertificateOwner getTarget() {
+        return target;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        final Domain[] doms = target.getDomains();
+        IterableDataset dts = new IterableDataset() {
+
+            private int point = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (point >= doms.length) {
+                    return false;
+                }
+                Domain domain = doms[point];
+                vars.put("id", domain.getId());
+                if ( !foreign) {
+                    vars.put("domainhref", DomainOverview.PATH + domain.getId());
+                }
+                vars.put("domain", domain.getSuffix());
+                vars.put("status", l.getTranslation(domain.isVerified() ? "verified" : "not verified"));
+                point++;
+                return true;
+            }
+        };
+        vars.put("domains", dts);
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainManagementForm.templ b/src/org/cacert/gigi/pages/account/domain/DomainManagementForm.templ
new file mode 100644 (file)
index 0000000..6dbbdb4
--- /dev/null
@@ -0,0 +1,18 @@
+<table class="table">
+  <tbody><tr>
+    <th colspan="3">Domains</th>
+  </tr>
+  <tr>
+    <td></td>
+    <td><?=_Status?></td>
+    <td><?=_Address?></td>
+
+  </tr>
+  <? foreach($domains) { ?>
+  <tr>
+       <td><button class="btn btn-danger" type="submit" name="delete" value="<?=$id?>">Delete</button></td>
+       <td><?=$status?></td>
+       <td><? if($domainhref) { ?><a href='<?=$domainhref?>'><?=$domain?><? } else { ?><?=$domain?><? } ?></a></td>
+  </tr>
+  <? } ?>
+</tbody></table>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainOverview.java b/src/org/cacert/gigi/pages/account/domain/DomainOverview.java
new file mode 100644 (file)
index 0000000..9c37921
--- /dev/null
@@ -0,0 +1,100 @@
+package org.cacert.gigi.pages.account.domain;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+
+public class DomainOverview extends Page {
+
+    public static final String PATH = "/account/domains/";
+
+    public DomainOverview() {
+        super("Domains");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        CertificateOwner u = LoginPage.getAuthorizationContext(req).getTarget();
+        String pi = req.getPathInfo();
+        if (pi.length() - PATH.length() > 0) {
+            int i = Integer.parseInt(pi.substring(PATH.length()));
+            Domain d;
+            try {
+                d = Domain.getById(i);
+            } catch (IllegalArgumentException e) {
+                resp.getWriter().println(getLanguage(req).getTranslation("Access denied"));
+                return;
+            }
+            if (u.getId() != d.getOwner().getId()) {
+                resp.getWriter().println(getLanguage(req).getTranslation("Access denied"));
+                return;
+            }
+            new DomainPinglogForm(req, d).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+            try {
+                new PingConfigForm(req, d).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+            } catch (GigiApiException e) {
+                e.format(resp.getWriter(), getLanguage(req));
+            }
+            return;
+
+        }
+        try {
+            DomainManagementForm domMan = new DomainManagementForm(req, u, false);
+            HashMap<String, Object> vars = new HashMap<>();
+            vars.put("domainman", domMan);
+            if (u instanceof User) {
+                DomainAddForm domAdd = new DomainAddForm(req, (User) u);
+                vars.put("domainadd", domAdd);
+            }
+            getDefaultTemplate().output(resp.getWriter(), getLanguage(req), vars);
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        String pi = req.getPathInfo();
+        if (pi.length() - PATH.length() > 0) {
+            try {
+                if (req.getParameter("configId") != null) {
+                    if ( !Form.getForm(req, DomainPinglogForm.class).submit(resp.getWriter(), req)) {
+                        // error?
+                    }
+
+                } else {
+                    if ( !Form.getForm(req, PingConfigForm.class).submit(resp.getWriter(), req)) {
+
+                    }
+                }
+            } catch (GigiApiException e) {
+                e.format(resp.getWriter(), getLanguage(req));
+                return;
+            }
+
+            resp.sendRedirect(pi);
+        }
+        if (req.getParameter("adddomain") != null) {
+            DomainAddForm f = Form.getForm(req, DomainAddForm.class);
+            if (f.submit(resp.getWriter(), req)) {
+                resp.sendRedirect(PATH);
+            }
+        } else if (req.getParameter("domdel") != null) {
+            DomainManagementForm f = Form.getForm(req, DomainManagementForm.class);
+            if (f.submit(resp.getWriter(), req)) {
+                resp.sendRedirect(PATH);
+            }
+        }
+        super.doPost(req, resp);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainOverview.templ b/src/org/cacert/gigi/pages/account/domain/DomainOverview.templ
new file mode 100644 (file)
index 0000000..e5b8bbe
--- /dev/null
@@ -0,0 +1,16 @@
+<?=$domainman?>
+<h2><?=_Add Domain?></h2>
+<? if($domainadd) { ?>
+<p>
+<?=_Please Note: You only need to enter the main part of your domain, eg. mydomain.com rather then www.mydomain.com. Once you have verified your domain you are able to enter any sub-domain, such as www.mydomain.com or www.this.is.mydomain.com as the system checks from right to left, rather then specific hostnames when you upload a CSR to the system.?>
+</p>
+<p>
+<?=_You need to have at least two successful checks to have your domain verified.?>
+</p>
+<?=$domainadd?>
+<p>
+<?=_Currently we only issue certificates for Punycode domains if the person requesting them has code signing attributes attached to their account, as these have potentially slightly higher security risk.?>
+</p>
+<? } else { ?>
+Please contact your Organisation Assurer to add a domain.
+<? } ?>
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainPinglogForm.java b/src/org/cacert/gigi/pages/account/domain/DomainPinglogForm.java
new file mode 100644 (file)
index 0000000..954c573
--- /dev/null
@@ -0,0 +1,87 @@
+package org.cacert.gigi.pages.account.domain;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.DomainPingConfiguration;
+import org.cacert.gigi.dbObjects.DomainPingExecution;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+
+public class DomainPinglogForm extends Form {
+
+    static Template t = new Template(DomainPinglogForm.class.getResource("DomainPinglogForm.templ"));
+
+    Domain target;
+
+    public DomainPinglogForm(HttpServletRequest hsr, Domain target) {
+        super(hsr);
+        this.target = target;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        CertificateOwner u = LoginPage.getAuthorizationContext(req).getTarget();
+
+        int i = Integer.parseInt(req.getPathInfo().substring(DomainOverview.PATH.length()));
+        Domain d = Domain.getById(i);
+        if (u.getId() != d.getOwner().getId()) {
+            return false;
+        }
+        int reping = Integer.parseInt(req.getParameter("configId"));
+        DomainPingConfiguration dpc = DomainPingConfiguration.getById(reping);
+        if (dpc.getTarget() != d) {
+            return false;
+        }
+        dpc.requestReping();
+        return true;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        final DomainPingExecution[] pings;
+        try {
+            pings = target.getPings();
+        } catch (GigiApiException e) {
+            e.format(out, l);
+            return;
+        }
+        vars.put("domainname", target.getSuffix());
+        vars.put("pings", new IterableDataset() {
+
+            int counter = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (counter >= pings.length) {
+                    return false;
+                }
+                vars.put("state", pings[counter].getState());
+                vars.put("type", pings[counter].getType());
+                vars.put("config", pings[counter].getInfo());
+                vars.put("date", pings[counter].getDate());
+                String ping3 = pings[counter].getResult();
+                if (ping3 == null) {
+                    vars.put("result", "");
+                } else {
+                    vars.put("result", ping3);
+                }
+                DomainPingConfiguration dpc = pings[counter].getConfig();
+                if (dpc != null) {
+                    vars.put("configId", Integer.toString(dpc.getId()));
+                }
+                counter++;
+                return true;
+            }
+        });
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/domain/DomainPinglogForm.templ b/src/org/cacert/gigi/pages/account/domain/DomainPinglogForm.templ
new file mode 100644 (file)
index 0000000..19c078a
--- /dev/null
@@ -0,0 +1,20 @@
+<h2><?=_Ping log for '${domainname}'?></h2>
+<table class="table">
+<tr><th><?=_Type?></th>
+<th><?=_State?></th>
+<th><?=_Config?></th>
+<th><?=_Result?></th>
+<th><?=_Execution Date?></th>
+<th><?=_Reping?></th></tr>
+<? foreach($pings) { ?>
+<tr>
+<td><?=$type?></td>
+<td><?=$state?></td>
+<td><?=$config?></td>
+<td><?=$result?></td>
+<td><?=$date?></td>
+<td><button name='configId' class='form-control' value="<?=$configId?>"><?=_Re-execute?></button></td>
+</tr>
+<?}?>
+</table>
+
diff --git a/src/org/cacert/gigi/pages/account/domain/PingConfigForm.java b/src/org/cacert/gigi/pages/account/domain/PingConfigForm.java
new file mode 100644 (file)
index 0000000..1810b7a
--- /dev/null
@@ -0,0 +1,221 @@
+package org.cacert.gigi.pages.account.domain;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.Gigi;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.DomainPingConfiguration;
+import org.cacert.gigi.dbObjects.DomainPingType;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.ping.SSLPinger;
+import org.cacert.gigi.util.RandomToken;
+
+public class PingConfigForm extends Form {
+
+    public enum SSLType {
+        DIRECT, XMPP, XMPP_SERVER, SMTP, IMAP;
+
+        @Override
+        public String toString() {
+            return super.toString().toLowerCase();
+        }
+    }
+
+    private Domain target;
+
+    private String tokenName = RandomToken.generateToken(8);
+
+    private String tokenValue = RandomToken.generateToken(16);
+
+    private static final int MAX_SSL_TESTS = 4;
+
+    public static final String[] AUTHORATIVE_EMAILS = new String[] {
+            "root", "hostmaster", "postmaster", "admin", "webmaster"
+    };
+
+    private int selectedMail = -1;
+
+    private boolean doMail, doDNS, doHTTP, doSSL;
+
+    private int[] ports = new int[MAX_SSL_TESTS];
+
+    private SSLType[] sslTypes = new SSLType[MAX_SSL_TESTS];
+
+    private final Template t = new Template(PingConfigForm.class.getResource("PingConfigForm.templ"));
+
+    public PingConfigForm(HttpServletRequest hsr, Domain target) throws GigiApiException {
+        super(hsr);
+        this.target = target;
+        if (target == null) {
+            return;
+        }
+        List<DomainPingConfiguration> configs = target.getConfiguredPings();
+        int portpos = 0;
+        for (DomainPingConfiguration dpc : configs) {
+            switch (dpc.getType()) {
+            case EMAIL:
+                doMail = true;
+                for (int i = 0; i < AUTHORATIVE_EMAILS.length; i++) {
+                    if (AUTHORATIVE_EMAILS[i].equals(dpc.getInfo())) {
+                        selectedMail = i;
+                    }
+                }
+                break;
+            case DNS: {
+                doDNS = true;
+                String[] parts = dpc.getInfo().split(":");
+                tokenName = parts[0];
+                tokenValue = parts[1];
+                break;
+            }
+            case HTTP: {
+                doHTTP = true;
+                String[] parts = dpc.getInfo().split(":");
+                tokenName = parts[0];
+                tokenValue = parts[1];
+                break;
+            }
+            case SSL: {
+                doSSL = true;
+                String[] parts = dpc.getInfo().split(":");
+                tokenName = parts[0];
+                tokenValue = parts[1];
+                ports[portpos] = Integer.parseInt(parts[2]);
+                if (parts.length == 4) {
+                    sslTypes[portpos] = SSLType.valueOf(parts[3].toUpperCase());
+                } else {
+                    sslTypes[portpos] = SSLType.DIRECT;
+                }
+                portpos++;
+                break;
+            }
+            }
+        }
+    }
+
+    public void setTarget(Domain target) {
+        this.target = target;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        target.clearPings();
+        if (req.getParameter("emailType") != null && req.getParameter("email") != null) {
+            try {
+                String mail = AUTHORATIVE_EMAILS[Integer.parseInt(req.getParameter("email"))];
+                target.addPing(DomainPingType.EMAIL, mail);
+            } catch (NumberFormatException e) {
+                throw new GigiApiException("A email address is required");
+            }
+        }
+        if (req.getParameter("DNSType") != null) {
+            target.addPing(DomainPingType.DNS, tokenName + ":" + tokenValue);
+        }
+        if (req.getParameter("HTTPType") != null) {
+            target.addPing(DomainPingType.HTTP, tokenName + ":" + tokenValue);
+        }
+        if (req.getParameter("SSLType") != null) {
+            List<String> types = Arrays.asList(SSLPinger.TYPES);
+            for (int i = 0; i < MAX_SSL_TESTS; i++) {
+                String type = req.getParameter("ssl-type-" + i);
+                String port = req.getParameter("ssl-port-" + i);
+                if (type == null || port == null || port.equals("")) {
+                    continue;
+                }
+                int portInt = Integer.parseInt(port);
+                if ("direct".equals(type)) {
+                    target.addPing(DomainPingType.SSL, tokenName + ":" + tokenValue + ":" + port);
+                } else if (types.contains(type)) {
+                    target.addPing(DomainPingType.SSL, tokenName + ":" + tokenValue + ":" + portInt + ":" + type);
+                }
+
+            }
+        }
+        Gigi.notifyPinger(null);
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.print("<table class=\"wrapper dataTable\"><tbody>");
+        outputEmbeddableContent(out, l, vars);
+        out.print("<tr><td></td><td><input type=\"submit\" value=\"Update\"/></td></tbody></table>");
+    }
+
+    protected void outputEmbeddableContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        vars.put("tokenName", tokenName);
+        vars.put("tokenValue", tokenValue);
+        vars.put("authEmails", new IterableDataset() {
+
+            int i = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (i >= AUTHORATIVE_EMAILS.length) {
+                    return false;
+                }
+                vars.put("i", i);
+                vars.put("email", AUTHORATIVE_EMAILS[i]);
+                if (i == selectedMail) {
+                    vars.put("checked", " checked=\"checked\"");
+                } else {
+                    vars.put("checked", "");
+                }
+
+                i++;
+                return true;
+            }
+        });
+        vars.put("mail", doMail ? " checked=\"checked\"" : "");
+        vars.put("dns", doDNS ? " checked=\"checked\"" : "");
+        vars.put("http", doHTTP ? " checked=\"checked\"" : "");
+        vars.put("ssl", doSSL ? " checked=\"checked\"" : "");
+        vars.put("ssl-services", new IterableDataset() {
+
+            int counter = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (counter >= MAX_SSL_TESTS) {
+                    return false;
+                }
+                vars.put("i", counter);
+                vars.put("port", ports[counter] == 0 ? "" : Integer.toString(ports[counter]));
+                final SSLType selectedType = sslTypes[counter];
+                vars.put("ssl-types", new IterableDataset() {
+
+                    int i = 0;
+
+                    SSLType[] type = SSLType.values();
+
+                    @Override
+                    public boolean next(Language l, Map<String, Object> vars) {
+                        if (i >= type.length) {
+                            return false;
+                        }
+                        vars.put("name", type[i].toString());
+                        if (selectedType == type[i]) {
+                            vars.put("selected", " selected=\"selected\"");
+                        } else {
+                            vars.put("selected", "");
+                        }
+                        i++;
+                        return true;
+                    }
+                });
+                counter++;
+                return true;
+            }
+        });
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/domain/PingConfigForm.templ b/src/org/cacert/gigi/pages/account/domain/PingConfigForm.templ
new file mode 100644 (file)
index 0000000..89ba9e0
--- /dev/null
@@ -0,0 +1,63 @@
+  <tr><th></th><th><?=_Verification mechanisms?></th></tr>
+
+  <tr>
+    <td class='domainPinglogFirstCell'><input type="checkbox" name="emailType" value="y"<?=$!mail?>></td>
+    <td><?=_Verify by sending an email to authoritative email addresses?> </td>
+  </tr>
+  <tr>
+    <td></td>
+    <td class='radio'>
+        <?=_Select the destination mail address:?><br/>
+        <? foreach($authEmails) { ?>
+        <input type="radio" id="email_<?=$i?>" name="email" value="<?=$i?>"<?=$!checked?>/>
+        <label for="email_<?=$i?>"><span class='name'><?=$email?>@<span class='exampleDomain'>example.org</span></span></label><div class='elements'></div>
+        <? } ?>
+       </td>
+  </tr>
+
+  <tr>
+    <td><input type="checkbox" name="DNSType" value="y"<?=$!dns?>></td>
+    <td><?=_Verify by reading DNS-TXT entries?> </td>
+  </tr>
+  <tr>
+    <td></td>
+    <td>
+        <?=_Please insert the following DNS TXT entry into the Zone-file of your domain:?><br/>
+        <pre>
+        <?=$tokenName?>._cacert._auth IN TXT <?=$tokenValue?>
+        </pre>
+    </td>
+  </tr>
+
+  <tr>
+    <td><input type="checkbox" name="HTTPType" value="y"<?=$!http?>></td>
+    <td><?=_Verify by reading HTTP-content?> </td>
+  </tr>
+  <tr>
+    <td></td>
+    <td>
+        <?=_Please make the following content available under ?><pre class='string'>http://<span class='exampleDomain'>example.org</span>/cacert-<?=$tokenName?>.txt</pre><br/>
+        <pre><?=$tokenValue?></pre>
+    </td>
+  </tr>
+
+  <tr>
+    <td><input type="checkbox" name="SSLType" value="y"<?=$!ssl?>></td>
+    <td><?=_Verify by searching for installed certificate.?> </td>
+  </tr>
+  <tr>
+    <td></td>
+    <td>
+        <?=_Please list up to four services using your certificate. You need to have one of them up and using a valid SomeCA certificate or a specific self-signed certificate in order to pass this test?>:
+        <?=_The self-signed certificate needs to contain your domain as CN and ${tokenValue} as organization unit, with -subj "/CN=<domain>/OU=${tokenValue}"?>:
+        <table>
+        <? foreach($ssl-services){ ?>
+        <tr><td><select name='ssl-type-<?=$i?>'>
+               <?foreach($ssl-types){ ?>
+          <option<?=$!selected?>><?=$name?></option><? } ?></select>
+        </td><td>Port: <input type='text' name='ssl-port-<?=$i?>' value='<?=$port?>'></td></tr>
+        <? } ?>
+        </table>
+    </td>
+  </tr>
+  
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/account/mail/MailAddForm.java b/src/org/cacert/gigi/pages/account/mail/MailAddForm.java
new file mode 100644 (file)
index 0000000..6a2bb2c
--- /dev/null
@@ -0,0 +1,53 @@
+package org.cacert.gigi.pages.account.mail;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+
+public class MailAddForm extends Form {
+
+    private static Template t;
+
+    private String mail;
+    static {
+        t = new Template(MailAddForm.class.getResource("MailAddForm.templ"));
+    }
+
+    private User target;
+
+    public MailAddForm(HttpServletRequest hsr, User target) {
+        super(hsr);
+        this.target = target;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        String formMail = req.getParameter("newemail");
+        mail = formMail;
+        try {
+            new EmailAddress(target, mail, Page.getLanguage(req).getLocale());
+        } catch (IllegalArgumentException e) {
+            out.println("<div class='formError'>Error: Invalid address!</div>");
+            return false;
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        t.output(out, l, vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/account/mail/MailAddForm.templ b/src/org/cacert/gigi/pages/account/mail/MailAddForm.templ
new file mode 100644 (file)
index 0000000..23d98c4
--- /dev/null
@@ -0,0 +1,9 @@
+<table class="table">
+  <tr>
+    <td><?=_Email Addresses?> </td>
+    <td><input class="form-control" type="text" name="newemail" value=""></td>
+  </tr>
+  <tr>
+    <td colspan="2"><input type="submit" name="addmail" value="<?=_I own or am authorised to control this email address?>"></td>
+  </tr>
+</table>
diff --git a/src/org/cacert/gigi/pages/account/mail/MailManagementForm.java b/src/org/cacert/gigi/pages/account/mail/MailManagementForm.java
new file mode 100644 (file)
index 0000000..cbf1439
--- /dev/null
@@ -0,0 +1,98 @@
+package org.cacert.gigi.pages.account.mail;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+
+public class MailManagementForm extends Form {
+
+    private static Template t;
+
+    private User target;
+    static {
+        t = new Template(MailAddForm.class.getResource("MailManagementForm.templ"));
+    }
+
+    public MailManagementForm(HttpServletRequest hsr, User target) {
+        super(hsr);
+        this.target = target;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        try {
+            String d;
+            if ((d = req.getParameter("default")) != null) {
+                target.updateDefaultEmail(EmailAddress.getById(Integer.parseInt(d)));
+            } else if ((d = req.getParameter("delete")) != null) {
+                target.deleteEmail(EmailAddress.getById(Integer.parseInt(d)));
+            } else if ((d = req.getParameter("reping")) != null) {
+                EmailAddress.getById(Integer.parseInt(d)).requestReping(Page.getLanguage(req));
+            }
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+            return false;
+        } catch (IOException e1) {
+            new GigiApiException("Error while doing reping.").format(out, Page.getLanguage(req));
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        final EmailAddress[] emails = target.getEmails();
+        IterableDataset ds = new IterableDataset() {
+
+            private int point = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (point >= emails.length) {
+                    return false;
+                }
+                EmailAddress emailAddress = emails[point];
+                int mailID = emailAddress.getId();
+                vars.put("id", mailID);
+                if (emailAddress.getAddress().equals(target.getEmail())) {
+                    vars.put("default", " disabled");
+                    vars.put("deletable", " disabled");
+                } else {
+                    vars.put("deletable", "");
+                    vars.put("default", "");
+                }
+                if (emailAddress.isVerified()) {
+                    vars.put("verification", l.getTranslation("Verified"));
+                } else {
+                    // only verified emails may become the default email
+                    // address.
+                    vars.put("default", " disabled");
+                    vars.put("verification", l.getTranslation("Unverified"));
+                }
+                vars.put("last_verification", emailAddress.getLastPing(true));
+                if (target.getEmail().equals(emailAddress.getAddress())) {
+                    vars.put("delete", "N/A");
+                } else {
+                    vars.put("delete", "<input type=\"checkbox\" name=\"delid[]\" value=\"" + mailID + "\"/>");
+                }
+                vars.put("address", emailAddress.getAddress());
+                point++;
+                return true;
+            }
+
+        };
+        vars.put("emails", ds);
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/mail/MailManagementForm.templ b/src/org/cacert/gigi/pages/account/mail/MailManagementForm.templ
new file mode 100644 (file)
index 0000000..25c056d
--- /dev/null
@@ -0,0 +1,27 @@
+<table class="table">
+  <thead>
+  <tr>
+    <th colspan="4"><?=_Email Accounts?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_Default?></td>
+    <td><?=_Status?></td>
+    <td><?=_Last successful verification?></td>
+    <td><?=_Address?></td>
+    <td><?=_Delete?></td>
+    <td><?=_Request reping?></td>
+  </tr>
+ <? foreach($emails) {?>
+       <tr>
+               <td><button class="btn btn-primary" type="submit" name="default" value="<?=$id?>"<?=$default?>><?=_Set as Default?></button></td>
+               <td><?=$verification?></td>
+               <td><?=$last_verification?></td>
+               <td><?=$address?></td>
+               <td><button class="btn btn-danger" type="submit" name="delete" value="<?=$id?>"<?=$deletable?>><?=_Delete?></button></td>
+               <td><button class="btn btn-primary" type="submit" name="reping" value="<?=$id?>"><?=_Request reping?></button></td>
+       </tr>
+ <? } ?>
+  </tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/account/mail/MailOverview.java b/src/org/cacert/gigi/pages/account/mail/MailOverview.java
new file mode 100644 (file)
index 0000000..da3befd
--- /dev/null
@@ -0,0 +1,55 @@
+package org.cacert.gigi.pages.account.mail;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class MailOverview extends Page {
+
+    public static final String DEFAULT_PATH = "/account/mails";
+
+    public MailOverview() {
+        super("Email addresses");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        final User us = getUser(req);
+        Language lang = Page.getLanguage(req);
+        HashMap<String, Object> vars = new HashMap<>();
+        vars.put("addForm", new MailAddForm(req, us));
+        vars.put("manForm", new MailManagementForm(req, us));
+        getDefaultTemplate().output(resp.getWriter(), lang, vars);
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PrintWriter out = resp.getWriter();
+        if (req.getParameter("addmail") != null) {
+            MailAddForm f = Form.getForm(req, MailAddForm.class);
+            if (f.submit(out, req)) {
+                resp.sendRedirect(MailOverview.DEFAULT_PATH);
+            }
+        } else {
+            MailManagementForm f = Form.getForm(req, MailManagementForm.class);
+            if (f.submit(out, req)) {
+                resp.sendRedirect(MailOverview.DEFAULT_PATH);
+            }
+        }
+        super.doPost(req, resp);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.getTarget() instanceof User;
+    }
+}
diff --git a/src/org/cacert/gigi/pages/account/mail/MailOverview.templ b/src/org/cacert/gigi/pages/account/mail/MailOverview.templ
new file mode 100644 (file)
index 0000000..487d052
--- /dev/null
@@ -0,0 +1,20 @@
+<div class="panel panel-default">
+  <!-- Default panel contents -->
+  <div class="panel-heading"><?=_Manage email accounts?></div>
+  <div class="panel-body">
+   <p>
+<?=_Please Note: You can not set an unverified account as a default account, and you can not remove a default account. To remove the default account you must set another verified account as the default.?>
+</p>
+  </div>
+<?=$manForm?>
+</div>
+<div class="panel panel-default">
+  <!-- Default panel contents -->
+  <div class="panel-heading"><?=_Add Email?></div>
+  <div class="panel-body">
+   <p>
+<?=_Currently we only issue certificates for Punycode domains if the person requesting them has code signing attributes attached to their account, as these have potentially slightly higher security risk.?>
+</p></div>
+<?=$addForm?>
+</div>
+
diff --git a/src/org/cacert/gigi/pages/admin/TTPAdminForm.java b/src/org/cacert/gigi/pages/admin/TTPAdminForm.java
new file mode 100644 (file)
index 0000000..a52afed
--- /dev/null
@@ -0,0 +1,45 @@
+package org.cacert.gigi.pages.admin;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+
+public class TTPAdminForm extends Form {
+
+    private static Template t = new Template(TTPAdminForm.class.getResource("TTPAdminForm.templ"));
+
+    User u;
+
+    User ttpAdmin;
+
+    public TTPAdminForm(HttpServletRequest hsr, User u) {
+        super(hsr);
+        this.u = u;
+        ttpAdmin = LoginPage.getUser(hsr);
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        if (req.getParameter("deny") != null) {
+            u.revokeGroup(ttpAdmin, TTPAdminPage.TTP_APPLICANT);
+        }
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        vars.put("name", u.getName());
+        vars.put("email", u.getEmail());
+        vars.put("DoB", u.getDoB());
+        vars.put("uid", Integer.toString(u.getId()));
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/admin/TTPAdminForm.templ b/src/org/cacert/gigi/pages/admin/TTPAdminForm.templ
new file mode 100644 (file)
index 0000000..a3fd9f2
--- /dev/null
@@ -0,0 +1,7 @@
+<input type="hidden" name="uid" value="<?=$uid?>"/>
+<table class="table">
+<tr><td><?=_Name?></td><td><?=$name?></td></tr>
+<tr><td><?=_Email?></td><td><?=$email?></td></tr>
+<tr><td><?=_Date of Birth?></td><td><?=$DoB?></td></tr>
+<tr><td colspan="2"><input type="submit" name="deny" value="<?=_Deny Request?>"></td></tr>
+</table>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/admin/TTPAdminPage.java b/src/org/cacert/gigi/pages/admin/TTPAdminPage.java
new file mode 100644 (file)
index 0000000..a6bd0d4
--- /dev/null
@@ -0,0 +1,94 @@
+package org.cacert.gigi.pages.admin;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.SprintfCommand;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.pages.error.PageNotFound;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class TTPAdminPage extends Page {
+
+    public static final String PATH = "/admin/ttp";
+
+    public static final Group TTP_APPLICANT = Group.getByString("ttp-applicant");
+
+    public TTPAdminPage() {
+        super("TTP-Admin");
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            Form.getForm(req, TTPAdminForm.class).submit(resp.getWriter(), req);
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+        resp.sendRedirect(PATH);
+    }
+
+    private static final int PAGE_LEN = 30;
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        String path = req.getPathInfo();
+        if (path != null && path.length() > PATH.length() + 1) {
+            int id = Integer.parseInt(path.substring(1 + PATH.length()));
+            User u = User.getById(id);
+            if (u == null || !u.isInGroup(TTP_APPLICANT)) {
+                SprintfCommand command = new SprintfCommand("The TTP-request is not available anymore. You might want to go {0}back{1}.", Arrays.asList("!'<a href=\"" + PATH + "\">", "!'</a>"));
+                req.setAttribute(PageNotFound.MESSAGE_ATTRIBUTE, command);
+                resp.sendError(404);
+                return;
+            }
+            new TTPAdminForm(req, u).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+            return;
+        }
+        int offset = 0;
+        String offsetS = req.getParameter("offset");
+        if (offsetS != null) {
+            offset = Integer.parseInt(offsetS);
+        }
+
+        final User[] users = TTP_APPLICANT.getMembers(offset, PAGE_LEN + 1);
+        HashMap<String, Object> vars = new HashMap<>();
+        vars.put("users", new IterableDataset() {
+
+            int i = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (i >= Math.min(PAGE_LEN, users.length)) {
+                    return false;
+                }
+                vars.put("id", Integer.toString(users[i].getId()));
+                vars.put("name", users[i].getName().toString());
+                vars.put("email", users[i].getEmail());
+
+                i++;
+                return true;
+            }
+        });
+        if (users.length == PAGE_LEN + 1) {
+            vars.put("next", Integer.toString(offset + 30));
+        }
+        getDefaultTemplate().output(resp.getWriter(), getLanguage(req), vars);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.isInGroup(Group.getByString("ttp-assurer"));
+    }
+}
diff --git a/src/org/cacert/gigi/pages/admin/TTPAdminPage.templ b/src/org/cacert/gigi/pages/admin/TTPAdminPage.templ
new file mode 100644 (file)
index 0000000..0a4b6a2
--- /dev/null
@@ -0,0 +1,9 @@
+<table class="table">
+<tr><th><?=_Name?></th><th><?=_Email?></th><th></th></tr>
+<? foreach($users) { ?>
+<tr><td><?=$name?></td><td><?=$email?></td><td><a href="/admin/ttp/<?=$id?>">Process</a></td></tr>
+<? } ?>
+</table>
+<? if($next) { ?>
+<a href="?offset=<?=$next?>"><?=_next?></a>
+<? } ?>
diff --git a/src/org/cacert/gigi/pages/admin/support/FindDomainForm.java b/src/org/cacert/gigi/pages/admin/support/FindDomainForm.java
new file mode 100644 (file)
index 0000000..1096f3d
--- /dev/null
@@ -0,0 +1,58 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.SprintfCommand;
+import org.cacert.gigi.output.template.Template;
+
+public class FindDomainForm extends Form {
+
+    private CertificateOwner res = null;
+
+    private static Template t;
+    static {
+        t = new Template(FindDomainForm.class.getResource("FindDomainForm.templ"));
+    }
+
+    public FindDomainForm(HttpServletRequest hsr) {
+        super(hsr);
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        String request = req.getParameter("domain");
+        Domain d = null;
+        if (request.matches("#[0-9]+")) {
+            try {
+                d = Domain.getById(Integer.parseInt(request.substring(1)));
+            } catch (IllegalArgumentException e) {
+                throw new GigiApiException(SprintfCommand.createSimple("No personal domains found matching the id {0}", request.substring(1)));
+            }
+        } else {
+            d = Domain.searchUserIdByDomain(request);
+        }
+        if (d == null) {
+            throw new GigiApiException(SprintfCommand.createSimple("No personal domains found matching {0}", request));
+        }
+        res = d.getOwner();
+        return true;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        t.output(out, l, vars);
+    }
+
+    public CertificateOwner getRes() {
+        return res;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/FindDomainForm.templ b/src/org/cacert/gigi/pages/admin/support/FindDomainForm.templ
new file mode 100644 (file)
index 0000000..0eb660d
--- /dev/null
@@ -0,0 +1,13 @@
+<table class="table">
+  <tbody><tr>
+    <th colspan="2"><?=_Find User by Domain?></th>
+  </tr>
+  <tr>
+    <td><?=_Domain?>:</td>
+    <td><input class="form-control" type="text" value="" name="domain" placeholder="<?=_For search by ID use # prefix e.g. #123456?>"></td>
+  </tr>
+  <tr>
+    <td colspan="2"><input type="submit" value="<?=_Next?>" name="process"></td>
+  </tr>
+</tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/admin/support/FindDomainPage.java b/src/org/cacert/gigi/pages/admin/support/FindDomainPage.java
new file mode 100644 (file)
index 0000000..06d4162
--- /dev/null
@@ -0,0 +1,34 @@
+package org.cacert.gigi.pages.admin.support;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.OneFormPage;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class FindDomainPage extends OneFormPage {
+
+    public static final String PATH = "/support/find/domain";
+
+    public FindDomainPage() {
+        super("Find Domain", FindDomainForm.class);
+    }
+
+    @Override
+    public String getSuccessPath(Form f) {
+        CertificateOwner res = ((FindDomainForm) f).getRes();
+        if (res instanceof User) {
+            return SupportUserDetailsPage.PATH + res.getId();
+        } else if (res instanceof Organisation) {
+            return "/support/domain/" + res.getId();
+        } else {
+            throw new Error("Unknown owner type.");
+        }
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.canSupport();
+    }
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/FindUserForm.java b/src/org/cacert/gigi/pages/admin/support/FindUserForm.java
new file mode 100644 (file)
index 0000000..21276db
--- /dev/null
@@ -0,0 +1,47 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.SprintfCommand;
+import org.cacert.gigi.output.template.Template;
+
+public class FindUserForm extends Form {
+
+    private User users[];
+
+    private static Template t;
+    static {
+        t = new Template(FindDomainForm.class.getResource("FindUserForm.templ"));
+    }
+
+    public FindUserForm(HttpServletRequest hsr) {
+        super(hsr);
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        User[] users = User.findByEmail(req.getParameter("email"));
+        if (users.length == 0) {
+            throw new GigiApiException(SprintfCommand.createSimple("No users found matching {0}", req.getParameter("email")));
+        }
+        this.users = users;
+        return true;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        t.output(out, l, vars);
+    }
+
+    public User[] getUsers() {
+        return users;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/FindUserForm.templ b/src/org/cacert/gigi/pages/admin/support/FindUserForm.templ
new file mode 100644 (file)
index 0000000..0168224
--- /dev/null
@@ -0,0 +1,24 @@
+<? if($usertable) {?>
+<p>Multiple users where found.</p>
+<table class="table">
+<tr>
+<th>Id</th><th>E-Mail</th></tr>
+<? foreach($usertable) {?>
+       <tr><td><a href="/support/user/<?=$usrid?>"><?=$usrid?></a></td><td><a href="/support/user/<?=$usrid?>"><?=$usermail?></a></td></tr>
+<? } ?>
+</table>
+<? } ?>
+<? if($first) {?>
+<table class="table">
+  <tbody><tr>
+    <th colspan="2"><?=_Find User?></th>
+  </tr>
+  <tr>
+    <td><?=_Email?>:</td>
+    <td><input class="form-control" name="email" value="" size="30" title="<?=_use % as wildcard?>" placeholder="<?=_use % as wildcard?>" type="text"/></td>
+  </tr>
+  <tr>
+    <td colspan="2"><input name="process" value="<?=_Next?>" type="submit"/></td>
+  </tr>
+</tbody></table>
+<? } ?>
diff --git a/src/org/cacert/gigi/pages/admin/support/FindUserPage.java b/src/org/cacert/gigi/pages/admin/support/FindUserPage.java
new file mode 100644 (file)
index 0000000..059298f
--- /dev/null
@@ -0,0 +1,72 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class FindUserPage extends Page {
+
+    public static final String PATH = "/support/find/user";
+
+    public FindUserPage() {
+        super("Find User");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        vars.put("first", true);
+        new FindUserForm(req).output(resp.getWriter(), Page.getLanguage(req), vars);
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        FindUserForm form = Form.getForm(req, FindUserForm.class);
+        try {
+            form.submit(resp.getWriter(), req);
+            final User[] users = form.getUsers();
+            if (users.length == 1) {
+                resp.sendRedirect(SupportUserDetailsPage.PATH + users[0].getId());
+            } else {
+                HashMap<String, Object> vars = new HashMap<String, Object>();
+                vars.put("first", false);
+                vars.put("usertable", new IterableDataset() {
+
+                    int i = 0;
+
+                    @Override
+                    public boolean next(Language l, Map<String, Object> vars) {
+                        if (i == users.length) {
+                            return false;
+                        }
+                        vars.put("usrid", users[i].getId());
+                        vars.put("usermail", users[i].getEmail());
+                        i++;
+                        return true;
+                    }
+                });
+                form.output(resp.getWriter(), getLanguage(req), vars);
+            }
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), Page.getLanguage(req));
+            doGet(req, resp);
+        }
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.canSupport();
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportEnterTicketForm.java b/src/org/cacert/gigi/pages/admin/support/SupportEnterTicketForm.java
new file mode 100644 (file)
index 0000000..5b57a1c
--- /dev/null
@@ -0,0 +1,52 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.Gigi;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class SupportEnterTicketForm extends Form {
+
+    private static Template t;
+
+    static {
+        t = new Template(SupportEnterTicketForm.class.getResource("SupportEnterTicketForm.templ"));
+    }
+
+    public SupportEnterTicketForm(HttpServletRequest hsr) {
+        super(hsr);
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        if (req.getParameter("setTicket") != null) {
+            // [asdmASDM]\d{8}\.\d+
+            String ticket = req.getParameter("ticketno");
+            if (ticket.matches("[asdmASDM]\\d{8}\\.\\d+")) {
+                AuthorizationContext ac = LoginPage.getAuthorizationContext(req);
+                req.getSession().setAttribute(Gigi.AUTH_CONTEXT, new AuthorizationContext(ac.getActor(), ticket));
+                return true;
+            }
+            return false;
+        } else if (req.getParameter("deleteTicket") != null) {
+            AuthorizationContext ac = LoginPage.getAuthorizationContext(req);
+            req.getSession().setAttribute(Gigi.AUTH_CONTEXT, new AuthorizationContext(ac.getActor(), ac.getActor()));
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        t.output(out, l, vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportEnterTicketForm.templ b/src/org/cacert/gigi/pages/admin/support/SupportEnterTicketForm.templ
new file mode 100644 (file)
index 0000000..5034bac
--- /dev/null
@@ -0,0 +1,17 @@
+<table class="table">
+<tr>
+            <th colspan="2"><?=_Ticket handling?></th>
+        </tr>
+        <tr>
+            <td><?=_Ticket no?>:</td>
+            <td><input type="text" <? if($ticketNo) {?>value="<?=$ticketNo?>" <? } ?>name="ticketno"></td>
+        </tr>
+        <tr>
+            <td colspan="2"><input class="form-control" type="submit" name="setTicket" value="<?=_Set ticket number?>"></td>
+        </tr>
+<? if($ticketNo) {?>
+        <tr>
+            <td colspan="2"><input type="submit" name="deleteTicket" value="<?=_End working on ticket?>"></td>
+        </tr>
+<? } ?>
+</table>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportEnterTicketPage.java b/src/org/cacert/gigi/pages/admin/support/SupportEnterTicketPage.java
new file mode 100644 (file)
index 0000000..eb1cfca
--- /dev/null
@@ -0,0 +1,54 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class SupportEnterTicketPage extends Page {
+
+    public static final String PATH = "/support/ticket";
+
+    public SupportEnterTicketPage() {
+        super("Set Ticket");
+    }
+
+    @Override
+    public boolean beforeTemplate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (req.getParameter("setTicket") == null && req.getParameter("deleteTicket") == null) {
+            return false;
+        }
+        SupportEnterTicketForm f = Form.getForm(req, SupportEnterTicketForm.class);
+        try {
+            if (f.submit(resp.getWriter(), req)) {
+                resp.sendRedirect(PATH);
+                return true;
+            }
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+        return false;
+
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        vars.put("ticketNo", LoginPage.getAuthorizationContext(req).getSupporterTicketId());
+        new SupportEnterTicketForm(req).output(resp.getWriter(), getLanguage(req), vars);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.isInGroup(Group.SUPPORTER);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportRevokeCertificatesForm.java b/src/org/cacert/gigi/pages/admin/support/SupportRevokeCertificatesForm.java
new file mode 100644 (file)
index 0000000..32f5225
--- /dev/null
@@ -0,0 +1,98 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.security.GeneralSecurityException;
+import java.util.Date;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.SupportedUser;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+
+public class SupportRevokeCertificatesForm extends Form {
+
+    private static Template t;
+
+    private SupportedUser user;
+    static {
+        t = new Template(SupportRevokeCertificatesForm.class.getResource("SupportRevokeCertificatesForm.templ"));
+    }
+
+    public SupportRevokeCertificatesForm(HttpServletRequest hsr, SupportedUser user) {
+        super(hsr);
+        this.user = user;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        if (user.getTicket() != null) {
+            user.revokeAllCertificates();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        final Certificate[] certs = user.getCertificates(true);
+        final CertificateProfile[] profiles = CertificateProfile.getAll();
+        vars.put("types", new IterableDataset() {
+
+            int typeIndex = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (typeIndex > profiles.length - 1) {
+                    return false;
+                }
+                int valid = 0;
+                int total = 0;
+                int revoked = 0;
+                long lastExpire = Long.MIN_VALUE;
+                for (int i = 0; i < certs.length; i++) {
+                    try {
+                        if (certs[i].getProfile().getId() != profiles[typeIndex].getId()) {
+                            continue;
+                        }
+                        total++;
+                        if (certs[i].getStatus() == CertificateStatus.DRAFT) {
+                            continue;
+                        }
+                        if (certs[i].getStatus() == CertificateStatus.REVOKED) {
+                            revoked++;
+                            continue;
+                        }
+                        certs[i].cert().checkValidity();
+                        lastExpire = Math.max(lastExpire, certs[i].cert().getNotAfter().getTime());
+                        valid++;
+                    } catch (GeneralSecurityException | IOException e) {
+                        continue;
+                    }
+                }
+                vars.put("total", total);
+                vars.put("profile", profiles[typeIndex].getVisibleName());
+                vars.put("valid", valid);
+                vars.put("exp", total - valid);
+                vars.put("rev", revoked);
+                if (lastExpire == Long.MIN_VALUE) {
+                    vars.put("lastdate", "-");
+                } else {
+                    vars.put("lastdate", new Date(lastExpire));
+                }
+                typeIndex++;
+                return true;
+            }
+        });
+        t.output(out, l, vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportRevokeCertificatesForm.templ b/src/org/cacert/gigi/pages/admin/support/SupportRevokeCertificatesForm.templ
new file mode 100644 (file)
index 0000000..db141cd
--- /dev/null
@@ -0,0 +1,28 @@
+<table class="table">
+        <tbody><tr>
+            <th colspan="6"><?=_Certificates?></td>
+        </tr>
+        <tr>
+            <th><?=_Cert Type?></th>
+            <th><?=_Total?></th>
+            <th><?=_Valid?></th>
+            <th><?=_Expired?></th>
+            <th><?=_Revoked?></th>
+            <th><?=_Latest Expire?></th>
+        </tr>
+       <? foreach($types) { ?>
+        <tr>
+            <td><?=$profile?></th>
+                <td><?=$total?></td>
+            <td><?=$valid?></td>
+            <td><?=$exp?></td>
+            <td><?=$rev?></td>
+            <td><?=$lastdate?></td>
+            </tr>
+            <? } ?>
+        <tr>
+            <th colspan="6">
+                    <input name="revokeall" value="<?=_revoke certificates?>" type="submit">
+            </th>
+        </tr>
+    </tbody></table>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsForm.java b/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsForm.java
new file mode 100644 (file)
index 0000000..60251e7
--- /dev/null
@@ -0,0 +1,125 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.PrintWriter;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.SupportedUser;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.output.GroupSelector;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.PasswordResetPage;
+
+public class SupportUserDetailsForm extends Form {
+
+    private static Template t;
+
+    private SupportedUser user;
+
+    private DateSelector dobSelector;
+
+    private GroupSelector value = new GroupSelector("groupToModify");
+
+    static {
+        t = new Template(FindDomainForm.class.getResource("SupportUserDetailsForm.templ"));
+    }
+
+    public SupportUserDetailsForm(HttpServletRequest hsr, SupportedUser user) {
+        super(hsr);
+        this.user = user;
+        dobSelector = new DateSelector("dobd", "dobm", "doby", user.getTargetUser().getDoB());
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        if (user.getTicket() == null) {
+            return false;
+        }
+        if ((req.getParameter("detailupdate") != null ? 1 : 0) + (req.getParameter("grant") != null ? 1 : 0) + (req.getParameter("deny") != null ? 1 : 0) + (req.getParameter("resetPass") != null ? 1 : 0) != 1) {
+            throw new GigiApiException("More than one action requested!");
+        }
+        if (req.getParameter("grant") != null || req.getParameter("deny") != null) {
+            value.update(req);
+            Group toMod = value.getGroup();
+            if (req.getParameter("grant") != null) {
+                user.grant(toMod);
+            } else {
+                user.revoke(toMod);
+            }
+            return true;
+        }
+        if (req.getParameter("resetPass") != null) {
+            String aword = req.getParameter("aword");
+            if (aword == null || aword.equals("")) {
+                throw new GigiApiException("An A-Word is required to perform a password reset.");
+            }
+            Language l = Language.getInstance(user.getTargetUser().getPreferredLocale());
+            String method = l.getTranslation("A password reset was triggered. Please enter the required text sent to you by support on this page:");
+            String subject = l.getTranslation("Password reset by support.");
+            PasswordResetPage.initPasswordResetProcess(out, user.getTargetUser(), req, aword, l, method, subject);
+            return true;
+        }
+        dobSelector.update(req);
+        String fname = req.getParameter("fname");
+        String mname = req.getParameter("mname");
+        String lname = req.getParameter("lname");
+        String suffix = req.getParameter("suffix");
+        if (fname == null || mname == null || lname == null | suffix == null) {
+            throw new GigiApiException("Incomplete request!");
+        }
+        if ( !dobSelector.isValid()) {
+            throw new GigiApiException("Invalid date of birth!");
+        }
+        Name newName = new Name(fname, lname, mname, suffix);
+        synchronized (user.getTargetUser()) {
+            if (user.setDob(dobSelector.getDate()) | user.setName(newName)) {
+                user.submitSupportAction();
+            }
+        }
+        return true;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        User user = this.user.getTargetUser();
+        Name name = user.getName();
+        vars.put("mail", user.getEmail());
+        vars.put("fname", name.getFname());
+        vars.put("mname", name.getMname());
+        vars.put("lname", name.getLname());
+        vars.put("suffix", name.getSuffix());
+        vars.put("assurer", user.canAssure());
+        vars.put("dob", dobSelector);
+        vars.put("assurancepoints", user.getAssurancePoints());
+        vars.put("exppoints", user.getExperiencePoints());
+        vars.put("id", user.getId());
+        final Set<Group> gr = user.getGroups();
+        vars.put("groups", new IterableDataset() {
+
+            Iterator<Group> i = gr.iterator();
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if ( !i.hasNext()) {
+                    return false;
+                }
+                Group g = i.next();
+                vars.put("group_name", g.getName());
+                return true;
+            }
+        });
+        vars.put("groupSelector", value);
+        t.output(out, l, vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsForm.templ b/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsForm.templ
new file mode 100644 (file)
index 0000000..511c9b0
--- /dev/null
@@ -0,0 +1,81 @@
+<table class="table">
+        <tbody><tr>
+            <th colspan="2"><?=$mail?>'s Account Details</th>
+        </tr>
+        <tr>
+            <td><?=_Email?>:</td>
+            <td><?=$mail?></td>
+        </tr>
+        <tr>
+            <td><?=_First Name?>:</td>
+            <td>
+                <input class="form-control" type="text" value="<?=$fname?>" name="fname">
+           </td>
+        </tr>
+        <tr>
+            <td><?=_Middle Name?>:</td>
+            <td><input class="form-control" type="text" value="<?=$mname?>" name="mname"></td>
+        </tr>
+        <tr>
+            <td><?=_Last Name?>:</td>
+            <td>
+                <input class="form-control" type="text" value="<?=$lname?>" name="lname">
+            </td>
+        </tr>
+        <tr>
+            <td><?=_Suffix?>:</td>
+            <td><input class="form-control" type="text" value="<?=$suffix?>" name="suffix"></td>
+        </tr>
+        <tr>
+            <td><?=_Date of Birth?>:</td>
+            <td>
+                       <?=$dob?>
+            </td>
+        </tr>
+        <tr>
+            <td><?=_Trainings?>:</td>
+            <td><a href="./<?=$id?>/trainings"><?=_Show?></a></td>
+        </tr>
+        <tr>
+            <td><?=_Is Assurer?>:</td>
+            <td>
+            <? if($assurer) { ?>
+            <?=_Yes?>
+            <? } else { ?>
+            <?=_No?>
+            <? } ?>
+            </td>
+        </tr>
+               <tr>
+            <td><?=_Assurance Points?>:</td>
+            <td><?=$assurancepoints?></td>
+        </tr>
+        <tr>
+            <td><?=_Experience Points?>:</td>
+            <td><?=$exppoints?></td>
+        </tr>
+        
+        <tr>
+            <td><?=_Groups?>:</td>
+            <td><p><? foreach($groups) { ?><?=$group_name?>, <? } ?></p>
+            <p><?=$groupSelector?><input type='submit' value='<?=_Grant Group?>' name='grant'><input type='submit' value='<?=_Deny Group?>' name='deny'></p>
+            </td>
+        </tr>
+        <tr>
+            <td><?=_Reset Password?>:</td>
+            <td><input type="text" name="aword"> <input type="submit" value="<?=_Reset Password?>" name="resetPass"></td>
+        </tr>
+        <tr>
+            <td><?=_Delete Account?>:</td>
+            <td><?=_Delete Account?></td>
+        </tr>
+            <tr>
+            <td><?=_Show Lost Password Details?></td>
+        </tr>
+            <tr>
+            <td colspan="2"><a href="./<?=$id?>/history"><?=_Show account history?></a></td>
+        </tr>
+        <tr><td colspan="2"><input name="detailupdate" type="submit" value="<?=_Update?>"/></td></tr>
+    </tbody>
+</table>
+<br/>
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsPage.java b/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsPage.java
new file mode 100644 (file)
index 0000000..726bdd3
--- /dev/null
@@ -0,0 +1,87 @@
+package org.cacert.gigi.pages.admin.support;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.SupportedUser;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class SupportUserDetailsPage extends Page {
+
+    public static final String PATH = "/support/user/";
+
+    public SupportUserDetailsPage() {
+        super("Support: User Details");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        int id = -1;
+        String[] idP = req.getPathInfo().split("/");
+        try {
+            id = Integer.parseInt(idP[idP.length - 1]);
+        } catch (NumberFormatException e) {
+            resp.sendError(404);
+        }
+        final User user = User.getById(id);
+        SupportedUser targetUser = new SupportedUser(user, getUser(req), LoginPage.getAuthorizationContext(req).getSupporterTicketId());
+        SupportUserDetailsForm f = new SupportUserDetailsForm(req, targetUser);
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        vars.put("details", f);
+        final EmailAddress[] addrs = user.getEmails();
+        vars.put("emails", new IterableDataset() {
+
+            int i = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                for (; i < addrs.length;) {
+                    String address = addrs[i++].getAddress();
+                    if ( !address.equals(user.getEmail())) {
+                        vars.put("secmail", address);
+                        return true;
+                    }
+                }
+                return false;
+            }
+        });
+        vars.put("certifrevoke", new SupportRevokeCertificatesForm(req, targetUser));
+        getDefaultTemplate().output(resp.getWriter(), getLanguage(req), vars);
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            if (req.getParameter("revokeall") != null) {
+                if ( !Form.getForm(req, SupportRevokeCertificatesForm.class).submit(resp.getWriter(), req)) {
+                    throw new GigiApiException("No ticket number set.");
+                }
+            } else if (req.getParameter("detailupdate") != null || req.getParameter("resetPass") != null || req.getParameter("deny") != null || req.getParameter("grant") != null) {
+                if ( !Form.getForm(req, SupportUserDetailsForm.class).submit(resp.getWriter(), req)) {
+                    throw new GigiApiException("No ticket number set.");
+                }
+            }
+        } catch (GigiApiException e) {
+            e.printStackTrace();
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+        super.doPost(req, resp);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.canSupport();
+    }
+}
diff --git a/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsPage.templ b/src/org/cacert/gigi/pages/admin/support/SupportUserDetailsPage.templ
new file mode 100644 (file)
index 0000000..879367f
--- /dev/null
@@ -0,0 +1,13 @@
+<?=$details?>
+<table class="table">
+        <tbody><tr>
+            <th><?=_Alternate Verified Email Addresses?></th>
+        </tr>
+        <? foreach($emails) {?>
+        <tr>
+            <td><?=$secmail?></td>
+        </tr>
+        <? } ?>
+        </tbody>
+</table>
+<?=$certifrevoke?>
diff --git a/src/org/cacert/gigi/pages/error/AccessDenied.java b/src/org/cacert/gigi/pages/error/AccessDenied.java
new file mode 100644 (file)
index 0000000..60c48bf
--- /dev/null
@@ -0,0 +1,26 @@
+package org.cacert.gigi.pages.error;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.pages.Page;
+
+public class AccessDenied extends Page {
+
+    public AccessDenied() {
+        super("Access denied");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        getDefaultTemplate().output(resp.getWriter(), Page.getLanguage(req), null);
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/error/AccessDenied.templ b/src/org/cacert/gigi/pages/error/AccessDenied.templ
new file mode 100644 (file)
index 0000000..69069ca
--- /dev/null
@@ -0,0 +1 @@
+<p><?=_The access to this page has been denied to you.?></p>
diff --git a/src/org/cacert/gigi/pages/error/PageNotFound.java b/src/org/cacert/gigi/pages/error/PageNotFound.java
new file mode 100644 (file)
index 0000000..ffc107f
--- /dev/null
@@ -0,0 +1,35 @@
+package org.cacert.gigi.pages.error;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.pages.Page;
+
+public class PageNotFound extends Page {
+
+    public static final String MESSAGE_ATTRIBUTE = "message-Str";
+
+    public PageNotFound() {
+        super("File not found!");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        HashMap<String, Object> vars = new HashMap<>();
+        Object customMessage = req.getAttribute(MESSAGE_ATTRIBUTE);
+        if (customMessage == null) {
+            customMessage = getLanguage(req).getTranslation("Due to recent site changes bookmarks may no longer be valid, please update your bookmarks.");
+        }
+        vars.put("message", customMessage);
+        getDefaultTemplate().output(resp.getWriter(), Page.getLanguage(req), vars);
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/error/PageNotFound.templ b/src/org/cacert/gigi/pages/error/PageNotFound.templ
new file mode 100644 (file)
index 0000000..1c1b95c
--- /dev/null
@@ -0,0 +1 @@
+<p><?=$message?></p>
index b38194994bb69044907a3f3910feaae618482311..1e6b33783ee3b89c47c3e602cb90b0196bb949a8 100644 (file)
@@ -8,58 +8,59 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
+import org.cacert.gigi.output.template.Form;
 import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.RateLimit;
 
 public class RegisterPage extends Page {
 
-       private static final String SIGNUP_PROCESS = "signupProcess";
-       public static final String PATH = "/register";
+    private static final String SIGNUP_PROCESS = "signupProcess";
 
-       public RegisterPage() {
-               super("Register");
-       }
+    public static final String PATH = "/register";
 
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               PrintWriter out = resp.getWriter();
-               HashMap<String, Object> vars = new HashMap<String, Object>();
-               getDefaultTemplate().output(out, getLanguage(req), vars);
-               Signup s = getForm(req);
-               s.output(out, getLanguage(req), vars);
-       }
-       public Signup getForm(HttpServletRequest req) {
-               HttpSession hs = req.getSession();
-               Signup s = (Signup) hs.getAttribute(SIGNUP_PROCESS);
-               if (s == null) {
-                       s = new Signup();
-                       hs.setAttribute(SIGNUP_PROCESS, s);
-               }
-               return s;
+    // 50 per 5 min
+    public static final RateLimit RATE_LIMIT = new RateLimit(50, 5 * 60 * 1000);
 
-       }
-       @Override
-       public void doPost(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               Signup s = getForm(req);
-               if (s.submit(resp.getWriter(), req)) {
-                       HttpSession hs = req.getSession();
-                       hs.setAttribute(SIGNUP_PROCESS, null);
-                       resp.getWriter()
-                                       .println(
-                                                       translate(
-                                                                       req,
-                                                                       "Your information has been submitted"
-                                                                                       + " into our system. You will now be sent an email with a web link,"
-                                                                                       + " you need to open that link in your web browser within 24 hours"
-                                                                                       + " or your information will be removed from our system!"));
-                       return;
-               }
+    public RegisterPage() {
+        super("Register");
+    }
 
-               super.doPost(req, resp);
-       }
-       @Override
-       public boolean needsLogin() {
-               return false;
-       }
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        Signup s = new Signup(req);
+        outputGet(req, resp, s);
+    }
+
+    private void outputGet(HttpServletRequest req, HttpServletResponse resp, Signup s) throws IOException {
+        PrintWriter out = resp.getWriter();
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        getDefaultTemplate().output(out, getLanguage(req), vars);
+        s.output(out, getLanguage(req), vars);
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        Signup s = Form.getForm(req, Signup.class);
+        if (s == null) {
+            resp.getWriter().println(translate(req, "CSRF token check failed."));
+        } else if (s.submit(resp.getWriter(), req)) {
+            HttpSession hs = req.getSession();
+            hs.setAttribute(SIGNUP_PROCESS, null);
+            resp.getWriter().println(translate(req, "Your information has been submitted" + " into our system. You will now be sent an email with a web link," + " you need to open that link in your web browser within 24 hours" + " or your information will be removed from our system!"));
+            return;
+        }
+
+        outputGet(req, resp, s);
+    }
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac == null;
+    }
 }
index 4fbb41d50023380e21b14283041ddc67f8a01f43..c792985abfc0d2be05f5b96bfbe82a4a9be82a09 100644 (file)
@@ -1,8 +1,8 @@
-<p><?=_By joining CAcert and becoming a member, you agree to the CAcert Community Agreement. Please take a moment now to read that and agree to it; this will be required to complete the process of joining.?></p>
+<p><?=_By joining SomeCA and becoming a member, you agree to the Terms of Service. Please take a moment now to read that and agree to it; this will be required to complete the process of joining.?></p>
 <p><?=_Warning! This site requires cookies to be enabled to ensure your privacy and security. This site uses session cookies to store temporary values to prevent people from copying and pasting the session ID to someone else exposing their account, personal details and identity theft as a result.?></p>
 <p style="border:dotted 1px #900;padding:0.3em;background-color:#ffe;">
 <b><?=_Note: Please enter your date of birth and names as they are written in your official documents.?></b><br /><br />
-<?=_Because CAcert is a certificate authority (CA) people rely on us knowing about the identity of the users of our certificates. So even as we value privacy very much, we need to collect at least some basic information about our members. This is especially the case for everybody who wants to take part in our web of trust.?>
+<?=_Because SomeCA is a certificate authority (CA) people rely on us knowing about the identity of the users of our certificates. So even as we value privacy very much, we need to collect at least some basic information about our members. This is especially the case for everybody who wants to take part in our web of trust.?>
 <?=_Your private information will be used for internal procedures only and will not be shared with third parties.?>
 </p>
 <p style="border:dotted 1px #900;padding:0.3em;background-color:#ffe;">
index 3d074441edbd3d587894d1ca865a0d3b62daaa02..d341df280f237ed2417d406c47f5570431211b20 100644 (file)
 package org.cacert.gigi.pages.main;
 
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 
-import org.cacert.gigi.Language;
-import org.cacert.gigi.User;
-import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.User;
 import org.cacert.gigi.email.EmailProvider;
+import org.cacert.gigi.localisation.Language;
 import org.cacert.gigi.output.DateSelector;
-import org.cacert.gigi.output.Form;
-import org.cacert.gigi.output.Template;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
 import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.CalendarUtil;
 import org.cacert.gigi.util.HTMLEncoder;
 import org.cacert.gigi.util.Notary;
 import org.cacert.gigi.util.PasswordStrengthChecker;
-import org.cacert.gigi.util.RandomToken;
-import org.cacert.gigi.util.ServerConstants;
 
 public class Signup extends Form {
-       User buildup = new User();
-       Template t;
-       boolean general = true, country = true, regional = true, radius = true;
-       public Signup() {
-               try {
-                       t = new Template(new InputStreamReader(
-                                       Signup.class.getResourceAsStream("Signup.templ"), "UTF-8"));
-               } catch (UnsupportedEncodingException e) {
-                       e.printStackTrace();
-               }
-               buildup.setFname("");
-               buildup.setMname("");
-               buildup.setLname("");
-               buildup.setSuffix("");
-               buildup.setEmail("");
-               buildup.setDob(new Date(0));
-       }
-       DateSelector myDoB = new DateSelector("day", "month", "year");
 
-       public void output(PrintWriter out, Language l,
-                       Map<String, Object> outerVars) {
-               HashMap<String, Object> vars = new HashMap<String, Object>();
-               vars.put("fname", HTMLEncoder.encodeHTML(buildup.getFname()));
-               vars.put("mname", HTMLEncoder.encodeHTML(buildup.getMname()));
-               vars.put("lname", HTMLEncoder.encodeHTML(buildup.getLname()));
-               vars.put("suffix", HTMLEncoder.encodeHTML(buildup.getSuffix()));
-               vars.put("dob", myDoB);
-               vars.put("email", HTMLEncoder.encodeHTML(buildup.getEmail()));
-               vars.put("general", general ? " checked=\"checked\"" : "");
-               vars.put("country", country ? " checked=\"checked\"" : "");
-               vars.put("regional", regional ? " checked=\"checked\"" : "");
-               vars.put("radius", radius ? " checked=\"checked\"" : "");
-               vars.put(
-                               "helpOnNames",
-                               String.format(
-                                               l.getTranslation("Help on Names %sin the wiki%s"),
-                                               "<a href=\"//wiki.cacert.org/FAQ/HowToEnterNamesInJoinForm\" target=\"_blank\">",
-                                               "</a>"));
-               t.output(out, l, vars);
-       }
-       private void update(HttpServletRequest r) {
-               if (r.getParameter("fname") != null) {
-                       buildup.setFname(r.getParameter("fname"));
-               }
-               if (r.getParameter("lname") != null) {
-                       buildup.setLname(r.getParameter("lname"));
-               }
-               if (r.getParameter("mname") != null) {
-                       buildup.setMname(r.getParameter("mname"));
-               }
-               if (r.getParameter("suffix") != null) {
-                       buildup.setSuffix(r.getParameter("suffix"));
-               }
-               if (r.getParameter("email") != null) {
-                       buildup.setEmail(r.getParameter("email"));
-               }
-               general = "1".equals(r.getParameter("general"));
-               country = "1".equals(r.getParameter("country"));
-               regional = "1".equals(r.getParameter("regional"));
-               radius = "1".equals(r.getParameter("radius"));
-               myDoB.update(r);
-       }
+    Name buildupName = new Name("", "", "", "");
 
-       @Override
-       public synchronized boolean submit(PrintWriter out, HttpServletRequest req) {
-               update(req);
-               boolean failed = false;
-               out.println("<div class='formError'>");
-               if (buildup.getFname().equals("") || buildup.getLname().equals("")) {
-                       outputError(out, req, "First and/or last names were blank.");
-                       failed = true;
-               }
-               if (!myDoB.isValid()) {
-                       outputError(out, req, "Invalid date of birth");
-                       failed = true;
-               }
-               if (!"1".equals(req.getParameter("cca_agree"))) {
-                       outputError(out, req,
-                                       "You have to agree to the CAcert Community agreement.");
-                       failed = true;
-               }
-               if (buildup.getEmail().equals("")) {
-                       outputError(out, req, "Email Address was blank");
-                       failed = true;
-               }
-               String pw1 = req.getParameter("pword1");
-               String pw2 = req.getParameter("pword2");
-               if (pw1 == null || pw1.equals("")) {
-                       outputError(out, req, "Pass Phrases were blank");
-                       failed = true;
-               } else if (!pw1.equals(pw2)) {
-                       outputError(out, req, "Pass Phrases don't match");
-                       failed = true;
-               }
-               int pwpoints = PasswordStrengthChecker.checkpw(pw1, buildup);
-               if (pwpoints < 3) {
-                       outputError(
-                                       out,
-                                       req,
-                                       "The Pass Phrase you submitted failed to contain enough"
-                                                       + " differing characters and/or contained words from"
-                                                       + " your name and/or email address.");
-                       failed = true;
-               }
-               if (failed) {
-                       out.println("</div>");
-                       return false;
-               }
-               try {
-                       PreparedStatement q1 = DatabaseConnection.getInstance().prepare(
-                                       "select * from `email` where `email`=? and `deleted`=0");
-                       PreparedStatement q2 = DatabaseConnection.getInstance().prepare(
-                                       "select * from `users` where `email`=? and `deleted`=0");
-                       q1.setString(1, buildup.getEmail());
-                       q2.setString(1, buildup.getEmail());
-                       ResultSet r1 = q1.executeQuery();
-                       ResultSet r2 = q2.executeQuery();
-                       if (r1.next() || r2.next()) {
-                               outputError(out, req,
-                                               "This email address is currently valid in the system.");
-                               failed = true;
-                       }
-                       r1.close();
-                       r2.close();
-                       PreparedStatement q3 = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "select `domain` from `baddomains` where `domain`=RIGHT(?, LENGTH(`domain`))");
-                       q3.setString(1, buildup.getEmail());
+    String email = "";
 
-                       ResultSet r3 = q3.executeQuery();
-                       if (r3.next()) {
-                               String domain = r3.getString(1);
-                               out.print("<div>");
-                               out.print(String.format(
-                                               Page.translate(req,
-                                                               "We don't allow signups from people using email addresses from %s"),
-                                               domain));
-                               out.println("</div>");
-                               failed = true;
-                       }
-                       r3.close();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-                       failed = true;
-               }
-               String mailResult = EmailProvider.FAIL;
-               try {
-                       mailResult = EmailProvider.getInstance().checkEmailServer(0,
-                                       buildup.getEmail());
-               } catch (IOException e) {
-               }
-               if (!mailResult.equals(EmailProvider.OK)) {
-                       if (mailResult.startsWith("4")) {
-                               outputError(
-                                               out,
-                                               req,
-                                               "The mail server responsible for your domain indicated"
-                                                               + " a temporary failure. This may be due to anti-SPAM measures, such"
-                                                               + " as greylisting. Please try again in a few minutes.");
-                       } else {
-                               outputError(
-                                               out,
-                                               req,
-                                               "Email Address given was invalid, or a test connection"
-                                                               + " couldn't be made to your server, or the server"
-                                                               + " rejected the email address as invalid");
-                       }
-                       if (mailResult.equals(EmailProvider.FAIL)) {
-                               outputError(out, req,
-                                               "Failed to make a connection to the mail server");
-                       } else {
-                               out.print("<div>");
-                               out.print(mailResult);
-                               out.println("</div>");
-                       }
-                       failed = true;
-               }
+    private Template t;
 
-               out.println("</div>");
-               if (failed) {
-                       return false;
-               }
-               try {
-                       run(req, pw1);
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-               return true;
-       }
+    boolean general = true, country = true, regional = true, radius = true;
 
-       private void run(HttpServletRequest req, String password)
-                       throws SQLException {
-               try {
-                       DatabaseConnection.getInstance().beginTransaction();
-                       String hash = RandomToken.generateToken(16);
+    public Signup(HttpServletRequest hsr) {
+        super(hsr);
+        t = new Template(Signup.class.getResource("Signup.templ"));
+    }
 
-                       buildup.setDob(myDoB.getDate());
-                       buildup.insert(password);
-                       int memid = buildup.getId();
-                       PreparedStatement ps = DatabaseConnection.getInstance().prepare(
-                                       "insert into `email` set `email`=?,"
-                                                       + " `hash`=?, `created`=NOW(),`memid`=?");
-                       ps.setString(1, buildup.getEmail());
-                       ps.setString(2, hash);
-                       ps.setInt(3, memid);
-                       ps.execute();
-                       int emailid = DatabaseConnection.lastInsertId(ps);
-                       ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "insert into `alerts` set `memid`=?,"
-                                                                       + " `general`=?, `country`=?, `regional`=?, `radius`=?");
-                       ps.setInt(1, memid);
-                       ps.setString(2, general ? "1" : "0");
-                       ps.setString(3, country ? "1" : "0");
-                       ps.setString(4, regional ? "1" : "0");
-                       ps.setString(5, radius ? "1" : "0");
-                       ps.execute();
-                       Notary.writeUserAgreement(memid, "CCA", "account creation", "",
-                                       true, 0);
+    DateSelector myDoB = new DateSelector("day", "month", "year");
 
-                       StringBuffer body = new StringBuffer();
-                       body.append(Page
-                                       .translate(
-                                                       req,
-                                                       "Thanks for signing up with CAcert.org, below is the link you need to open to verify your account. Once your account is verified you will be able to start issuing certificates till your hearts' content!"));
-                       body.append("\n\n");
-                       body.append(ServerConstants.NORMAL_HOST_NAME);
-                       body.append("/verify?type=email&id=");
-                       body.append(emailid);
-                       body.append("&hash=");
-                       body.append(hash);
-                       body.append("\n\n");
-                       body.append(Page.translate(req, "Best regards"));
-                       body.append("\n");
-                       body.append(Page.translate(req, "CAcert.org Support!"));
-                       try {
-                               EmailProvider.getInstance().sendmail(buildup.getEmail(),
-                                               "[CAcert.org] " + Page.translate(req, "Mail Probe"),
-                                               body.toString(), "support@cacert.org", null, null,
-                                               null, null, false);
-                       } catch (IOException e) {
-                               e.printStackTrace();
-                       }
-                       DatabaseConnection.getInstance().commitTransaction();
-               } finally {
-                       DatabaseConnection.getInstance().quitTransaction();
-               }
+    @Override
+    public void outputContent(PrintWriter out, Language l, Map<String, Object> outerVars) {
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        vars.put("fname", HTMLEncoder.encodeHTML(buildupName.getFname()));
+        vars.put("mname", HTMLEncoder.encodeHTML(buildupName.getMname()));
+        vars.put("lname", HTMLEncoder.encodeHTML(buildupName.getLname()));
+        vars.put("suffix", HTMLEncoder.encodeHTML(buildupName.getSuffix()));
+        vars.put("dob", myDoB);
+        vars.put("email", HTMLEncoder.encodeHTML(email));
+        vars.put("general", general ? " checked=\"checked\"" : "");
+        vars.put("country", country ? " checked=\"checked\"" : "");
+        vars.put("regional", regional ? " checked=\"checked\"" : "");
+        vars.put("radius", radius ? " checked=\"checked\"" : "");
+        vars.put("helpOnNames", String.format(l.getTranslation("Help on Names %sin the wiki%s"), "<a href=\"//wiki.cacert.org/FAQ/HowToEnterNamesInJoinForm\" target=\"_blank\">", "</a>"));
+        vars.put("csrf", getCSRFToken());
+        vars.put("dobmin", User.MINIMUM_AGE + "");
+        t.output(out, l, vars);
+    }
 
-       }
+    private void update(HttpServletRequest r) {
+        String fname = buildupName.getFname();
+        String lname = buildupName.getLname();
+        String mname = buildupName.getMname();
+        String suffix = buildupName.getSuffix();
+        if (r.getParameter("fname") != null) {
+            fname = r.getParameter("fname");
+        }
+        if (r.getParameter("lname") != null) {
+            lname = r.getParameter("lname");
+        }
+        if (r.getParameter("mname") != null) {
+            mname = r.getParameter("mname");
+        }
+        if (r.getParameter("suffix") != null) {
+            suffix = r.getParameter("suffix");
+        }
+        if (r.getParameter("email") != null) {
+            email = r.getParameter("email");
+        }
+        buildupName = new Name(fname, lname, mname, suffix);
+        general = "1".equals(r.getParameter("general"));
+        country = "1".equals(r.getParameter("country"));
+        regional = "1".equals(r.getParameter("regional"));
+        radius = "1".equals(r.getParameter("radius"));
+        try {
+            myDoB.update(r);
+        } catch (GigiApiException e) {
+        }
+    }
+
+    @Override
+    public synchronized boolean submit(PrintWriter out, HttpServletRequest req) {
+        if (RegisterPage.RATE_LIMIT.isLimitExceeded(req.getRemoteAddr())) {
+            outputError(out, req, "Rate Limit Exceeded");
+            return false;
+        }
+
+        update(req);
+        if (buildupName.getLname().trim().equals("")) {
+            outputError(out, req, "Last name were blank.");
+        }
+        if ( !myDoB.isValid()) {
+            outputError(out, req, "Invalid date of birth");
+        }
+
+        if ( !CalendarUtil.isOfAge(myDoB.getDate(), User.MINIMUM_AGE)) {
+            outputError(out, req, "Entered dated of birth is below the restricted age requirements.");
+        }
+
+        if ( !"1".equals(req.getParameter("tos_agree"))) {
+            outputError(out, req, "Acceptance of the ToS is required to continue.");
+        }
+        if (email.equals("")) {
+            outputError(out, req, "Email Address was blank");
+        }
+        String pw1 = req.getParameter("pword1");
+        String pw2 = req.getParameter("pword2");
+        if (pw1 == null || pw1.equals("")) {
+            outputError(out, req, "Pass Phrases were blank");
+        } else if ( !pw1.equals(pw2)) {
+            outputError(out, req, "Pass Phrases don't match");
+        }
+        int pwpoints = PasswordStrengthChecker.checkpw(pw1, buildupName, email);
+        if (pwpoints < 3) {
+            outputError(out, req, "The Pass Phrase you submitted failed to contain enough" + " differing characters and/or contained words from" + " your name and/or email address.");
+        }
+        if (isFailed(out)) {
+            return false;
+        }
+        try (GigiPreparedStatement q1 = new GigiPreparedStatement("SELECT * FROM `emails` WHERE `email`=? AND `deleted` IS NULL"); GigiPreparedStatement q2 = new GigiPreparedStatement("SELECT * FROM `certOwners` INNER JOIN `users` ON `users`.`id`=`certOwners`.`id` WHERE `email`=? AND `deleted` IS NULL")) {
+            q1.setString(1, email);
+            q2.setString(1, email);
+            GigiResultSet r1 = q1.executeQuery();
+            GigiResultSet r2 = q2.executeQuery();
+            if (r1.next() || r2.next()) {
+                outputError(out, req, "This email address is currently valid in the system.");
+            }
+        }
+        try (GigiPreparedStatement q3 = new GigiPreparedStatement("SELECT `domain` FROM `baddomains` WHERE `domain`=RIGHT(?, LENGTH(`domain`))")) {
+            q3.setString(1, email);
+
+            GigiResultSet r3 = q3.executeQuery();
+            if (r3.next()) {
+                String domain = r3.getString(1);
+                outputError(out, req, "We don't allow signups from people using email addresses from %s", domain);
+            }
+        }
+        String mailResult = EmailProvider.FAIL;
+        try {
+            mailResult = HTMLEncoder.encodeHTML(EmailProvider.getInstance().checkEmailServer(0, email));
+        } catch (IOException e) {
+        }
+        if ( !mailResult.equals(EmailProvider.OK)) {
+            if (mailResult.startsWith("4")) {
+                outputError(out, req, "The mail server responsible for your domain indicated" + " a temporary failure. This may be due to anti-SPAM measures, such" + " as greylisting. Please try again in a few minutes.");
+            } else {
+                outputError(out, req, "Email Address given was invalid, or a test connection" + " couldn't be made to your server, or the server" + " rejected the email address as invalid");
+            }
+            if (mailResult.equals(EmailProvider.FAIL)) {
+                outputError(out, req, "Failed to make a connection to the mail server");
+            } else {
+                outputErrorPlain(out, mailResult);
+            }
+        }
+
+        if (isFailed(out)) {
+            return false;
+        }
+        try {
+            run(req, pw1);
+        } catch (SQLException e) {
+            e.printStackTrace();
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+            return false;
+        }
+        return true;
+    }
+
+    private void run(HttpServletRequest req, String password) throws SQLException, GigiApiException {
+        User u = new User(email, password, buildupName, myDoB.getDate(), Page.getLanguage(req).getLocale());
+
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `alerts` SET `memid`=?," + " `general`=?, `country`=?, `regional`=?, `radius`=?")) {
+            ps.setInt(1, u.getId());
+            ps.setBoolean(2, general);
+            ps.setBoolean(3, country);
+            ps.setBoolean(4, regional);
+            ps.setBoolean(5, radius);
+            ps.execute();
+        }
+        Notary.writeUserAgreement(u, "ToS", "account creation", "", true, 0);
+
+    }
 }
index 631215e313ac4737b07a03711b6d10112aa15812..cef5a309591fed534de6fd21c13709b2a915f102 100644 (file)
@@ -1,83 +1,83 @@
-<form method="post" action="/register" autocomplete="off">
-<table align="center" valign="middle" border="0" cellspacing="0" cellpadding="0" class="wrapper" width="400">
+<table class="table">
+  <thead>
   <tr>
-    <td colspan="3" class="title"><?=_My Details?></td>
+    <th colspan="3"><?=_My Details?></th>
   </tr>
-
+  </thead>
+  <tbody>
   <tr>
-    <td class="DataTD" width="125"><?=_First Name?>: </td>
-    <td class="DataTD" width="125"><input type="text" name="fname" size="30" value="<?=$fname?>" autocomplete="off"></td>
-    <td rowspan="4" class="DataTD" width="125"><?=$helpOnNames?></td>
+    <td><?=_First Name?>: </td>
+    <td><input class="form-control" type="text" name="fname" size="30" value="<?=$fname?>" autocomplete="off"></td>
+    <td rowspan="4"><?=$!helpOnNames?></td>
   </tr>
 
   <tr>
-    <td class="DataTD" valign="top"><?=_Middle Name(s)?><br>
+    <td><?=_Middle Name(s)?><br>
       (<?=_optional?>)
     </td>
-    <td class="DataTD"><input type="text" name="mname" size="30" value="<?=$mname?>" autocomplete="off"></td>
+    <td><input class="form-control" type="text" name="mname" size="30" value="<?=$mname?>" autocomplete="off"></td>
   </tr>
 
   <tr>
-    <td class="DataTD"><?=_Last Name?>: </td>
-    <td class="DataTD"><input type="text" name="lname" size="30" value="<?=$lname?>" autocomplete="off"></td>
+    <td><?=_Last Name?>: </td>
+    <td><input class="form-control" type="text" name="lname" size="30" value="<?=$lname?>" autocomplete="off"></td>
   </tr>
 
   <tr>
-    <td class="DataTD"><?=_Suffix?><br>
+    <td><?=_Suffix?><br>
       (<?=_optional?>)</td>
-    <td class="DataTD"><input type="text" name="suffix" size="30" value="<?=$suffix?>" autocomplete="off"><br><?=_Please only write Name Suffixes into this field.?></td>
+    <td><input class="form-control" type="text" name="suffix" size="30" value="<?=$suffix?>" autocomplete="off"><br><?=_Please only write Name Suffixes into this field.?></td>
   </tr>
 
   <tr>
-    <td class="DataTD"><?=_Date of Birth?><br>
-           (<?=_dd/mm/yyyy?>)</td>
-    <td class="DataTD"><?=$dob?></td>
-    <td class="DataTD">&nbsp;</td>
+    <td><?=_Date of Birth (minimum age: ${dobmin} years)?><br>
+           (<?=_yyyy-mm-dd?>)</td>
+    <td><?=$dob?></td>
+    <td>&nbsp;</td>
   </tr>
 
   <tr>
-    <td class="DataTD"><?=_Email Address?>: </td>
-    <td class="DataTD"><input type="text" name="email" size="30" value="<?=$email?>" autocomplete="off"></td>
-    <td class="DataTD"><?=_I own or am authorised to control this email address?></td>
+    <td><?=_Email Address?>: </td>
+    <td><input type="text" name="email" size="30" value="<?=$email?>" autocomplete="off"></td>
+    <td><?=_I own or am authorised to control this email address?></td>
   </tr>
 
   <tr>
-    <td class="DataTD"><?=_Pass Phrase?><font color="red">*</font>: </td>
-    <td class="DataTD"><input type="password" name="pword1" size="30" autocomplete="off"></td>
-    <td class="DataTD" rowspan="2">&nbsp;</td>
+    <td><?=_Pass Phrase?><font color="red">*</font>: </td>
+    <td><input class="form-control" type="password" name="pword1" size="30" autocomplete="off"></td>
+    <td rowspan="2">&nbsp;</td>
   </tr>
   <tr>
-    <td class="DataTD"><?=_Pass Phrase Again?><font color="red">*</font>: </td>
-    <td class="DataTD"><input type="password" name="pword2" size="30" autocomplete="off"></td>
+    <td><?=_Pass Phrase Again?><font color="red">*</font>: </td>
+    <td><input class="form-control" type="password" name="pword2" size="30" autocomplete="off"></td>
   </tr>
 
   <tr>
-    <td class="DataTD" colspan="3"><font color="red">*</font><?=_Please note, in the interests of good security, the pass phrase must be made up of an upper case letter, lower case letter, number and symbol.?></td>
+    <td colspan="3"><font color="red">*</font><?=_Please note, in the interests of good security, the pass phrase must be made up of an upper case letter, lower case letter, number and symbol.?></td>
   </tr>
   <tr>
-    <td class="DataTD" colspan="3"><?=_It's possible to get notifications of up and coming events and even just general announcements, untick any notifications you don't wish to receive. For country, regional and radius notifications to work you must choose your location once you've verified your account and logged in.?></td>
+    <td colspan="3"><?=_It's possible to get notifications of up and coming events and even just general announcements, untick any notifications you don't wish to receive. For country, regional and radius notifications to work you must choose your location once you've verified your account and logged in.?></td>
   </tr>
 
   <tr>
-    <td class="DataTD" valign="top"><?=_Alert me if?>: </td>
-    <td class="DataTD" align="left">
-        <input type="checkbox" name="general" value="1"<?=$general?>><?=_General Announcements?><br>
-       <input type="checkbox" name="country" value="1"<?=$country?>><?=_Country Announcements?><br>
-       <input type="checkbox" name="regional" value="1"<?=$regional?>><?=_Regional Announcements?><br>
-       <input type="checkbox" name="radius" value="1"<?=$radius?>><?=_Within 200km Announcements?></td>
-    <td class="DataTD">&nbsp;</td>
+    <td valign="top"><?=_Alert me if?>: </td>
+    <td align="left">
+        <input type="checkbox" name="general" value="1"<?=$!general?>><?=_General Announcements?><br>
+       <input type="checkbox" name="country" value="1"<?=$!country?>><?=_Country Announcements?><br>
+       <input type="checkbox" name="regional" value="1"<?=$!regional?>><?=_Regional Announcements?><br>
+       <input type="checkbox" name="radius" value="1"<?=$!radius?>><?=_Within 200km Announcements?></td>
+    <td>&nbsp;</td>
   </tr>
 
   <tr>
-    <td class="DataTD" colspan="3"><?=_When you click on next, we will send a confirmation email to the email address you have entered above.?></td>
+    <td colspan="3"><?=_When you click on next, we will send a confirmation email to the email address you have entered above.?></td>
   </tr>
   <tr>
-    <td class="DataTD" colspan="3"><input type="checkbox" name="cca_agree" value="1"><?=_I agree to the terms and conditions of the CAcert Community Agreement?>: <a href="/policy/CAcertCommunityAgreement.php">http://www.cacert.org/policy/CAcertCommunityAgreement.php</a></td>
+    <td colspan="3"><input type="checkbox" name="tos_agree" value="1"><?=_I agree to the Terms of Service (!'<a href="/policy/TermsOfService.html">'ToS!'</a>').?></td>
   </tr>
 
   <tr>
-    <td class="DataTD" colspan="3"><input type="submit" name="process" value="<?=_Next?>"></td>
+    <td colspan="3"><input type="submit" name="process" value="<?=_Next?>"></td>
   </tr>
-
+  </tbody>
 </table>
-</form>
diff --git a/src/org/cacert/gigi/pages/orga/AffiliationForm.java b/src/org/cacert/gigi/pages/orga/AffiliationForm.java
new file mode 100644 (file)
index 0000000..c8dd3e5
--- /dev/null
@@ -0,0 +1,78 @@
+package org.cacert.gigi.pages.orga;
+
+import java.io.PrintWriter;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.Organisation.Affiliation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+
+public class AffiliationForm extends Form {
+
+    Organisation o;
+
+    private static final Template t = new Template(AffiliationForm.class.getResource("AffiliationForm.templ"));
+
+    public AffiliationForm(HttpServletRequest hsr, Organisation o) {
+        super(hsr);
+        this.o = o;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        if (req.getParameter("del") != null) {
+            User toRemove = User.getByEmail(req.getParameter("del"));
+            if (toRemove != null) {
+                o.removeAdmin(toRemove, LoginPage.getUser(req));
+                return true;
+            }
+        } else if (req.getParameter("do_affiliate") != null) {
+            User byEmail = User.getByEmail(req.getParameter("email"));
+            if (byEmail != null && byEmail.canAssure()) {
+                o.addAdmin(byEmail, LoginPage.getUser(req), req.getParameter("master") != null);
+                return true;
+            } else {
+                out.println(Page.getLanguage(req).getTranslation("Requested user is not an assurer. We need an assurer here."));
+            }
+        }
+        out.println(Page.getLanguage(req).getTranslation("No action could have been carried out."));
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        final List<Affiliation> admins = o.getAllAdmins();
+        vars.put("admins", new IterableDataset() {
+
+            Iterator<Affiliation> iter = admins.iterator();
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if ( !iter.hasNext()) {
+                    return false;
+                }
+                Affiliation aff = iter.next();
+                vars.put("name", aff.getTarget().getName());
+                vars.put("master", aff.isMaster() ? l.getTranslation("master") : "");
+                vars.put("e-mail", aff.getTarget().getEmail());
+                return true;
+            }
+        });
+        t.output(out, l, vars);
+    }
+
+    public Organisation getOrganisation() {
+        return o;
+    }
+}
diff --git a/src/org/cacert/gigi/pages/orga/AffiliationForm.templ b/src/org/cacert/gigi/pages/orga/AffiliationForm.templ
new file mode 100644 (file)
index 0000000..a67730f
--- /dev/null
@@ -0,0 +1,22 @@
+<table class="table">
+  <tr>
+    <th><?=_Name?></th>
+    <th><?=_Email?></th>
+    <th><?=_Master?></th>
+    <th> </th>
+  </tr>
+<? foreach($admins) { ?>
+  <tr>
+    <td><?=$name?></td>
+    <td><?=$e-mail?></td>
+    <td><?=$master?></td>
+    <td><button type="submit" name="del" value="<?=$e-mail?>">X</button> </td>
+  </tr>
+<? } ?>
+  <tr>
+    <td></td>
+    <td><input class="form-control" type="text" name="email"></td>
+    <td><input type="checkbox" name="master" value="y"></td>
+    <td><input type="submit" name="do_affiliate" value="<?=_Add?>"></td>
+  </tr>
+</table>
diff --git a/src/org/cacert/gigi/pages/orga/CreateOrgForm.java b/src/org/cacert/gigi/pages/orga/CreateOrgForm.java
new file mode 100644 (file)
index 0000000..5e6b35a
--- /dev/null
@@ -0,0 +1,118 @@
+package org.cacert.gigi.pages.orga;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+
+public class CreateOrgForm extends Form {
+
+    private final static Template t = new Template(CreateOrgForm.class.getResource("CreateOrgForm.templ"));
+
+    private Organisation result;
+
+    private String o = "";
+
+    private String c = "";
+
+    private String st = "";
+
+    private String l = "";
+
+    private String email = "";
+
+    private String optionalName = "";
+
+    private String postalAddress = "";
+
+    private boolean isEdit = false;
+
+    public CreateOrgForm(HttpServletRequest hsr) {
+        super(hsr);
+    }
+
+    public CreateOrgForm(HttpServletRequest hsr, Organisation t) {
+        super(hsr);
+        isEdit = true;
+        result = t;
+        o = t.getName();
+        c = t.getState();
+        st = t.getProvince();
+        l = t.getCity();
+        email = t.getContactEmail();
+        optionalName = t.getOptionalName();
+        postalAddress = t.getPostalAddress();
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        String action = req.getParameter("action");
+        if (action == null) {
+            return false;
+        }
+        if (action.equals("new")) {
+            o = req.getParameter("O");
+            c = req.getParameter("C");
+            st = req.getParameter("ST");
+            l = req.getParameter("L");
+            email = req.getParameter("contact");
+            optionalName = req.getParameter("optionalName");
+            postalAddress = req.getParameter("postalAddress");
+
+            Organisation ne = new Organisation(o, c, st, l, email, optionalName, postalAddress, LoginPage.getUser(req));
+            result = ne;
+            return true;
+        } else if (action.equals("updateOrganisationData")) {
+            updateOrganisationData(out, req);
+            return true;
+        } else if (action.equals("updateCertificateData")) {
+            updateCertificateData(out, req);
+            return true;
+        }
+
+        return false;
+    }
+
+    private void updateOrganisationData(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        email = req.getParameter("contact");
+        optionalName = req.getParameter("optionalName");
+        postalAddress = req.getParameter("postalAddress");
+
+        result.updateOrgData(email, optionalName, postalAddress);
+    }
+
+    private void updateCertificateData(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        o = req.getParameter("O");
+        c = req.getParameter("C");
+        st = req.getParameter("ST");
+        l = req.getParameter("L");
+
+        result.updateCertData(o, c, st, l);
+    }
+
+    public Organisation getResult() {
+        return result;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        vars.put("O", o);
+        vars.put("C", c);
+        vars.put("ST", st);
+        vars.put("L", this.l);
+        vars.put("email", email);
+        vars.put("optionalName", optionalName);
+        vars.put("postalAddress", postalAddress);
+        if (isEdit) {
+            vars.put("edit", true);
+        }
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/orga/CreateOrgForm.templ b/src/org/cacert/gigi/pages/orga/CreateOrgForm.templ
new file mode 100644 (file)
index 0000000..bd0f74d
--- /dev/null
@@ -0,0 +1,72 @@
+<table class="table">
+  <tr>
+    <th colspan="2">
+    <? if($edit) { ?>
+    <?=_Edit Organisation?>
+    <? } else { ?>
+    <?=_New Organisation?>
+    <? } ?></th>
+  </tr>
+  <tr>
+    <th colspan="2"><?=_Certificate data, all fields need to be filled?></th>
+  </tr>
+  <tr>
+    <td><?=_Organisation Name?>:</td>
+    <td><input class="form-control" type="text" name="O" value="<?=$O?>" maxlength="64" size="90">
+        <?=_(max. 64 characters)?>
+    </td>
+  </tr>
+  <tr>
+    <td><?=_Town/Suburb?>:</td>
+    <td><input class="form-control" type="text" name="L" value="<?=$L?>" maxlength="255" size="90"></td>
+  </tr>
+  <tr>
+    <td><?=_State/Province?>:</td>
+    <td><input class="form-control" type="text" name="ST" value="<?=$ST?>" maxlength="255" size="90"></td>
+  </tr>
+  <tr>
+    <td><?=_Country?>:</td>
+    <td><input class="form-control" type="text" name="C" value="<?=$C?>" maxlength="2" size="5">
+      <?=_(2 letter !'<a href="http://www.iso.org/iso/home/standards/country_codes/iso-3166-1_decoding_table.htm">'ISO code!'</a>')?>
+    </td>
+  </tr>
+  <? if($edit) { ?>
+  <tr>
+    <td></td>
+    <td><div class="alert alert-warning"><?=_WARNING: updating the data will revoke all issued certificates.?></div></td>
+  </tr>
+  <tr>
+    <td colspan="2"><button type="submit" name="action" value="updateCertificateData" class="btn btn-primary"><?=_Update certificate data?></button></td>
+  </tr>
+  <? } ?>
+  <tr>
+    <th colspan="2"><?=_Organisation data?></th>
+  </tr>
+  <tr>
+    <td><?=_Organisation name?>:</td>
+    <td><input class="form-control" type="text" name="optionalName" value="<?=$optionalName?>" maxlength="255" size="90">
+      <?=_Optional: full organisation name (if organisation name is longer than 64 characters)?>
+    </td>
+  </tr>
+    <tr>
+    <td><?=_Postal address?>:</td>
+    <td><textarea class="form-control" name="postalAddress" cols="60" rows="5"><?=$postalAddress?></textarea></td>
+  </tr>
+  <tr>
+    <td><?=_Contact Email?>:</td>
+    <td><input class="form-control" type="text" name="contact" value="<?=$email?>" maxlength="255" size="90"></td>
+  </tr>
+  <tr>
+    <td><?=_Comments?>:</td>
+    <td><textarea class="form-control" name="comments" cols="60" rows="10"></textarea></td>
+  </tr>
+  <? if($edit) { ?>
+  <tr>
+    <td colspan="2"><button type="submit" name="action" value="updateOrganisationData" class="btn btn-primary"><?=_Update organisation data?></button></td>
+  </tr>
+  <? } else {?>
+  <tr>
+    <td colspan="2"><button type="submit" name="action" value="new" class="btn btn-primary"><?=_Submit?></button></td>
+  </tr>
+  <? } ?>
+</table>
diff --git a/src/org/cacert/gigi/pages/orga/CreateOrgPage.java b/src/org/cacert/gigi/pages/orga/CreateOrgPage.java
new file mode 100644 (file)
index 0000000..4d6a387
--- /dev/null
@@ -0,0 +1,47 @@
+package org.cacert.gigi.pages.orga;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class CreateOrgPage extends Page {
+
+    public static final Group ORG_ASSURER = Group.getByString("orgassurer");
+
+    public static final String DEFAULT_PATH = "/orga/new";
+
+    public CreateOrgPage() {
+        super("Create Organisation");
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.isInGroup(ORG_ASSURER);
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            CreateOrgForm form = Form.getForm(req, CreateOrgForm.class);
+            if (form.submit(resp.getWriter(), req)) {
+                resp.sendRedirect(ViewOrgPage.DEFAULT_PATH + "/" + form.getResult().getId());
+                return;
+            }
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        new CreateOrgForm(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+    }
+}
diff --git a/src/org/cacert/gigi/pages/orga/EditOrg.templ b/src/org/cacert/gigi/pages/orga/EditOrg.templ
new file mode 100644 (file)
index 0000000..36e683f
--- /dev/null
@@ -0,0 +1,5 @@
+<? if($editForm) { ?><?=$editForm?>
+<br/><? } else { ?><h1><?=$orgName?></h1><? } ?>
+<?=$affForm?>
+<br/>
+<? if($addDom) { ?><?=$mgmDom?><?=$addDom?><? } ?>
diff --git a/src/org/cacert/gigi/pages/orga/OrgDomainAddForm.java b/src/org/cacert/gigi/pages/orga/OrgDomainAddForm.java
new file mode 100644 (file)
index 0000000..b3df26e
--- /dev/null
@@ -0,0 +1,42 @@
+package org.cacert.gigi.pages.orga;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+
+public class OrgDomainAddForm extends Form {
+
+    public static Template t = new Template(OrgDomainAddForm.class.getResource("OrgDomainAddForm.templ"));
+
+    Organisation target;
+
+    public OrgDomainAddForm(HttpServletRequest hsr, Organisation target) {
+        super(hsr);
+        this.target = target;
+    }
+
+    public Organisation getOrganisation() {
+        return target;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        String domain = req.getParameter("domain");
+        new Domain(LoginPage.getUser(req), target, domain);
+        return true;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        t.output(out, l, vars);
+    }
+}
diff --git a/src/org/cacert/gigi/pages/orga/OrgDomainAddForm.templ b/src/org/cacert/gigi/pages/orga/OrgDomainAddForm.templ
new file mode 100644 (file)
index 0000000..dc38aec
--- /dev/null
@@ -0,0 +1,10 @@
+<table class="table">
+  <tr>
+    <th><?=_Add Domain:?></td>
+    <td><input class="form-control" type="text" name="domain"></td>
+  </tr>
+  <tr>
+    <td></td>
+    <td><button class="btn btn-primary" type="submit" name="addDomain" value="action"><?=_Add?></button></td>
+  </tr>
+</table>
diff --git a/src/org/cacert/gigi/pages/orga/ViewOrgPage.java b/src/org/cacert/gigi/pages/orga/ViewOrgPage.java
new file mode 100644 (file)
index 0000000..49833e5
--- /dev/null
@@ -0,0 +1,145 @@
+package org.cacert.gigi.pages.orga;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.pages.account.domain.DomainManagementForm;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class ViewOrgPage extends Page {
+
+    private final Template orgas = new Template(ViewOrgPage.class.getResource("ViewOrgs.templ"));
+
+    private final Template mainTempl = new Template(ViewOrgPage.class.getResource("EditOrg.templ"));
+
+    public static final String DEFAULT_PATH = "/orga";
+
+    public ViewOrgPage() {
+        super("View Organisation");
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && (ac.isInGroup(CreateOrgPage.ORG_ASSURER) || ac.getActor().getOrganisations(true).size() != 0);
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            User u = LoginPage.getUser(req);
+            if (req.getParameter("do_affiliate") != null || req.getParameter("del") != null) {
+                AffiliationForm form = Form.getForm(req, AffiliationForm.class);
+                if (form.submit(resp.getWriter(), req)) {
+                    resp.sendRedirect(DEFAULT_PATH + "/" + form.getOrganisation().getId());
+                }
+                return;
+            } else {
+                if ( !u.isInGroup(CreateOrgPage.ORG_ASSURER)) {
+                    resp.sendError(403, "Access denied");
+                    return;
+                }
+
+                if (req.getParameter("addDomain") != null) {
+                    OrgDomainAddForm form = Form.getForm(req, OrgDomainAddForm.class);
+                    if (form.submit(resp.getWriter(), req)) {
+                        resp.sendRedirect(DEFAULT_PATH + "/" + form.getOrganisation().getId());
+                    }
+                } else if (req.getParameter("delete") != null) {
+                    DomainManagementForm form = Form.getForm(req, DomainManagementForm.class);
+                    if (form.submit(resp.getWriter(), req)) {
+                        resp.sendRedirect(DEFAULT_PATH + "/" + form.getTarget().getId());
+                    }
+                } else {
+                    CreateOrgForm form = Form.getForm(req, CreateOrgForm.class);
+                    if (form.submit(resp.getWriter(), req)) {
+                        resp.sendRedirect(DEFAULT_PATH + "/" + form.getResult().getId());
+                    }
+                }
+            }
+
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        User u = LoginPage.getUser(req);
+        String idS = req.getPathInfo();
+        Language lang = getLanguage(req);
+        PrintWriter out = resp.getWriter();
+        if (idS.length() < DEFAULT_PATH.length() + 2) {
+            final Organisation[] orgas = Organisation.getOrganisations(0, 30);
+            HashMap<String, Object> map = new HashMap<>();
+            final List<Organisation> myOrgs = u.getOrganisations(true);
+            final boolean orgAss = u.isInGroup(CreateOrgPage.ORG_ASSURER);
+            if (orgAss) {
+                map.put("orgas", makeOrgDataset(orgas));
+            } else {
+                map.put("orgas", makeOrgDataset(myOrgs.toArray(new Organisation[myOrgs.size()])));
+            }
+            this.orgas.output(out, lang, map);
+            return;
+        }
+        idS = idS.substring(DEFAULT_PATH.length() + 1);
+        int id = Integer.parseInt(idS);
+        Organisation o;
+        try {
+            o = Organisation.getById(id);
+        } catch (IllegalArgumentException e) {
+            resp.sendError(404);
+            return;
+        }
+        final List<Organisation> myOrgs = u.getOrganisations();
+        final boolean orgAss = u.isInGroup(CreateOrgPage.ORG_ASSURER);
+        if ( !orgAss && !myOrgs.contains(o)) {
+            resp.sendError(404);
+            return;
+        }
+        HashMap<String, Object> vars = new HashMap<>();
+        if (orgAss) {
+            vars.put("editForm", new CreateOrgForm(req, o));
+            vars.put("affForm", new AffiliationForm(req, o));
+            vars.put("mgmDom", new DomainManagementForm(req, o, true));
+            vars.put("addDom", new OrgDomainAddForm(req, o));
+        } else {
+            vars.put("affForm", new AffiliationForm(req, o));
+            vars.put("orgName", o.getName());
+        }
+        mainTempl.output(out, lang, vars);
+    }
+
+    private IterableDataset makeOrgDataset(final Organisation[] orgas) {
+        return new IterableDataset() {
+
+            int count = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if (count >= orgas.length) {
+                    return false;
+                }
+                Organisation org = orgas[count++];
+                vars.put("id", Integer.toString(org.getId()));
+                vars.put("name", org.getName());
+                vars.put("country", org.getState());
+                return true;
+            }
+        };
+    }
+}
diff --git a/src/org/cacert/gigi/pages/orga/ViewOrgs.templ b/src/org/cacert/gigi/pages/orga/ViewOrgs.templ
new file mode 100644 (file)
index 0000000..e7465aa
--- /dev/null
@@ -0,0 +1,8 @@
+<table class="table">
+<? foreach($orgas) { ?>
+  <tr>
+    <td><?=$country?></td>
+    <td><a href='orga/<?=$id?>'><?=$name?></a></td>
+  </tr>
+<? } ?>
+</table>
index b3546fb9a54f330dce38c771688e7db2388cf4d2..79f4d509ac263bad0775a04a91871ddf2aebede9 100644 (file)
 package org.cacert.gigi.pages.wot;
 
-import java.io.InputStreamReader;
 import java.io.PrintWriter;
-import java.sql.SQLException;
-import java.text.ParseException;
 import java.text.SimpleDateFormat;
-import java.util.Date;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 
-import org.cacert.gigi.Language;
-import org.cacert.gigi.User;
-import org.cacert.gigi.output.Form;
-import org.cacert.gigi.output.Template;
-import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Assurance.AssuranceType;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.pages.PasswordResetPage;
+import org.cacert.gigi.util.DayDate;
 import org.cacert.gigi.util.Notary;
 
 public class AssuranceForm extends Form {
-       User assuree;
-       static final Template templ;
-       static {
-               templ = new Template(new InputStreamReader(
-                               AssuranceForm.class.getResourceAsStream("AssuranceForm.templ")));
-       }
-
-       public AssuranceForm(int assuree) {
-               this.assuree = new User(assuree);
-       }
-       SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-
-       @Override
-       public void output(PrintWriter out, Language l, Map<String, Object> vars) {
-               HashMap<String, Object> res = new HashMap<String, Object>();
-               res.putAll(vars);
-               res.put("name", assuree.getName());
-               try {
-                       res.put("maxpoints", assuree.getMaxAssurePoints());
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-               res.put("dob", sdf.format(assuree.getDob()));
-               templ.output(out, l, res);
-       }
-
-       @Override
-       public boolean submit(PrintWriter out, HttpServletRequest req) {
-               out.println("<div class='formError'>");
-               boolean failed = false;
-
-               if (!"1".equals(req.getParameter("certify"))
-                               || !"1".equals(req.getParameter("rules"))
-                               || !"1".equals(req.getParameter("CCAAgreed"))
-                               || !"1".equals(req.getParameter("assertion"))) {
-                       outputError(out, req, "You failed to check all boxes to validate"
-                                       + " your adherence to the rules and policies of CAcert");
-                       failed = true;
-
-               }
-               if (req.getParameter("date") == null
-                               || req.getParameter("date").equals("")) {
-                       outputError(out, req,
-                                       "You must enter the date when you met the assuree.");
-                       failed = true;
-               } else {
-                       try {
-                               Date d = sdf.parse(req.getParameter("date"));
-                               if (d.getTime() > System.currentTimeMillis()) {
-                                       outputError(out, req,
-                                                       "You must not enter a date in the future.");
-                                       failed = true;
-                               }
-                       } catch (ParseException e) {
-                               outputError(out, req,
-                                               "You must enter the date in this format: YYYY-MM-DD.");
-                               failed = true;
-                       }
-               }
-               // check location, min 3 characters
-               if (req.getParameter("location") == null
-                               || req.getParameter("location").equals("")) {
-                       outputError(out, req,
-                                       "You failed to enter a location of your meeting.");
-                       failed = true;
-               } else if (req.getParameter("location").length() <= 2) {
-                       outputError(out, req,
-                                       "You must enter a location with at least 3 characters eg town and country.");
-                       failed = true;
-               }
-               // TODO checkPoints
-               String points = req.getParameter("points");
-               if (points == null || "".equals(points)) {
-                       // TODO message
-                       failed = true;
-               }
-               if (failed) {
-                       out.println("</div>");
-                       return false;
-               }
-               try {
-                       boolean success = Notary.assure(LoginPage.getUser(req), assuree,
-                                       Integer.parseInt(req.getParameter("points")),
-                                       req.getParameter("location"), req.getParameter("date"));
-                       if (!success) {
-                               outputError(out, req,
-                                               "Assurance failed. Maybe user data changed.");
-                       }
-                       out.println("</div>");
-                       return success;
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-
-               out.println("</div>");
-               return false;
-       }
+
+    private User assuree;
+
+    private Name assureeName;
+
+    private DayDate dob;
+
+    private String location = "";
+
+    private String date = "";
+
+    private String aword;
+
+    private User assurer;
+
+    private AssuranceType type = AssuranceType.FACE_TO_FACE;
+
+    private static final Template templ;
+    static {
+        templ = new Template(AssuranceForm.class.getResource("AssuranceForm.templ"));
+    }
+
+    public AssuranceForm(HttpServletRequest hsr, User assuree) {
+        super(hsr);
+        assurer = Page.getUser(hsr);
+        this.assuree = assuree;
+        assureeName = this.assuree.getName();
+        dob = this.assuree.getDoB();
+    }
+
+    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+
+    SimpleDateFormat sdf2 = new SimpleDateFormat("dd. MMM yyyy");
+
+    @Override
+    public void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        HashMap<String, Object> res = new HashMap<String, Object>();
+        res.putAll(vars);
+        res.put("nameExplicit", assuree.getName());
+        res.put("name", assuree.getName().toString());
+        res.put("maxpoints", assurer.getMaxAssurePoints());
+        res.put("dob", sdf.format(assuree.getDoB().toDate()));
+        res.put("dobFmt2", sdf2.format(assuree.getDoB().toDate()));
+        res.put("location", location);
+        res.put("date", date);
+        res.put("aword", aword);
+        final LinkedList<AssuranceType> ats = new LinkedList<>();
+        for (AssuranceType at : AssuranceType.values()) {
+            try {
+                Notary.may(assurer, assuree, at);
+                ats.add(at);
+            } catch (GigiApiException e) {
+            }
+        }
+        res.put("ats", new IterableDataset() {
+
+            Iterator<AssuranceType> t = ats.iterator();
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                if ( !t.hasNext()) {
+                    return false;
+                }
+                AssuranceType t1 = t.next();
+                vars.put("type", t1.getDescription());
+                vars.put("id", t1.toString());
+                vars.put("sel", t1 == type ? " selected" : "");
+                return true;
+            }
+        });
+        templ.output(out, l, res);
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        location = req.getParameter("location");
+        date = req.getParameter("date");
+        if (date == null || location == null) {
+            outputError(out, req, "You need to enter location and date!");
+        }
+
+        if ( !"1".equals(req.getParameter("certify")) || !"1".equals(req.getParameter("rules")) || !"1".equals(req.getParameter("tos_agree")) || !"1".equals(req.getParameter("assertion"))) {
+            outputError(out, req, "You failed to check all boxes to validate" + " your adherence to the rules and policies of SomeCA");
+        }
+        if ("1".equals(req.getParameter("passwordReset"))) {
+            aword = req.getParameter("passwordResetValue");
+            if ("".equals(aword)) {
+                aword = null;
+            }
+        } else {
+            aword = null;
+        }
+        String val = req.getParameter("assuranceType");
+        if (val != null) {
+            try {
+                type = AssuranceType.valueOf(val);
+            } catch (IllegalArgumentException e) {
+                outputError(out, req, "Assurance Type wrong.");
+            }
+        }
+
+        int pointsI = 0;
+        String points = req.getParameter("points");
+        if (points == null || "".equals(points)) {
+            outputError(out, req, "For an assurance, you need to enter points.");
+        } else {
+            try {
+                pointsI = Integer.parseInt(points);
+            } catch (NumberFormatException e) {
+                outputError(out, req, "The points entered were not a number.");
+            }
+        }
+
+        if (isFailed(out)) {
+            return false;
+        }
+        try {
+            Notary.assure(assurer, assuree, assureeName, dob, pointsI, location, req.getParameter("date"), type);
+            if (aword != null && !aword.equals("")) {
+                Language l = Language.getInstance(assuree.getPreferredLocale());
+                String method = l.getTranslation("A password reset was triggered. If you did a password reset by assurance, please enter your secret password using this form:");
+                String subject = l.getTranslation("Password reset by assurance");
+                PasswordResetPage.initPasswordResetProcess(out, assuree, req, aword, l, method, subject);
+            }
+            return true;
+        } catch (GigiApiException e) {
+            e.format(out, Page.getLanguage(req));
+        }
+
+        return false;
+    }
+
+    public User getAssuree() {
+        return assuree;
+    }
+
 }
index c3df60c6dfc62f9cece2349f191d680566941b2d..2e346be390c18645e01a6f641549e06ee6dfefb4 100644 (file)
@@ -1,58 +1,68 @@
-<form method="POST">
-<table class="wrapper" width="600">
-<tr><td colspan="2" class="title"><?=_Assurance Confirmation?></td></tr>
-<tr><td colspan="2" class="DataTD"><?=s,$name,Please check the following details match against what you witnessed when you met %s in person. You MUST NOT proceed unless you are sure the details are correct. You may be held responsible by the CAcert Arbitrator for any issues with this Assurance.?>
+<table class="table">
+<thead>
+<tr><th colspan="2"><?=_Assurance Confirmation?></th></tr>
+</thead>
+<tbody>
+<tr><td colspan="2"><?=_Please check the following details match against what you witnessed when you met ${name} in person. You MUST NOT proceed unless you are sure the details are correct. You may be held responsible by the SomeCA Arbitrator for any issues with this Assurance.?>
 </td></tr>
 
        <tr>
-               <td class="DataTD"><?=_Name?>: </td>
-               <td class="DataTD"><span class="accountdetail"><?=$name?></span></td>
+               <td><?=_Name?>: </td>
+               <td><span class="accountdetail"><?=$nameExplicit?></span></td>
        </tr>
        <tr>
-               <td class="DataTD"><?=_Date of Birth?>: </td>
-               <td class="DataTD"><span class="accountdetail dob"><?=$dob?></span></td>
+               <td><?=_Date of Birth?>: </td>
+               <td><span class="accountdetail dob"><?=$dob?> (<?=$dobFmt2?>)</span></td>
        </tr>
        <tr>
-               <td class="DataTD"><input type="checkbox" name="certify" value="1"></td>
-               <td class="DataTD"><?=s,$name,I certify that %s has appeared in person.?></td>
+               <td><input type="checkbox" name="certify" value="1"></td>
+               <td><?=_I certify that ${name} has appeared in person.?></td>
        </tr>
        <tr>
-               <td class="DataTD"><input type="checkbox" name="CCAAgreed" value="1"></td>
-               <td class="DataTD"><?=s,$name,I verify that %s has accepted the CAcert Community Agreement.?></td>
+               <td><input type="checkbox" name="tos_agree" value="1"></td>
+               <td><?=_I verify that ${name} has accepted the Terms of Service (!'<a href="/policy/TermsOfService.html">'ToS!'</a>').?></td>
        </tr>
        <tr>
-               <td class="DataTD"><?=_Location?></td>
-               <td class="DataTD"><input type="text" name="location"></td>
+               <td><?=_Location?></td>
+               <td><input class="form-control" type="text" name="location" value="<?=$location?>"></td>
        </tr>
        <tr>
-               <td class="DataTD"><?=_Date?></td>
-               <td class="DataTD"><input type="text" name="date"><br/><?=_The date when the assurance took place. Please adjust the date if you assured the person on a different day (YYYY-MM-DD).?></td>
+               <td><?=_Date?></td>
+               <td><input class="form-control" type="text" name="date" value="<?=$date?>"><br/><?=_The date when the assurance took place. Please adjust the date if you assured the person on a different day (YYYY-MM-DD).?></td>
        </tr>
        <tr>
-               <td class="DataTD"><input type="checkbox" name="assertion" value="1"></td>
-               <td class="DataTD"><?=_I believe that the assertion of identity I am making is correct, complete and verifiable. I have seen original documentation attesting to this identity. I accept that the CAcert Arbitrator may call upon me to provide evidence in any dispute, and I may be held responsible.?></td>
+               <td><input type="checkbox" name="assertion" value="1"></td>
+               <td><?=_I believe that the assertion of identity I am making is correct, complete and verifiable. I have seen original documentation attesting to this identity. I accept that the SomeCA Arbitrator may call upon me to provide evidence in any dispute, and I may be held responsible.?></td>
        </tr>
        <tr>
-               <td class="DataTD"><input type="checkbox" name="rules" value="1"></td>
-               <td class="DataTD"><?=_I have read and understood the CAcert Community Agreement (CCA), Assurance Policy and the Assurance Handbook. I am making this Assurance subject to and in compliance with the CCA, Assurance policy and handbook.?></td>
+               <td><input type="checkbox" name="rules" value="1"></td>
+               <td><?=_I have read and understood the Terms of Service (!'<a href="/policy/TermsOfService.html">'ToS!'</a>'), Assurance Policy and the Assurance Handbook. I am making this Assurance subject to and in compliance with the ToS, Assurance policy and handbook.?></td>
        </tr>
        <tr>
-               <td class="DataTD"><?=_Policy?>: </td>
-               <td class="DataTD">
-                       <a href="/policy/CAcertCommunityAgreement.php" target="_blank"><?=_CAcert Community Agreement?></a>
+               <td><?=_Policy?>: </td>
+               <td>
+                       <a href="/policy/TermsOfService.html" target="_blank"><?=_Terms of Service?></a>
                         - <a href="/policy/AssurancePolicy.php" target="_blank"><?=_Assurance Policy?></a>
                         - <a href="http://wiki.cacert.org/AssuranceHandbook2" target="_blank"><?=_Assurance Handbook?></a>
                </td>
        </tr>
        <tr>
-               <td class="DataTD"><?=_Points?></td>
-               <td class="DataTD"><input type="text" name="points"><br/>(Max. <?=$maxpoints?>)</td>
+               <td><?=_Points?></td>
+               <td><input class="form-control" type="text" name="points"><br/>(Max. <?=$maxpoints?>)</td>
        </tr>
        <tr>
-               <td class="DataTD" colspan="2">
+               <td><?=_Type?></td>
+               <td><select name="assuranceType"><? foreach($ats) { ?><option value="<?=$id?>"<?=$sel?>><?=$type?></option><? } ?></select></td>
+       </tr>
+       <tr>
+               <td><input type="checkbox" name="passwordReset" value="1" <? if($aword) { ?>checked<? } ?>></td>
+               <td><?=_I have conducted a passwort reset with assurance. The established "A-Word" is:?><input type="text" name="passwordResetValue" value="<? if($aword) { ?><?=$aword?><? } ?>"></td>
+       </tr>
+       <tr>
+               <td colspan="2">
                        <input type="submit" name="process" value="<?=_I confirm this Assurance?>" />
                        <input type="submit" name="cancel" value="<?=_Cancel?>" />
                </td>
        </tr>
+       </tbody>
 </table>
-</form>
index 8862535c97aa6a6ffd404ae49e4c48c562bb8ee9..94c582f220e5765f19c3d2d2d7d05d8182646d9c 100644 (file)
 package org.cacert.gigi.pages.wot;
 
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.PrintWriter;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
+import java.sql.Date;
+import java.util.Calendar;
 import java.util.HashMap;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
 
-import org.cacert.gigi.User;
-import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.User;
 import org.cacert.gigi.output.DateSelector;
-import org.cacert.gigi.output.Template;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
 import org.cacert.gigi.pages.LoginPage;
 import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
 import org.cacert.gigi.util.Notary;
 
 public class AssurePage extends Page {
-       public static final String PATH = "/wot/assure";
-       public static final String SESSION = "/wot/assure/FORM";
-       DateSelector ds = new DateSelector("day", "month", "year");
-       Template t;
-
-       public AssurePage() {
-               super("Assure someone");
-               t = new Template(new InputStreamReader(
-                               AssuranceForm.class.getResourceAsStream("AssureeSearch.templ")));
-
-       }
-
-       @Override
-       public void doGet(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-
-               PrintWriter out = resp.getWriter();
-               String pi = req.getPathInfo().substring(PATH.length());
-               if (pi.length() > 1) {
-                       User myself = LoginPage.getUser(req);
-                       int mid = Integer.parseInt(pi.substring(1));
-
-                       if (!Notary.checkAssuranceIsPossible(myself, new User(mid), out)) {
-                               return;
-                       }
-                       HttpSession hs = req.getSession();
-                       AssuranceForm form = (AssuranceForm) hs.getAttribute(SESSION);
-                       if (form == null || form.assuree.getId() != mid) {
-                               form = new AssuranceForm(mid);
-                               hs.setAttribute(SESSION, form);
-                       }
-
-                       form.output(out, getLanguage(req), new HashMap<String, Object>());;
-               } else {
-                       HashMap<String, Object> vars = new HashMap<String, Object>();
-                       vars.put("DoB", ds);
-                       t.output(out, getLanguage(req), vars);
-               }
-       }
-       @Override
-       public void doPost(HttpServletRequest req, HttpServletResponse resp)
-                       throws IOException {
-               PrintWriter out = resp.getWriter();
-               String pi = req.getPathInfo().substring(PATH.length());
-               if (pi.length() > 1) {
-                       User myself = LoginPage.getUser(req);
-                       int mid = Integer.parseInt(pi.substring(1));
-                       if (mid == myself.getId()) {
-                               out.println("Cannot assure myself.");
-                               return;
-                       }
-
-                       AssuranceForm form = (AssuranceForm) req.getSession().getAttribute(
-                                       SESSION);
-                       if (form == null) {
-                               out.println("No form found. This is an Error. Fill in the form again.");
-                               return;
-                       }
-                       form.submit(out, req);
-
-                       return;
-               }
-
-               System.out.println("searching for");
-               ResultSet rs = null;
-               try {
-                       PreparedStatement ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "SELECT id, verified FROM users WHERE email=? AND dob=? AND deleted=0");
-                       ps.setString(1, req.getParameter("email"));
-                       String day = req.getParameter("year") + "-"
-                                       + req.getParameter("month") + "-" + req.getParameter("day");
-                       ps.setString(2, day);
-                       rs = ps.executeQuery();
-                       int id = 0;
-                       if (rs.next()) {
-                               id = rs.getInt(1);
-                               int verified = rs.getInt(2);
-                               if (rs.next()) {
-                                       out.println("Error, ambigous user. Please contact support@cacert.org.");
-                               } else {
-                                       if (verified == 0) {
-                                               out.println(translate(req,
-                                                               "User is not yet verified. Please try again in 24 hours!"));
-                                       }
-                                       resp.sendRedirect(PATH + "/" + id);
-                               }
-                       } else {
-                               out.print("<div class='formError'>");
-
-                               out.println(translate(
-                                               req,
-                                               "I'm sorry, there was no email and date of birth matching"
-                                                               + " what you entered in the system. Please double check"
-                                                               + " your information."));
-                               out.print("</div>");
-                       }
-
-                       rs.close();
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               } finally {
-                       try {
-                               if (rs != null) {
-                                       rs.close();
-                               }
-                       } catch (SQLException e) {
-                               e.printStackTrace();
-                       }
-               }
-       }
+
+    public static final String PATH = "/wot/assure";
+
+    DateSelector ds = new DateSelector("day", "month", "year");
+
+    Template t;
+
+    public AssurePage() {
+        super("Assure someone");
+        t = new Template(AssuranceForm.class.getResource("AssureeSearch.templ"));
+
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+
+        PrintWriter out = resp.getWriter();
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        vars.put("DoB", ds);
+        t.output(out, getLanguage(req), vars);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.canAssure();
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PrintWriter out = resp.getWriter();
+        if (req.getParameter("search") == null) {
+            AssuranceForm form = Form.getForm(req, AssuranceForm.class);
+            if (form.submit(out, req)) {
+                out.println(translate(req, "Assurance complete."));
+            } else {
+                try {
+                    Notary.checkAssuranceIsPossible(LoginPage.getUser(req), form.getAssuree());
+                    form.output(out, getLanguage(req), new HashMap<String, Object>());
+                } catch (GigiApiException e) {
+                    e.format(out, Page.getLanguage(req));
+                }
+            }
+
+            return;
+        }
+
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `users`.`id`, `verified` FROM `users` INNER JOIN `certOwners` ON `certOwners`.`id`=`users`.`id` WHERE `email`=? AND `dob`=? AND `deleted` IS NULL")) {
+            ps.setString(1, req.getParameter("email"));
+            Calendar c = Calendar.getInstance();
+            c.set(Integer.parseInt(req.getParameter("year")), Integer.parseInt(req.getParameter("month")) - 1, Integer.parseInt(req.getParameter("day")));
+            ps.setDate(2, new Date(c.getTimeInMillis()));
+            GigiResultSet rs = ps.executeQuery();
+            int id = 0;
+            if (rs.next()) {
+                id = rs.getInt(1);
+                boolean verified = rs.getBoolean(2);
+                if (rs.next()) {
+                    out.println("Error, ambigous user. Please contact support@cacert.org.");
+                } else {
+                    if ( !verified) {
+                        out.println(translate(req, "User is not yet verified. Please try again in 24 hours!"));
+                    } else if (getUser(req).getId() == id) {
+
+                    } else {
+                        User assuree = User.getById(id);
+                        User myself = LoginPage.getUser(req);
+                        try {
+                            Notary.checkAssuranceIsPossible(myself, assuree);
+                            new AssuranceForm(req, assuree).output(out, getLanguage(req), new HashMap<String, Object>());
+                        } catch (GigiApiException e) {
+                            e.format(out, Page.getLanguage(req));
+                        }
+                    }
+                }
+            } else {
+                out.print("<div class='formError'>");
+
+                out.println(translate(req, "I'm sorry, there was no email and date of birth matching" + " what you entered in the system. Please double check" + " your information."));
+                out.print("</div>");
+            }
+
+        }
+    }
 }
index cd1cb28bc0c9e2cac08f8cab2d5574b9ffb9cf9a..f36f87e7a6cbd80f42f75338268f694061187922 100644 (file)
@@ -1,19 +1,23 @@
 <form method="POST">
-<table class="wrapper" width="300">
+<table class="table">
+  <thead>
   <tr>
-    <td colspan="2" class="title"><?=_Assure Someone?></td>
+    <th colspan="2" class="title"><?=_Assure Someone?></th>
   </tr>
+  </thead>
+  <tbody>
   <tr>
-    <td class="DataTD" width="125"><?=_Email?>: </td>
-    <td class="DataTD" width="125"><input type="text" name="email"></td>
+    <td><?=_Email?>: </td>
+    <td><input class="form-control" type="text" name="email"></td>
   </tr>
   <tr>
-    <td class="DataTD" width="125"><?=_Date of Birth?><br>
-           (<?=_dd/mm/yyyy?>)</td>
-    <td class="DataTD" width="125"><?=$DoB?></td>
+    <td><?=_Date of Birth?><br>
+           (<?=_yyyy-mm-dd?>)</td>
+    <td><?=$DoB?></td>
   </tr>
   <tr>
-    <td class="DataTD" colspan="2"><input type="submit" name="process" value="<?=_Next?>"></td>
+    <td colspan="2"><input type="submit" name="search" value="<?=_Next?>"></td>
   </tr>
+  </tbody>
 </table>
 </form>
diff --git a/src/org/cacert/gigi/pages/wot/MyListingForm.java b/src/org/cacert/gigi/pages/wot/MyListingForm.java
new file mode 100644 (file)
index 0000000..97218a5
--- /dev/null
@@ -0,0 +1,57 @@
+package org.cacert.gigi.pages.wot;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+
+public class MyListingForm extends Form {
+
+    private static Template template;
+
+    static {
+        template = new Template(MyListingForm.class.getResource("MyListingForm.templ"));
+    }
+
+    private User target;
+
+    public MyListingForm(HttpServletRequest hsr, User target) {
+        super(hsr);
+        this.target = target;
+    }
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) {
+        if (req.getParameter("listme") != null && req.getParameter("contactinfo") != null) {
+            boolean on = !req.getParameter("listme").equals("0");
+            target.setDirectoryListing(on);
+            if (on) {
+                target.setContactInformation(req.getParameter("contactinfo"));
+            } else {
+                target.setContactInformation("");
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+        if (target.wantsDirectoryListing()) {
+            vars.put("selected", "selected");
+            vars.put("notSelected", "");
+            vars.put("activeInfo", target.getContactInformation());
+        } else {
+            vars.put("selected", "");
+            vars.put("notSelected", "selected");
+            vars.put("activeInfo", target.getContactInformation());
+        }
+        template.output(out, l, vars);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/wot/MyListingForm.templ b/src/org/cacert/gigi/pages/wot/MyListingForm.templ
new file mode 100644 (file)
index 0000000..49902af
--- /dev/null
@@ -0,0 +1,21 @@
+<table class="table">
+  <tr>
+    <th colspan="2"><?=_My Listing?></td>
+  </tr>
+  <tr>
+    <td><?=_Directory Listing?>:</td>
+    <td>
+       <select name="listme">
+               <option value="0" <?=$notSelected?>><?=_I don't want to be listed?></option>
+               <option value="1" <?=$selected?>><?=_I want to be listed?></option>
+       </select>
+    </td>
+  </tr>
+  <tr>
+    <td><?=_Contact information?>:</td>
+    <td><textarea class="form-control" name="contactinfo" cols="40" rows="5" wrap="virtual"><?=$activeInfo?></textarea></td>
+  </tr>
+  <tr>
+    <td colspan="2"><input type="submit" name="processContact" value="<?=_Update?>"></td>
+  </tr>
+</table>
\ No newline at end of file
diff --git a/src/org/cacert/gigi/pages/wot/MyListingPage.java b/src/org/cacert/gigi/pages/wot/MyListingPage.java
new file mode 100644 (file)
index 0000000..0b64b2e
--- /dev/null
@@ -0,0 +1,41 @@
+package org.cacert.gigi.pages.wot;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class MyListingPage extends Page {
+
+    public static final String PATH = "/wot/listing";
+
+    public MyListingPage() {
+        super("My Listing");
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (Form.getForm(req, MyListingForm.class).submit(resp.getWriter(), req)) {
+            resp.sendRedirect(PATH);
+            return;
+        }
+        super.doPost(req, resp);
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        new MyListingForm(req, getUser(req)).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.getTarget() instanceof User;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/wot/MyPoints.java b/src/org/cacert/gigi/pages/wot/MyPoints.java
new file mode 100644 (file)
index 0000000..69f5d7e
--- /dev/null
@@ -0,0 +1,46 @@
+package org.cacert.gigi.pages.wot;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.AssurancesDisplay;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class MyPoints extends Page {
+
+    public static final String PATH = "/wot/mypoints";
+
+    private AssurancesDisplay myDisplay = new AssurancesDisplay("asArr", false);
+
+    private AssurancesDisplay toOtherDisplay = new AssurancesDisplay("otherAsArr", true);
+
+    public MyPoints() {
+        super("My Points");
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        HashMap<String, Object> vars = new HashMap<String, Object>();
+        vars.put("pointlist", myDisplay);
+        vars.put("madelist", toOtherDisplay);
+        User user = getUser(req);
+        vars.put("asArr", user.getReceivedAssurances());
+        vars.put("otherAsArr", user.getMadeAssurances());
+        vars.put("assP", user.getAssurancePoints());
+        if (user.canAssure()) {
+            vars.put("expP", user.getExperiencePoints());
+            vars.put("maxP", user.getMaxAssurePoints());
+        }
+        getDefaultTemplate().output(resp.getWriter(), getLanguage(req), vars);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.getTarget() instanceof User;
+    }
+}
diff --git a/src/org/cacert/gigi/pages/wot/MyPoints.templ b/src/org/cacert/gigi/pages/wot/MyPoints.templ
new file mode 100644 (file)
index 0000000..0129f69
--- /dev/null
@@ -0,0 +1,10 @@
+<?=$pointlist?>
+<h2><?=_Assurance Points You Issued?></h2>
+<?=$madelist?>
+
+<?=_Assurance points?>: <?=$assP?><br/>
+<? if($expP) { ?>
+<?=_Experience points?>: <?=$expP?><br/>
+<?=_Max points to issue?>: <?=$maxP?><br/>
+
+<? } ?>
diff --git a/src/org/cacert/gigi/pages/wot/RequestTTPForm.java b/src/org/cacert/gigi/pages/wot/RequestTTPForm.java
new file mode 100644 (file)
index 0000000..8504186
--- /dev/null
@@ -0,0 +1,64 @@
+package org.cacert.gigi.pages.wot;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.OutputableArrayIterable;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.LoginPage;
+
+public class RequestTTPForm extends Form {
+
+    public static final Group TTP_APPLICANT = Group.getByString("ttp-applicant");
+
+    private static final Template t = new Template(RequestTTPForm.class.getResource("RequestTTPForm.templ"));
+
+    private User u;
+
+    public RequestTTPForm(HttpServletRequest hsr) {
+        super(hsr);
+        u = LoginPage.getUser(hsr);
+    }
+
+    private final String[] COUNTRIES = new String[] {
+            "Australia", "Puerto Rico", "USA"
+    };
+
+    @Override
+    public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+        String country = req.getParameter("country");
+        if (country != null) {
+            int cid = Integer.parseInt(country);
+            if (cid < 0 || cid >= COUNTRIES.length) {
+                throw new GigiApiException("Invalid country id");
+            }
+            country = COUNTRIES[cid];
+        }
+        // TODO use country?
+
+        User uReq = LoginPage.getUser(req);
+
+        if ( !u.equals(uReq)) {
+            return false;
+        }
+
+        u.grantGroup(u, TTP_APPLICANT);
+
+        return false;
+    }
+
+    @Override
+    protected void outputContent(PrintWriter out, Language l, Map<String, Object> map) {
+        map.put("countries", new OutputableArrayIterable(COUNTRIES, "country"));
+
+        t.output(out, l, map);
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/wot/RequestTTPForm.templ b/src/org/cacert/gigi/pages/wot/RequestTTPForm.templ
new file mode 100644 (file)
index 0000000..01cedb8
--- /dev/null
@@ -0,0 +1,19 @@
+<table class="table">
+       <tr>
+               <td class="DataTD"><?=_Country where you want to visit the TTP?></td>
+               <td class="DataTD"><select size="1" name="country">
+                       <? foreach($countries) {?>
+                               <option value="<?=$i?>"><?=$country?></option>
+                       <? } ?>
+               </select></td>
+       </tr>
+<!--   <tr>
+               <td class="DataTD"><?=_I want to take part in the TTP Topup programme?></td>
+               <td class="DataTD"><input type="checkbox" name="ttptopup" value="1"></td>
+       </tr>-->
+       <tr>
+               <td colspan="2" >
+                       <input type="submit" name="ttp" value="<?=_I need a TTP assurance?>">
+               </td>
+       </tr>
+</table>
diff --git a/src/org/cacert/gigi/pages/wot/RequestTTPPage.java b/src/org/cacert/gigi/pages/wot/RequestTTPPage.java
new file mode 100644 (file)
index 0000000..c97c98b
--- /dev/null
@@ -0,0 +1,67 @@
+package org.cacert.gigi.pages.wot;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Assurance;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class RequestTTPPage extends Page {
+
+    public static final String PATH = "/wot/ttp";
+
+    public RequestTTPPage() {
+        super("Request TTP");
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            Form.getForm(req, RequestTTPForm.class).submit(resp.getWriter(), req);
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        User u = LoginPage.getUser(req);
+        HashMap<String, Object> map = new HashMap<String, Object>();
+        if (u.isInGroup(RequestTTPForm.TTP_APPLICANT)) {
+            map.put("inProgress", true);
+        } else {
+            if (u.getAssurancePoints() < 100) {
+                int ttpCount = 0;
+                for (Assurance a : u.getReceivedAssurances()) {
+                    if (a.getMethod().equals(Assurance.AssuranceType.TTP_ASSISTED.getDescription())) {
+                        ttpCount++;
+                    }
+                }
+                if (ttpCount < 2) {
+                    map.put("ttp", true);
+                    map.put("form", new RequestTTPForm(req));
+                } else {
+                    map.put("nothing", true);
+                }
+            } else {
+                map.put("nothing", true);
+            }
+        }
+        map.put("form", new RequestTTPForm(req));
+        getDefaultTemplate().output(resp.getWriter(), getLanguage(req), map);
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return ac != null && ac.getTarget() instanceof User;
+    }
+
+}
diff --git a/src/org/cacert/gigi/pages/wot/RequestTTPPage.templ b/src/org/cacert/gigi/pages/wot/RequestTTPPage.templ
new file mode 100644 (file)
index 0000000..212c4f1
--- /dev/null
@@ -0,0 +1,35 @@
+<h3><?=_Trusted Third Parties?></h3>
+
+<p><?=_The Trusted Third Party (TTP) programme is intended to be used in areas without many SomeCA Assurers.?></p>
+
+<p><?=_A Trusted Third Party (TTP) is simply someone in your country that is responsible for witnessing signatures and ID documents. This role is covered by many different titles such as public notary, justice of the peace and so on.?></p>
+
+<p><?=_With the TTP programme you can potentially gain assurance up to a maximum of 100 assurance points.?></p>
+
+<p><?=_Currently SomeCA has only developed the TTP programme to the level that you can gain 70 assurance points by TTP assurances.?></p>
+
+<p><?=_We are working to develop a process that will fill the gap of the missing 30 assurance points to allow you to get the maximum 100 assurance points.?> </p>
+
+<p><?=_In the meanwhile you would need to close this gap with face to face assurances with SomeCA Assurers. Think not only travelling to populated countries, but also remember that assurers may occasionally visit your country or area.?></p>
+
+<p><?=_If you are interested in the TTP programme, read the pages !'<a href="//wiki.cacert.org/TTP/TTPuser">https://wiki.cacert.org/TTP/TTPuser</a>' for the basic way how the TTP programme works for you, and !'<a href="//wiki.cacert.org/TTP/TTPAL">https://wiki.cacert.org/TTP/TTPAL</a>' whether the TTP programme affects the country where you are located.?> </p>
+
+<? if($ttp) { ?>
+<p><?=_If you want to ask for TTP assurances fill out the missing data and send the request to support@cacert.org to start the process. SomeCA will then inform you about the next steps.?></p>
+<?=$form?>
+<? } ?>
+
+<? if($topup) { ?>
+<p><?=_As you have already got 2 TTP assurances you can only take part in the TTP TOPUP programme. If you want to ask for the TTP TOPUP programme use the submit button to send the request to support@cacert.org to start the process. SomeCA will then inform you about the next steps.?></p>
+<form method="post" action="ttp">
+       <input type="submit" name="ttptopup" value="<?=_I need a TTP TOPUP?>">
+</form>
+<p><?=_We are working to develop the TTP TOPUP process to be able to fill the gap of the missing 30 assurance points to 100 assurance points. Meanwhile you have to close this gap with face to face assurances from SomeCA Assurers. Think not only travelling to populated countries, but as well to assurers visiting your country or area.?></p>  
+<? } ?>
+
+<? if($nothing) { ?>
+<p><?=_You reached the maximum points that can be granted by the TTP programme and therefore you cannot take part in the TTP programme any more.?></p>
+<? } ?>
+<? if($inProgress) { ?>
+<p><?=_Your request for a TTP assurance is in progress. Please be patient.?></p>
+<? } ?>
diff --git a/src/org/cacert/gigi/pages/wot/Rules.templ b/src/org/cacert/gigi/pages/wot/Rules.templ
new file mode 100644 (file)
index 0000000..43a87a4
--- /dev/null
@@ -0,0 +1,30 @@
+<h3><?=_CAcert Web of Trust Rules?></h3>
+
+<p><?=_It is essential that SomeCA Assurers understand and follow the rules below to ensure that applicants for assurance are suitably identified, which, in turn, maintains trust in the system.?></p>
+
+<p><?=_Contact?><br>
+<br>
+* <?=_You must meet the applicant in person;?><br>
+* <?=_You must sight at least one form of government issued photo identification.  It's preferable if 2 forms of Government issued photo ID are presented, as less points may be issued if there is any doubt on the person by the person issuing points;?><br>
+* <?=_Complete the assurance form if the applicant has not already done so.  Ensure that all information matches.?><br>
+</p>
+
+<p><?=_Processing?><br>
+<?=_After the meeting, visit the SomeCA Web site's make an Assurance page and:?><br>
+<br>
+* <?=_Enter the applicant's email address;?><br>
+* <?=_Compare the online information to the information recorded on the paper form;?><br>
+* <?=_If, and only if, the two match completely - you may award trust points up to the maximum points you are able to allocate;?><br>
+</p>
+
+<p><?=_Privacy?><br>
+<?=_It is imperative that you maintain the confidentiality and privacy of the applicant, and never disclose the information obtained without the applicant's consent.?></p>
+
+<p><?=_Fees?><br>
+<?=_You may charge a fee for your expenses if the applicant has been advised of the amount prior to the meeting.?></p>
+
+<p><?=_Liability?><br>
+<?=_A SomeCA Assurer who knowingly, or reasonably ought to have known, assures an applicant contrary to this policy may be held liable.?></p>
+
+<p><?=_Assurance Points?><br>
+<?=_CAcert may, from time to time, alter the amount of Assurance Points that a class of assurer may assign as is necessary to effect a policy or rule change.  We may also alter the amount of Assurance Points available to an individual, or new class of assurer, should another policy of SomeCA require this.?></p>
diff --git a/src/org/cacert/gigi/ping/DNSPinger.java b/src/org/cacert/gigi/ping/DNSPinger.java
new file mode 100644 (file)
index 0000000..eb6327b
--- /dev/null
@@ -0,0 +1,58 @@
+package org.cacert.gigi.ping;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.naming.NamingException;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.util.DNSUtil;
+
+public class DNSPinger extends DomainPinger {
+
+    @Override
+    public void ping(Domain domain, String expToken, CertificateOwner u, int confId) {
+        String[] tokenParts = expToken.split(":", 2);
+        List<String> nameservers;
+        try {
+            nameservers = Arrays.asList(DNSUtil.getNSNames(domain.getSuffix()));
+        } catch (NamingException e) {
+            enterPingResult(confId, "error", "No authorative nameserver found.", null);
+            return;
+        }
+        StringBuffer result = new StringBuffer();
+        result.append("failed: ");
+        boolean failed = nameservers.isEmpty();
+        nameservers:
+        for (String NS : nameservers) {
+            boolean found = false;
+            try {
+                for (String token : DNSUtil.getTXTEntries(tokenParts[0] + "._cacert._auth." + domain.getSuffix(), NS)) {
+                    if (token.isEmpty()) {
+                        continue;
+                    }
+                    found = true;
+                    if (token.equals(tokenParts[1])) {
+                        continue nameservers;
+                    }
+                }
+            } catch (NamingException e) {
+                found = false;
+            }
+            result.append(NS);
+            if (found) {
+                result.append(" DIFFER;");
+            } else {
+                result.append(" EMPTY;");
+            }
+            failed = true;
+
+        }
+        if ( !failed) {
+            enterPingResult(confId, PING_SUCCEDED, "", null);
+        } else {
+            enterPingResult(confId, "error", result.toString(), null);
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/ping/DomainPinger.java b/src/org/cacert/gigi/ping/DomainPinger.java
new file mode 100644 (file)
index 0000000..9423349
--- /dev/null
@@ -0,0 +1,36 @@
+package org.cacert.gigi.ping;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+
+public abstract class DomainPinger {
+
+    public static final String PING_STILL_PENDING = null;
+
+    public static final String PING_SUCCEDED = "";
+
+    public abstract void ping(Domain domain, String configuration, CertificateOwner target, int confId);
+
+    protected static void enterPingResult(int configId, String state, String result, String token) {
+        try (GigiPreparedStatement enterPingResult = new GigiPreparedStatement("INSERT INTO `domainPinglog` SET `configId`=?, `state`=?::`pingState`, `result`=?, `challenge`=?")) {
+            enterPingResult.setInt(1, configId);
+            enterPingResult.setString(2, DomainPinger.PING_STILL_PENDING == state ? "open" : DomainPinger.PING_SUCCEDED.equals(state) ? "success" : "failed");
+            enterPingResult.setString(3, result);
+            enterPingResult.setString(4, token);
+            enterPingResult.execute();
+        }
+
+    }
+
+    protected static void updatePingResult(int configId, String state, String result, String token) {
+        try (GigiPreparedStatement updatePingResult = new GigiPreparedStatement("UPDATE `domainPinglog` SET `state`=?::`pingState`, `result`=? WHERE `configId`=? AND `challenge`=?")) {
+            updatePingResult.setString(1, DomainPinger.PING_STILL_PENDING == state ? "open" : DomainPinger.PING_SUCCEDED.equals(state) ? "success" : "failed");
+            updatePingResult.setString(2, result);
+            updatePingResult.setInt(3, configId);
+            updatePingResult.setString(4, token);
+            updatePingResult.execute();
+        }
+
+    }
+}
diff --git a/src/org/cacert/gigi/ping/EmailPinger.java b/src/org/cacert/gigi/ping/EmailPinger.java
new file mode 100644 (file)
index 0000000..dfb2b74
--- /dev/null
@@ -0,0 +1,33 @@
+package org.cacert.gigi.ping;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.email.MailProbe;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.util.RandomToken;
+
+public class EmailPinger extends DomainPinger {
+
+    @Override
+    public void ping(Domain domain, String configuration, CertificateOwner u, int confId) {
+        String mail = configuration + "@" + domain.getSuffix();
+        String token = RandomToken.generateToken(16);
+        try {
+            enterPingResult(confId, PING_STILL_PENDING, "", token);
+            Locale l = Locale.ENGLISH;
+            if (u instanceof User) {
+                l = ((User) u).getPreferredLocale();
+                // TODO what to do with orgs?
+            }
+            MailProbe.sendMailProbe(Language.getInstance(l), "domain", domain.getId(), token, mail);
+        } catch (IOException e) {
+            e.printStackTrace();
+            updatePingResult(confId, "error", "Mail connection interrupted", token);
+        }
+    }
+
+}
diff --git a/src/org/cacert/gigi/ping/HTTPFetch.java b/src/org/cacert/gigi/ping/HTTPFetch.java
new file mode 100644 (file)
index 0000000..a575584
--- /dev/null
@@ -0,0 +1,42 @@
+package org.cacert.gigi.ping;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+
+public class HTTPFetch extends DomainPinger {
+
+    @Override
+    public void ping(Domain domain, String expToken, CertificateOwner user, int confId) {
+        try {
+            String[] tokenParts = expToken.split(":", 2);
+            URL u = new URL("http://" + domain.getSuffix() + "/cacert-" + tokenParts[0] + ".txt");
+            HttpURLConnection huc = (HttpURLConnection) u.openConnection();
+            if (huc.getResponseCode() != 200) {
+                enterPingResult(confId, "error", "Invaild status code " + huc.getResponseCode() + ".", null);
+                return;
+            }
+            BufferedReader br = new BufferedReader(new InputStreamReader(huc.getInputStream(), "UTF-8"));
+            String line = br.readLine();
+            if (line == null) {
+                enterPingResult(confId, "error", "Empty document.", null);
+                return;
+            }
+            if (line.trim().equals(tokenParts[1])) {
+                enterPingResult(confId, PING_SUCCEDED, "", null);
+                return;
+            }
+            enterPingResult(confId, "error", "Challange tokens differed.", null);
+            return;
+        } catch (IOException e) {
+            e.printStackTrace();
+            enterPingResult(confId, "error", "Exception: connection closed.", null);
+            return;
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/ping/PingerDaemon.java b/src/org/cacert/gigi/ping/PingerDaemon.java
new file mode 100644 (file)
index 0000000..397ad58
--- /dev/null
@@ -0,0 +1,110 @@
+package org.cacert.gigi.ping;
+
+import java.security.KeyStore;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.database.DatabaseConnection.Link;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.DomainPingConfiguration;
+import org.cacert.gigi.dbObjects.DomainPingType;
+import org.cacert.gigi.util.RandomToken;
+
+public class PingerDaemon extends Thread {
+
+    HashMap<DomainPingType, DomainPinger> pingers = new HashMap<>();
+
+    private GigiPreparedStatement searchNeededPings;
+
+    private KeyStore truststore;
+
+    private Queue<DomainPingConfiguration> toExecute = new LinkedList<>();
+
+    public PingerDaemon(KeyStore truststore) {
+        this.truststore = truststore;
+    }
+
+    @Override
+    public void run() {
+        try (Link l = DatabaseConnection.newLink(false)) {
+            runWithConnection();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void runWithConnection() {
+        searchNeededPings = new GigiPreparedStatement("SELECT `pingconfig`.`id` FROM `pingconfig` LEFT JOIN `domainPinglog` ON `domainPinglog`.`configId` = `pingconfig`.`id` INNER JOIN `domains` ON `domains`.`id` = `pingconfig`.`domainid` WHERE ( `domainPinglog`.`configId` IS NULL OR `domainPinglog`.`when` < CURRENT_TIMESTAMP - interval '6 mons') AND `domains`.`deleted` IS NULL AND `pingconfig`.`deleted` IS NULL GROUP BY `pingconfig`.`id`");
+        pingers.put(DomainPingType.EMAIL, new EmailPinger());
+        pingers.put(DomainPingType.SSL, new SSLPinger(truststore));
+        pingers.put(DomainPingType.HTTP, new HTTPFetch());
+        pingers.put(DomainPingType.DNS, new DNSPinger());
+
+        while (true) {
+            try {
+                boolean worked = false;
+                synchronized (this) {
+                    DomainPingConfiguration conf;
+                    while ((conf = toExecute.peek()) != null) {
+                        worked = true;
+                        handle(conf);
+                        toExecute.remove();
+                    }
+                    notifyAll();
+                }
+
+                GigiResultSet rs = searchNeededPings.executeQuery();
+                while (rs.next()) {
+                    worked = true;
+                    handle(DomainPingConfiguration.getById(rs.getInt("id")));
+                }
+                try {
+                    if ( !worked) {
+                        Thread.sleep(5000);
+                    }
+                } catch (InterruptedException e) {
+                }
+            } catch (Throwable t) {
+                t.printStackTrace();
+            }
+        }
+    }
+
+    private void handle(DomainPingConfiguration conf) {
+        DomainPingType type = conf.getType();
+        String config = conf.getInfo();
+        DomainPinger dp = pingers.get(type);
+        if (dp != null) {
+            if (dp instanceof EmailPinger) {
+                String token = null;
+                token = RandomToken.generateToken(16);
+                config = config + ":" + token;
+            }
+            Domain target = conf.getTarget();
+            System.err.println("Executing " + dp + " on " + target + " (" + System.currentTimeMillis() + ")");
+            try {
+                dp.ping(target, config, target.getOwner(), conf.getId());
+            } catch (Throwable t) {
+                t.printStackTrace();
+                DomainPinger.enterPingResult(conf.getId(), "error", "exception", null);
+            }
+            System.err.println("done (" + System.currentTimeMillis() + ")");
+        }
+    }
+
+    public synchronized void queue(DomainPingConfiguration toReping) {
+        interrupt();
+        toExecute.add(toReping);
+        while (toExecute.size() > 0) {
+            try {
+                wait();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/ping/SSLPinger.java b/src/org/cacert/gigi/ping/SSLPinger.java
new file mode 100644 (file)
index 0000000..7161292
--- /dev/null
@@ -0,0 +1,288 @@
+package org.cacert.gigi.ping;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+import javax.security.cert.CertificateException;
+import javax.security.cert.X509Certificate;
+
+import org.cacert.gigi.dbObjects.CACertificate;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Domain;
+
+import sun.security.x509.AVA;
+import sun.security.x509.X500Name;
+
+public class SSLPinger extends DomainPinger {
+
+    public static final String[] TYPES = new String[] {
+            "xmpp", "server-xmpp", "smtp", "imap"
+    };
+
+    private KeyStore truststore;
+
+    public SSLPinger(KeyStore truststore) {
+        this.truststore = truststore;
+    }
+
+    @Override
+    public void ping(Domain domain, String configuration, CertificateOwner u, int confId) {
+        try (SocketChannel sch = SocketChannel.open()) {
+            sch.socket().setSoTimeout(5000);
+            String[] parts = configuration.split(":", 4);
+            sch.socket().connect(new InetSocketAddress(domain.getSuffix(), Integer.parseInt(parts[2])), 5000);
+            if (parts.length == 4) {
+                switch (parts[3]) {
+                case "xmpp":
+                    startXMPP(sch, false, domain.getSuffix());
+                    break;
+                case "server-xmpp":
+                    startXMPP(sch, true, domain.getSuffix());
+                    break;
+                case "smtp":
+                    startSMTP(sch);
+                    break;
+                case "imap":
+                    startIMAP(sch);
+                    break;
+
+                }
+            }
+            String key = parts[0];
+            String value = parts[1];
+            String res = test(sch, domain.getSuffix(), u, value);
+            enterPingResult(confId, res, res, null);
+            return;
+        } catch (IOException e) {
+            enterPingResult(confId, "error", "connection Failed", null);
+            return;
+        }
+
+    }
+
+    private void startIMAP(SocketChannel sch) throws IOException {
+        Socket s = sch.socket();
+        InputStream is = s.getInputStream();
+        OutputStream os = s.getOutputStream();
+        scanFor(is, "\n");
+        os.write("ENABLE STARTTLS\r\n".getBytes("UTF-8"));
+        os.flush();
+        scanFor(is, "\n");
+    }
+
+    private void startXMPP(SocketChannel sch, boolean server, String domain) throws IOException {
+        Socket s = sch.socket();
+        InputStream is = s.getInputStream();
+        OutputStream os = s.getOutputStream();
+        os.write(("<stream:stream to=\"" + domain + "\" xmlns=\"jabber:" + (server ? "server" : "client") + "\"" + " xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">").getBytes("UTF-8"));
+        os.flush();
+        os.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>".getBytes("UTF-8"));
+        os.flush();
+        scanFor(is, "<proceed");
+        scanFor(is, ">");
+
+    }
+
+    private void scanFor(InputStream is, String scanFor) throws IOException {
+        int pos = 0;
+        while (pos < scanFor.length()) {
+            if (is.read() == scanFor.charAt(pos)) {
+                pos++;
+            } else {
+                pos = 0;
+            }
+        }
+    }
+
+    private void startSMTP(SocketChannel sch) throws IOException {
+        Socket s = sch.socket();
+        InputStream is = s.getInputStream();
+        readSMTP(is);
+        s.getOutputStream().write("EHLO ssl.pinger\r\n".getBytes("UTF-8"));
+        s.getOutputStream().flush();
+        readSMTP(is);
+        s.getOutputStream().write("HELP\r\n".getBytes("UTF-8"));
+        s.getOutputStream().flush();
+        readSMTP(is);
+        s.getOutputStream().write("STARTTLS\r\n".getBytes("UTF-8"));
+        s.getOutputStream().flush();
+        readSMTP(is);
+    }
+
+    private void readSMTP(InputStream is) throws IOException {
+        int counter = 0;
+        boolean finish = true;
+        while (true) {
+            char c = (char) is.read();
+            if (counter == 3) {
+                if (c == ' ') {
+                    finish = true;
+                } else if (c == '-') {
+                    finish = false;
+                } else {
+                    throw new Error("Invalid smtp: " + c);
+                }
+            }
+            if (c == '\n') {
+                if (finish) {
+                    return;
+                }
+                counter = 0;
+            } else {
+                counter++;
+            }
+        }
+    }
+
+    private String test(SocketChannel sch, String domain, CertificateOwner subject, String tok) {
+        System.out.println("SSL- connecting");
+
+        try {
+            sch.socket().setSoTimeout(5000);
+            SSLContext sc = SSLContext.getInstance("SSL");
+            try {
+                TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
+                tmf.init(truststore);
+                sc.init(null, new TrustManager[] {
+                        new X509TrustManager() {
+
+                            @Override
+                            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+                                return null;
+                            }
+
+                            @Override
+                            public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws java.security.cert.CertificateException {
+                                java.security.cert.X509Certificate c = chain[0];
+                                if ( !c.getExtendedKeyUsage().contains("1.3.6.1.5.5.7.3.1")) {
+                                    throw new java.security.cert.CertificateException("Illegal EKU");
+                                }
+                            }
+
+                            @Override
+                            public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws java.security.cert.CertificateException {}
+                        }
+                }, new SecureRandom());
+            } catch (KeyManagementException e) {
+                e.printStackTrace();
+            } catch (KeyStoreException e) {
+                e.printStackTrace();
+            }
+            SSLEngine se = sc.createSSLEngine();
+            ByteBuffer enc_in = ByteBuffer.allocate(se.getSession().getPacketBufferSize());
+            ByteBuffer enc_out = ByteBuffer.allocate(se.getSession().getPacketBufferSize());
+            ByteBuffer dec_in = ByteBuffer.allocate(se.getSession().getApplicationBufferSize());
+            ByteBuffer dec_out = ByteBuffer.allocate(se.getSession().getApplicationBufferSize());
+            se.setUseClientMode(true);
+            SSLParameters sp = se.getSSLParameters();
+            sp.setServerNames(Arrays.<SNIServerName>asList(new SNIHostName(domain)));
+            se.setSSLParameters(sp);
+            se.beginHandshake();
+            enc_in.limit(0);
+            while (se.getHandshakeStatus() != HandshakeStatus.FINISHED && se.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) {
+                switch (se.getHandshakeStatus()) {
+                case NEED_WRAP:
+                    dec_out.limit(0);
+                    se.wrap(dec_out, enc_out);
+                    enc_out.flip();
+                    while (enc_out.remaining() > 0) {
+                        sch.write(enc_out);
+                    }
+                    enc_out.clear();
+                    break;
+                case NEED_UNWRAP:
+                    if (enc_in.remaining() == 0) {
+                        enc_in.clear();
+                        sch.read(enc_in);
+                        enc_in.flip();
+                    }
+                    while (se.unwrap(enc_in, dec_in).getStatus() == Status.BUFFER_UNDERFLOW) {
+                        enc_in.position(enc_in.limit());
+                        enc_in.limit(enc_in.capacity());
+                        sch.read(enc_in);
+                        enc_in.flip();
+                    }
+                    enc_in.compact();
+                    enc_in.flip();
+                    break;
+                case NEED_TASK:
+                    se.getDelegatedTask().run();
+                    break;
+                case NOT_HANDSHAKING:
+                case FINISHED:
+
+                }
+
+            }
+            System.out.println("SSL- connected");
+            X509Certificate[] peerCertificateChain = se.getSession().getPeerCertificateChain();
+            X509Certificate first = peerCertificateChain[0];
+            if (first.getIssuerDN().equals(first.getSubjectDN())) {
+                first.verify(first.getPublicKey());
+                X500Name p = (X500Name) first.getSubjectDN();
+                X500Name n = new X500Name(p.getEncoded());
+                for (AVA i : n.allAvas()) {
+                    if (i.getObjectIdentifier().equals((Object) X500Name.orgUnitName_oid)) {
+                        String toke = i.getDerValue().getAsString();
+                        if (tok.equals(toke)) {
+                            return PING_SUCCEDED;
+                        } else {
+                            return "Self-signed certificate is wrong";
+                        }
+                    }
+                }
+            }
+
+            BigInteger serial = first.getSerialNumber();
+            Certificate c = Certificate.getBySerial(serial.toString(16));
+            if (c == null) {
+                return "Certificate not found: Serial " + serial.toString(16) + " missing.";
+            }
+            CACertificate p = c.getParent();
+            if ( !first.getIssuerDN().equals(p.getCertificate().getSubjectDN())) {
+                return "Broken certificate supplied";
+            }
+            first.verify(p.getCertificate().getPublicKey());
+            if (c.getOwner().getId() != subject.getId()) {
+                return "Owner mismatch";
+            }
+            return PING_SUCCEDED;
+        } catch (GeneralSecurityException e) {
+            // e.printStackTrace();
+            return "Security failed";
+        } catch (SSLException e) {
+            // e.printStackTrace(); TODO log for user debugging?
+            return "Security failed";
+        } catch (IOException e) {
+            // e.printStackTrace(); TODO log for user debugging?
+            return "Connection closed";
+        } catch (CertificateException e) {
+            // e.printStackTrace();
+            return "Security failed";
+        }
+    }
+}
diff --git a/src/org/cacert/gigi/util/AuthorizationContext.java b/src/org/cacert/gigi/util/AuthorizationContext.java
new file mode 100644 (file)
index 0000000..dfd591e
--- /dev/null
@@ -0,0 +1,98 @@
+package org.cacert.gigi.util;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.SprintfCommand;
+
+public class AuthorizationContext implements Outputable {
+
+    CertificateOwner target;
+
+    User actor;
+
+    String supporterTicketId;
+
+    public AuthorizationContext(CertificateOwner target, User actor) {
+        this.target = target;
+        this.actor = actor;
+    }
+
+    public AuthorizationContext(User actor, String supporterTicket) throws GigiApiException {
+        this.target = actor;
+        this.actor = actor;
+        if ( !isInGroup(Group.SUPPORTER)) {
+            throw new GigiApiException("requires a supporter");
+        }
+        supporterTicketId = supporterTicket;
+    }
+
+    public CertificateOwner getTarget() {
+        return target;
+    }
+
+    public User getActor() {
+        return actor;
+    }
+
+    public boolean isInGroup(Group g) {
+        return actor.isInGroup(g);
+    }
+
+    public User getActor(AuthorizationContext ac) {
+        if (ac == null) {
+            return null;
+        }
+        return ac.getActor();
+    }
+
+    public String getSupporterTicketId() {
+        return supporterTicketId;
+    }
+
+    public boolean canSupport() {
+        return getSupporterTicketId() != null && isInGroup(Group.SUPPORTER);
+    }
+
+    private static final SprintfCommand sp = new SprintfCommand("Logged in as {0} via {1}.", Arrays.asList("${username}", "${loginMethod}"));
+
+    private static final SprintfCommand inner = new SprintfCommand("{0} (on behalf of {1})", Arrays.asList("${user}", "${target}"));
+
+    @Override
+    public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+        out.println("<div>");
+        vars.put("username", new Outputable() {
+
+            @Override
+            public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+                if (target != actor) {
+                    vars.put("user", ((Organisation) target).getName().toString());
+                    vars.put("target", actor.getName().toString());
+                    inner.output(out, l, vars);
+                } else {
+                    out.println(actor.getName().toString());
+                }
+            }
+        });
+        sp.output(out, l, vars);
+        out.println("</div>");
+        if (supporterTicketId != null) {
+            out.println("<div>");
+            out.println(l.getTranslation("SupportTicket: "));
+            out.println(HTMLEncoder.encodeHTML(supporterTicketId));
+            out.println("</div>");
+        }
+    }
+
+    public boolean canAssure() {
+        return target instanceof User && ((User) target).canAssure();
+    }
+}
diff --git a/src/org/cacert/gigi/util/CalendarUtil.java b/src/org/cacert/gigi/util/CalendarUtil.java
new file mode 100644 (file)
index 0000000..03fdf2f
--- /dev/null
@@ -0,0 +1,38 @@
+package org.cacert.gigi.util;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+public class CalendarUtil {
+
+    public static boolean isDateValid(int year, int month, int day) {
+
+        Calendar c = GregorianCalendar.getInstance();
+        c.set(year, month - 1, day);
+        return c.get(Calendar.YEAR) == year && c.get(Calendar.MONTH) == month - 1 && c.get(Calendar.DATE) == day;
+
+    }
+
+    public static boolean isOfAge(DayDate dob, int age) {
+        Calendar c = Calendar.getInstance();
+        c.setTimeInMillis(dob.getTime());
+        int year = c.get(Calendar.YEAR);
+        int month = c.get(Calendar.MONTH);
+        int day = c.get(Calendar.DAY_OF_MONTH);
+        c.set(year, month, day);
+        c.add(Calendar.YEAR, age);
+
+        return System.currentTimeMillis() >= c.getTime().getTime();
+    }
+
+    public static DayDate getDateFromComponents(int year, int month, int day) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTimeZone(TimeZone.getTimeZone("UTC"));
+        cal.setTimeInMillis(0);
+        cal.set(year, month - 1, day, 0, 0, 0);
+        Date dob = cal.getTime();
+        return new DayDate(dob.getTime());
+    }
+}
diff --git a/src/org/cacert/gigi/util/CertExporter.java b/src/org/cacert/gigi/util/CertExporter.java
new file mode 100644 (file)
index 0000000..6c18097
--- /dev/null
@@ -0,0 +1,165 @@
+package org.cacert.gigi.util;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.cert.CRLException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509CRL;
+import java.security.cert.X509Certificate;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Set;
+
+import javax.servlet.ServletOutputStream;
+
+import org.cacert.gigi.dbObjects.CACertificate;
+import org.cacert.gigi.dbObjects.Certificate;
+
+import sun.security.pkcs.ContentInfo;
+import sun.security.pkcs.PKCS7;
+import sun.security.pkcs.SignerInfo;
+import sun.security.util.DerOutputStream;
+import sun.security.util.DerValue;
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.X509CRLImpl;
+import sun.security.x509.X509CertImpl;
+
+public class CertExporter {
+
+    private CertExporter() {}
+
+    public static void writeCertCrt(Certificate c, ServletOutputStream out, boolean doChain, boolean includeAnchor) throws IOException, GeneralSecurityException {
+        X509Certificate cert = c.cert();
+        out.println(PEM.encode("CERTIFICATE", cert.getEncoded()));
+        if (doChain) {
+            CACertificate ca = c.getParent();
+            while ( !ca.isSelfsigned()) {
+                out.println(PEM.encode("CERTIFICATE", ca.getCertificate().getEncoded()));
+                ca = ca.getParent();
+            }
+            if (includeAnchor) {
+                out.println(PEM.encode("CERTIFICATE", ca.getCertificate().getEncoded()));
+            }
+        }
+    }
+
+    public static void writeCertCer(Certificate c, ServletOutputStream out, boolean doChain, boolean includeAnchor) throws IOException, GeneralSecurityException {
+        X509Certificate cert = c.cert();
+        if (doChain) {
+            PKCS7 p7 = toP7Chain(c);
+            p7.encodeSignedData(out);
+        } else {
+            out.write(cert.getEncoded());
+        }
+    }
+
+    private static PKCS7 toP7Chain(Certificate c) throws IOException, GeneralSecurityException {
+        LinkedList<X509Certificate> ll = getChain(c);
+        PKCS7 p7 = new PKCS7(new AlgorithmId[0], new ContentInfo(ContentInfo.DATA_OID, null), ll.toArray(new X509Certificate[ll.size()]), new SignerInfo[0]) {
+
+            @Override
+            public void encodeSignedData(DerOutputStream out) throws IOException {
+                DerOutputStream signedData = new DerOutputStream();
+                BigInteger version = getVersion();
+                AlgorithmId[] digestAlgorithmIds = getDigestAlgorithmIds();
+                ContentInfo contentInfo = getContentInfo();
+                X509Certificate[] certificates = getCertificates();
+                X509CRL[] crls = getCRLs();
+                SignerInfo[] signerInfos = getSignerInfos();
+
+                // version
+                signedData.putInteger(version);
+
+                // digestAlgorithmIds
+                signedData.putOrderedSetOf(DerValue.tag_Set, digestAlgorithmIds);
+
+                // contentInfo
+                contentInfo.encode(signedData);
+
+                // certificates (optional)
+                if (certificates != null && certificates.length != 0) {
+                    DerOutputStream sub = new DerOutputStream();
+                    // cast to X509CertImpl[] since X509CertImpl implements
+                    // DerEncoder
+                    X509CertImpl implCerts[] = new X509CertImpl[certificates.length];
+                    for (int i = 0; i < certificates.length; i++) {
+                        try {
+                            sub.write(certificates[i].getEncoded());
+                        } catch (CertificateEncodingException e) {
+                            sub.close();
+                            throw new IOException(e);
+                        }
+                        if (certificates[i] instanceof X509CertImpl) {
+                            implCerts[i] = (X509CertImpl) certificates[i];
+                        } else {
+                            try {
+                                byte[] encoded = certificates[i].getEncoded();
+                                implCerts[i] = new X509CertImpl(encoded);
+                            } catch (CertificateException ce) {
+                                sub.close();
+                                throw new IOException(ce);
+                            }
+                        }
+                    }
+
+                    // Add the certificate set (tagged with [0] IMPLICIT)
+                    // to the signed data
+                    signedData.write((byte) 0xA0, sub);
+                    sub.close();
+                }
+
+                // CRLs (optional)
+                if (crls != null && crls.length != 0) {
+                    // cast to X509CRLImpl[] since X509CRLImpl implements
+                    // DerEncoder
+                    Set<X509CRLImpl> implCRLs = new HashSet<X509CRLImpl>(crls.length);
+                    for (X509CRL crl : crls) {
+                        if (crl instanceof X509CRLImpl) {
+                            implCRLs.add((X509CRLImpl) crl);
+                        } else {
+                            try {
+                                byte[] encoded = crl.getEncoded();
+                                implCRLs.add(new X509CRLImpl(encoded));
+                            } catch (CRLException ce) {
+                                throw new IOException(ce);
+                            }
+                        }
+                    }
+
+                    // Add the CRL set (tagged with [1] IMPLICIT)
+                    // to the signed data
+                    signedData.putOrderedSetOf((byte) 0xA1, implCRLs.toArray(new X509CRLImpl[implCRLs.size()]));
+                }
+
+                // signerInfos
+                signedData.putOrderedSetOf(DerValue.tag_Set, signerInfos);
+
+                // making it a signed data block
+                DerValue signedDataSeq = new DerValue(DerValue.tag_Sequence, signedData.toByteArray());
+
+                // making it a content info sequence
+                ContentInfo block = new ContentInfo(ContentInfo.SIGNED_DATA_OID, signedDataSeq);
+
+                // writing out the contentInfo sequence
+                block.encode(out);
+            }
+
+        };
+        return p7;
+    }
+
+    private static LinkedList<X509Certificate> getChain(Certificate c) throws IOException, GeneralSecurityException {
+        LinkedList<X509Certificate> ll = new LinkedList<>();
+        ll.add(c.cert());
+        CACertificate ca = c.getParent();
+        while ( !ca.isSelfsigned()) {
+            ll.add(ca.getCertificate());
+            ca = ca.getParent();
+        }
+        ll.add(ca.getCertificate());
+        return ll;
+    }
+
+}
index 5860c119903a17407c7dea83010af02954c3d909..a1aaecdfb25ff6e27ad0cac402046582757df231 100644 (file)
@@ -10,279 +10,322 @@ import java.util.TreeSet;
 import sun.security.ssl.SSLContextImpl;
 
 public class CipherInfo implements Comparable<CipherInfo> {
-       private static class CipherInfoGenerator {
-               private Class<?> cipherSuite;
-               private Field cipherSuiteNameMap;
-               private Field exchange;
-               private Field cipher;
-               private Field keySize;
-               private Field algortihm;
-               private Field transformation;
-               private HashMap<?, ?> names;
-               private Field macAlg;
-               private Field macName;
-               private Field macSize;
-
-               public CipherInfoGenerator() throws ReflectiveOperationException {
-                       SSLContextImpl sc = new SSLContextImpl.TLS12Context();
-                       Method m = SSLContextImpl.class
-                                       .getDeclaredMethod("getSupportedCipherSuiteList");
-                       m.setAccessible(true);
-                       Object o = m.invoke(sc);
-                       Class<?> cipherSuiteList = o.getClass();
-                       Method collection = cipherSuiteList.getDeclaredMethod("collection");
-                       collection.setAccessible(true);
-                       Collection<?> suites = (Collection<?>) collection.invoke(o);
-                       Object oneSuite = suites.iterator().next();
-                       cipherSuite = oneSuite.getClass();
-                       cipherSuiteNameMap = cipherSuite.getDeclaredField("nameMap");
-                       cipherSuiteNameMap.setAccessible(true);
-                       names = (HashMap<?, ?>) cipherSuiteNameMap.get(null);
-                       exchange = cipherSuite.getDeclaredField("keyExchange");
-                       exchange.setAccessible(true);
-                       cipher = cipherSuite.getDeclaredField("cipher");
-                       cipher.setAccessible(true);
-                       Class<?> bulkCipher = cipher.getType();
-                       keySize = bulkCipher.getDeclaredField("keySize");
-                       keySize.setAccessible(true);
-                       algortihm = bulkCipher.getDeclaredField("algorithm");
-                       algortihm.setAccessible(true);
-                       transformation = bulkCipher.getDeclaredField("transformation");
-                       transformation.setAccessible(true);
-
-                       macAlg = cipherSuite.getDeclaredField("macAlg");
-                       macAlg.setAccessible(true);
-                       Class<?> mac = macAlg.getType();
-                       macName = mac.getDeclaredField("name");
-                       macName.setAccessible(true);
-                       macSize = mac.getDeclaredField("size");
-                       macSize.setAccessible(true);
-               }
-               public CipherInfo generateInfo(String suiteName)
-                               throws IllegalArgumentException, IllegalAccessException {
-                       Object suite = names.get(suiteName);
-                       String keyExchange = exchange.get(suite).toString();
-                       Object bulkCipher = cipher.get(suite);
-                       Object mac = macAlg.get(suite);
-
-                       String transform = (String) transformation.get(bulkCipher);
-                       String[] transformationParts = transform.split("/");
-                       int keysize = keySize.getInt(bulkCipher);
-
-                       String macNam = (String) macName.get(mac);
-                       int macSiz = macSize.getInt(mac);
-
-                       String chaining = null;
-                       String padding = null;
-                       if (transformationParts.length > 1) {
-                               chaining = transformationParts[1];
-                               padding = transformationParts[2];
-                       }
-
-                       return new CipherInfo(suiteName, keyExchange,
-                                       transformationParts[0], keysize * 8, chaining, padding,
-                                       macNam, macSiz * 8);
-
-               }
-       }
-       String keyExchange;
-       String cipher;
-       int keySize;
-       String cipherChaining;
-       String cipherPadding;
-       String macName;
-       int macSize;
-       String suiteName;
-
-       private CipherInfo(String suiteName, String keyExchange, String cipher,
-                       int keySize, String cipherChaining, String cipherPadding,
-                       String macName, int macSize) {
-               this.suiteName = suiteName;
-               this.keyExchange = keyExchange;
-               this.cipher = cipher;
-               this.keySize = keySize;
-               this.cipherChaining = cipherChaining;
-               this.cipherPadding = cipherPadding;
-               this.macName = macName;
-               this.macSize = macSize;
-       }
-
-       static CipherInfoGenerator cig;
-       static {
-               try {
-                       cig = new CipherInfoGenerator();
-               } catch (ReflectiveOperationException e) {
-                       e.printStackTrace();
-               }
-       }
-
-       public static CipherInfo generateInfo(String name) {
-               if (cig == null) {
-                       return null;
-               }
-               try {
-                       return cig.generateInfo(name);
-               } catch (IllegalArgumentException e) {
-                       e.printStackTrace();
-               } catch (IllegalAccessException e) {
-                       e.printStackTrace();
-               }
-               return null;
-       }
-       public String getSuiteName() {
-               return suiteName;
-       }
-       /**
-        * 5: ECDHE, AES||CAMELLIA, keysize >=256 <br>
-        * 4: DHE, AES||CAMELLIA, keysize >= 256<br>
-        * 3: ECDHE|| DHE, AES||CAMELLIA<br>
-        * 2: ECDHE||DHE<br>
-        * 1: RSA||DSA <br>
-        * 0: Others
-        * 
-        * @return the strength
-        */
-       public int getStrength() {
-               if (cipher.equals("NULL") || cipher.equals("RC4")
-                               || cipher.contains("DES")) {
-                       return 0;
-               }
-               boolean ecdhe = keyExchange.startsWith("ECDHE");
-               boolean dhe = keyExchange.startsWith("DHE");
-               boolean pfs = ecdhe || dhe;
-               boolean goodCipher = cipher.equals("AES") || cipher.equals("CAMELLIA");
-               if (ecdhe && goodCipher && keySize >= 256) {
-                       return 5;
-               }
-               if (dhe && goodCipher && keySize >= 256) {
-                       return 4;
-               }
-               if (pfs && goodCipher) {
-                       return 3;
-               }
-               if (pfs) {
-                       return 2;
-               }
-               if (keyExchange.equals("RSA") || keyExchange.equals("DSA")) {
-                       return 1;
-               }
-               return 0;
-       }
-       private static final String[] CIPHER_RANKING = new String[]{"CAMELLIA",
-                       "AES", "RC4", "3DES", "DES", "DES40"};
-
-       @Override
-       public String toString() {
-               return "CipherInfo [keyExchange=" + keyExchange + ", cipher=" + cipher
-                               + ", keySize=" + keySize + ", cipherChaining=" + cipherChaining
-                               + ", cipherPadding=" + cipherPadding + ", macName=" + macName
-                               + ", macSize=" + macSize + "]";
-       }
-       /**
-        * ECDHE<br>
-        * GCM<br>
-        * Cipher {@link #CIPHER_RANKING}<br>
-        * Cipher {@link #keySize}<br>
-        * HMAC<br>
-        * HMAC size<br>
-        * 
-        * @return
-        */
-       @Override
-       public int compareTo(CipherInfo o) {
-               int myStrength = getStrength();
-               int oStrength = o.getStrength();
-               if (myStrength > oStrength) {
-                       return -1;
-               }
-               if (myStrength < oStrength) {
-                       return 1;
-               }
-               // TODO sort SSL/TLS
-               boolean myEcdhe = keyExchange.startsWith("ECDHE");
-               boolean oEcdhe = o.keyExchange.startsWith("ECDHE");
-               if (myEcdhe && !oEcdhe) {
-                       return -1;
-               }
-               if (!myEcdhe && oEcdhe) {
-                       return 1;
-               }
-               boolean myGCM = "GCM".equals(cipherChaining);
-               boolean oGCM = "GCM".equals(o.cipherChaining);
-               if (myGCM && !oGCM) {
-                       return -1;
-               }
-               if (!myGCM && oGCM) {
-                       return 1;
-               }
-               if (!cipher.equals(o.cipher)) {
-
-                       for (String testCipher : CIPHER_RANKING) {
-                               if (cipher.equals(testCipher)) {
-                                       return -1;
-                               }
-                               if (o.cipher.equals(testCipher)) {
-                                       return 1;
-                               }
-                       }
-                       if (cipher.equals("NULL")) {
-                               return 1;
-                       }
-                       if (o.cipher.equals("NULL")) {
-                               return -1;
-                       }
-               }
-               if (keySize > o.keySize) {
-                       return -1;
-               }
-               if (keySize < o.keySize) {
-                       return 1;
-               }
-               boolean mySHA = macName.startsWith("SHA");
-               boolean oSHA = o.macName.startsWith("SHA");
-               if (mySHA && !oSHA) {
-                       return -1;
-               }
-               if (mySHA && !oSHA) {
-                       return 1;
-               }
-               if (macSize > o.macSize) {
-                       return -1;
-               }
-               if (macSize < o.macSize) {
-                       return 1;
-               }
-
-               return suiteName.compareTo(o.suiteName);
-       }
-       static String[] cipherRanking = null;
-       public static String[] getCompleteRanking() {
-               if (cipherRanking == null) {
-                       String[] ciphers = filterCiphers((Iterable<String>) cig.names
-                                       .keySet());
-                       cipherRanking = ciphers;
-               }
-               return cipherRanking;
-       }
-       private static String[] filterCiphers(Iterable<String> toFilter) {
-               TreeSet<CipherInfo> chosenCiphers = new TreeSet<CipherInfo>();
-               for (String o : toFilter) {
-                       String s = o;
-                       CipherInfo info = CipherInfo.generateInfo(s);
-                       if (info != null) {
-                               if (info.getStrength() > 1) {
-                                       chosenCiphers.add(info);
-                               }
-                       }
-               }
-               String[] ciphers = new String[chosenCiphers.size()];
-               int counter = 0;
-               for (CipherInfo i : chosenCiphers) {
-                       ciphers[counter++] = i.getSuiteName();
-               }
-               return ciphers;
-       }
-       public static String[] filter(String[] supportedCipherSuites) {
-               return filterCiphers(Arrays.asList(supportedCipherSuites));
-       }
+
+    private static class CipherInfoGenerator {
+
+        private Class<?> cipherSuite;
+
+        private Field cipherSuiteNameMap;
+
+        private Field exchange;
+
+        private Field cipher;
+
+        private Field keySize;
+
+        private Field algortihm;
+
+        private Field transformation;
+
+        private HashMap<?, ?> names;
+
+        private Field macAlg;
+
+        private Field macName;
+
+        private Field macSize;
+
+        public CipherInfoGenerator() throws ReflectiveOperationException {
+            SSLContextImpl sc = new SSLContextImpl.TLS12Context();
+            Method m = SSLContextImpl.class.getDeclaredMethod("getSupportedCipherSuiteList");
+            m.setAccessible(true);
+            Object o = m.invoke(sc);
+            Class<?> cipherSuiteList = o.getClass();
+            Method collection = cipherSuiteList.getDeclaredMethod("collection");
+            collection.setAccessible(true);
+            Collection<?> suites = (Collection<?>) collection.invoke(o);
+            Object oneSuite = suites.iterator().next();
+            cipherSuite = oneSuite.getClass();
+            cipherSuiteNameMap = cipherSuite.getDeclaredField("nameMap");
+            cipherSuiteNameMap.setAccessible(true);
+            names = (HashMap<?, ?>) cipherSuiteNameMap.get(null);
+            exchange = cipherSuite.getDeclaredField("keyExchange");
+            exchange.setAccessible(true);
+            cipher = cipherSuite.getDeclaredField("cipher");
+            cipher.setAccessible(true);
+            Class<?> bulkCipher = cipher.getType();
+            keySize = bulkCipher.getDeclaredField("keySize");
+            keySize.setAccessible(true);
+            algortihm = bulkCipher.getDeclaredField("algorithm");
+            algortihm.setAccessible(true);
+            transformation = bulkCipher.getDeclaredField("transformation");
+            transformation.setAccessible(true);
+
+            macAlg = cipherSuite.getDeclaredField("macAlg");
+            macAlg.setAccessible(true);
+            Class<?> mac = macAlg.getType();
+            macName = mac.getDeclaredField("name");
+            macName.setAccessible(true);
+            macSize = mac.getDeclaredField("size");
+            macSize.setAccessible(true);
+        }
+
+        public CipherInfo generateInfo(String suiteName) throws IllegalArgumentException, IllegalAccessException {
+            Object suite = names.get(suiteName);
+            String keyExchange = exchange.get(suite).toString();
+            Object bulkCipher = cipher.get(suite);
+            Object mac = macAlg.get(suite);
+
+            String transform = (String) transformation.get(bulkCipher);
+            String[] transformationParts = transform.split("/");
+            int keysize = keySize.getInt(bulkCipher);
+
+            String macNam = (String) macName.get(mac);
+            int macSiz = macSize.getInt(mac);
+
+            String chaining = null;
+            String padding = null;
+            if (transformationParts.length > 1) {
+                chaining = transformationParts[1];
+                padding = transformationParts[2];
+            }
+
+            return new CipherInfo(suiteName, keyExchange, transformationParts[0], keysize * 8, chaining, padding, macNam, macSiz * 8);
+
+        }
+    }
+
+    String keyExchange;
+
+    String cipher;
+
+    int keySize;
+
+    String cipherChaining;
+
+    String cipherPadding;
+
+    String macName;
+
+    int macSize;
+
+    String suiteName;
+
+    private CipherInfo(String suiteName, String keyExchange, String cipher, int keySize, String cipherChaining, String cipherPadding, String macName, int macSize) {
+        this.suiteName = suiteName;
+        this.keyExchange = keyExchange;
+        this.cipher = cipher;
+        this.keySize = keySize;
+        this.cipherChaining = cipherChaining;
+        this.cipherPadding = cipherPadding;
+        this.macName = macName;
+        this.macSize = macSize;
+    }
+
+    static CipherInfoGenerator cig;
+    static {
+        try {
+            cig = new CipherInfoGenerator();
+        } catch (ReflectiveOperationException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static CipherInfo generateInfo(String name) {
+        if (cig == null) {
+            return null;
+        }
+        try {
+            return cig.generateInfo(name);
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public String getSuiteName() {
+        return suiteName;
+    }
+
+    /**
+     * 5: ECDHE, AES||CAMELLIA, keysize >=256 <br>
+     * 4: DHE, AES||CAMELLIA, keysize >= 256<br>
+     * 3: ECDHE|| DHE, AES||CAMELLIA<br>
+     * 2: ECDHE||DHE<br>
+     * 1: RSA||DSA <br>
+     * 0: Others
+     * 
+     * @return the strength
+     */
+    public int getStrength() {
+        if (cipher.equals("NULL") || cipher.equals("RC4") || cipher.contains("DES")) {
+            return 0;
+        }
+        boolean ecdhe = keyExchange.startsWith("ECDHE");
+        boolean dhe = keyExchange.startsWith("DHE");
+        boolean pfs = ecdhe || dhe;
+        boolean goodCipher = cipher.equals("AES") || cipher.equals("CAMELLIA");
+        if (ecdhe && goodCipher && keySize >= 256) {
+            return 5;
+        }
+        if (dhe && goodCipher && keySize >= 256) {
+            return 4;
+        }
+        if (pfs && goodCipher) {
+            return 3;
+        }
+        if (pfs) {
+            return 2;
+        }
+        if (keyExchange.equals("RSA") || keyExchange.equals("DSA")) {
+            return 1;
+        }
+        return 0;
+    }
+
+    private static final String[] CIPHER_RANKING = new String[] {
+            "CAMELLIA", "AES", "RC4", "3DES", "DES", "DES40"
+    };
+
+    @Override
+    public String toString() {
+        return "CipherInfo [keyExchange=" + keyExchange + ", cipher=" + cipher + ", keySize=" + keySize + ", cipherChaining=" + cipherChaining + ", cipherPadding=" + cipherPadding + ", macName=" + macName + ", macSize=" + macSize + "]";
+    }
+
+    /**
+     * ECDHE<br>
+     * GCM<br>
+     * Cipher {@link #CIPHER_RANKING}<br>
+     * Cipher {@link #keySize}<br>
+     * HMAC<br>
+     * HMAC size<br>
+     * 
+     * @return
+     */
+    @Override
+    public int compareTo(CipherInfo o) {
+        int myStrength = getStrength();
+        int oStrength = o.getStrength();
+        if (myStrength > oStrength) {
+            return -1;
+        }
+        if (myStrength < oStrength) {
+            return 1;
+        }
+        // TODO sort SSL/TLS
+        boolean myEcdhe = keyExchange.startsWith("ECDHE");
+        boolean oEcdhe = o.keyExchange.startsWith("ECDHE");
+        if (myEcdhe && !oEcdhe) {
+            return -1;
+        }
+        if ( !myEcdhe && oEcdhe) {
+            return 1;
+        }
+        boolean myGCM = "GCM".equals(cipherChaining);
+        boolean oGCM = "GCM".equals(o.cipherChaining);
+        if (myGCM && !oGCM) {
+            return -1;
+        }
+        if ( !myGCM && oGCM) {
+            return 1;
+        }
+        if ( !cipher.equals(o.cipher)) {
+
+            for (String testCipher : CIPHER_RANKING) {
+                if (cipher.equals(testCipher)) {
+                    return -1;
+                }
+                if (o.cipher.equals(testCipher)) {
+                    return 1;
+                }
+            }
+            if (cipher.equals("NULL")) {
+                return 1;
+            }
+            if (o.cipher.equals("NULL")) {
+                return -1;
+            }
+        }
+        if (keySize > o.keySize) {
+            return -1;
+        }
+        if (keySize < o.keySize) {
+            return 1;
+        }
+        boolean mySHA = macName.startsWith("SHA");
+        boolean oSHA = o.macName.startsWith("SHA");
+        if ( !mySHA && oSHA) {
+            return -1;
+        }
+        if (mySHA && !oSHA) {
+            return 1;
+        }
+        if (macSize > o.macSize) {
+            return -1;
+        }
+        if (macSize < o.macSize) {
+            return 1;
+        }
+
+        return suiteName.compareTo(o.suiteName);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof CipherInfo) {
+            return 0 == this.compareTo((CipherInfo) o);
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((cipher == null) ? 0 : cipher.hashCode());
+        result = prime * result + ((cipherChaining == null) ? 0 : cipherChaining.hashCode());
+        result = prime * result + ((cipherPadding == null) ? 0 : cipherPadding.hashCode());
+        result = prime * result + ((keyExchange == null) ? 0 : keyExchange.hashCode());
+        result = prime * result + keySize;
+        result = prime * result + ((macName == null) ? 0 : macName.hashCode());
+        result = prime * result + macSize;
+        result = prime * result + ((suiteName == null) ? 0 : suiteName.hashCode());
+        return result;
+    }
+
+    static String[] cipherRanking = null;
+
+    public static String[] getCompleteRanking() {
+        if (cipherRanking == null) {
+            String[] ciphers = filterCiphers((Iterable<String>) cig.names.keySet());
+            cipherRanking = ciphers;
+        }
+        return cipherRanking;
+    }
+
+    private static String[] filterCiphers(Iterable<String> toFilter) {
+        TreeSet<CipherInfo> chosenCiphers = new TreeSet<CipherInfo>();
+        for (String o : toFilter) {
+            String s = o;
+            CipherInfo info = CipherInfo.generateInfo(s);
+            if (info != null) {
+                if (info.getStrength() > 1) {
+                    chosenCiphers.add(info);
+                }
+            }
+        }
+        String[] ciphers = new String[chosenCiphers.size()];
+        int counter = 0;
+        for (CipherInfo i : chosenCiphers) {
+            ciphers[counter++] = i.getSuiteName();
+        }
+        return ciphers;
+    }
+
+    public static String[] filter(String[] supportedCipherSuites) {
+        return filterCiphers(Arrays.asList(supportedCipherSuites));
+    }
 }
diff --git a/src/org/cacert/gigi/util/DNSUtil.java b/src/org/cacert/gigi/util/DNSUtil.java
new file mode 100644 (file)
index 0000000..31fb1d6
--- /dev/null
@@ -0,0 +1,79 @@
+package org.cacert.gigi.util;
+
+import java.util.Arrays;
+import java.util.Hashtable;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.InitialDirContext;
+
+public class DNSUtil {
+
+    private static InitialDirContext context;
+    static {
+        Hashtable<String, String> env = new Hashtable<String, String>();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
+        try {
+            context = new InitialDirContext(env);
+        } catch (NamingException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    public static String[] getNSNames(String name) throws NamingException {
+        Attributes dnsLookup = context.getAttributes(name, new String[] {
+                "NS"
+        });
+        return extractTextEntries(dnsLookup.get("NS"));
+    }
+
+    public static String[] getTXTEntries(String name, String server) throws NamingException {
+        Hashtable<String, String> env = new Hashtable<String, String>();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
+        env.put(Context.AUTHORITATIVE, "true");
+        env.put(Context.PROVIDER_URL, "dns://" + server);
+        InitialDirContext context = new InitialDirContext(env);
+        try {
+
+            Attributes dnsLookup = context.getAttributes(name, new String[] {
+                    "TXT"
+            });
+            return extractTextEntries(dnsLookup.get("TXT"));
+        } finally {
+            context.close();
+        }
+
+    }
+
+    private static String[] extractTextEntries(Attribute nsRecords) throws NamingException {
+        if (nsRecords == null) {
+            return new String[] {};
+        }
+        String[] result = new String[nsRecords.size()];
+        for (int i = 0; i < result.length; i++) {
+            result[i] = (String) nsRecords.get(i);
+        }
+        return result;
+    }
+
+    public static String[] getMXEntries(String domain) throws NamingException {
+        Attributes dnsLookup = context.getAttributes(domain, new String[] {
+                "MX"
+        });
+        return extractTextEntries(dnsLookup.get("MX"));
+    }
+
+    public static void main(String[] args) throws NamingException {
+        if (args[0].equals("MX")) {
+            System.out.println(Arrays.toString(getMXEntries(args[1])));
+        } else if (args[0].equals("NS")) {
+            System.out.println(Arrays.toString(getNSNames(args[1])));
+        } else if (args[0].equals("TXT")) {
+            System.out.println(Arrays.toString(getTXTEntries(args[1], args[2])));
+        }
+    }
+
+}
diff --git a/src/org/cacert/gigi/util/DayDate.java b/src/org/cacert/gigi/util/DayDate.java
new file mode 100644 (file)
index 0000000..0b7ba38
--- /dev/null
@@ -0,0 +1,70 @@
+package org.cacert.gigi.util;
+
+import java.sql.Date;
+
+/**
+ * This Class consists of a millisecond timestamp that is only interesting up to
+ * day-precision.
+ */
+public class DayDate {
+
+    public static final long MILLI_DAY = 24 * 60 * 60 * 1000;
+
+    private long time;
+
+    /**
+     * Creates a new {@link DayDate} from the SQL Day-exact variant {@link Date}
+     * .
+     * 
+     * @see #toSQLDate()
+     */
+    public DayDate(Date date) {
+        this(date.getTime());
+    }
+
+    /**
+     * Creates a new {@link DayDate} based on the given millisecond timestamp.
+     * 
+     * @param millis
+     *            the timestamp to create the Date from.
+     * @throws IllegalArgumentException
+     *             if the parameter contains more precision than needed.
+     */
+    public DayDate(long millis) {
+        this.time = millis;
+        if (millis % MILLI_DAY != 0) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Gets the enclosed timestamp.
+     * 
+     * @return the enclosed timestamp.
+     */
+    public long getTime() {
+        return time;
+    }
+
+    /**
+     * Converts this DayDate to an {@link Date}.
+     * 
+     * @return the corresponding {@link Date}
+     * @see #DayDate(Date)
+     */
+    public Date toSQLDate() {
+        return new Date(time);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if ( !(obj instanceof DayDate)) {
+            throw new Error("You may not compare this date somthing other than a DayDate");
+        }
+        return ((DayDate) obj).time == time;
+    }
+
+    public java.util.Date toDate() {
+        return new java.util.Date(time);
+    }
+}
index 9303d8d95737dad979b40e05b91e4f7e89e852d6..ed943cbe103f435d631f109546ca955817cf0583 100644 (file)
@@ -1,12 +1,13 @@
 package org.cacert.gigi.util;
 
 public class HTMLEncoder {
-       public static String encodeHTML(String s) {
-               s = s.replace("&", "&amp;");
-               s = s.replace("<", "&lt;");
-               s = s.replace(">", "&gt;");
-               s = s.replace("\"", "&quot;");
-               s = s.replace("'", "&#39;");
-               return s;
-       }
+
+    public static String encodeHTML(String s) {
+        s = s.replace("&", "&amp;");
+        s = s.replace("<", "&lt;");
+        s = s.replace(">", "&gt;");
+        s = s.replace("\"", "&quot;");
+        s = s.replace("'", "&#39;");
+        return s;
+    }
 }
diff --git a/src/org/cacert/gigi/util/KeyStorage.java b/src/org/cacert/gigi/util/KeyStorage.java
new file mode 100644 (file)
index 0000000..7ce0d28
--- /dev/null
@@ -0,0 +1,26 @@
+package org.cacert.gigi.util;
+
+import java.io.File;
+
+public class KeyStorage {
+
+    private static final File csr = new File("keys/csr");
+
+    private static final File crt = new File("keys/crt");
+
+    public static File locateCrt(int id) {
+        File parent = new File(crt, (id / 1000) + "");
+        if ( !parent.exists() && !parent.mkdirs()) {
+            throw new Error("cert folder could not be created");
+        }
+        return new File(parent, id + ".crt");
+    }
+
+    public static File locateCsr(int id) {
+        File parent = new File(csr, (id / 1000) + "");
+        if ( !parent.exists() && !parent.mkdirs()) {
+            throw new Error("csr folder could not be created");
+        }
+        return new File(parent, id + ".csr");
+    }
+}
index 657264a6e7c90b8dbf589aee579c0136373e6f49..901e5ef62e46891479bf7af5f12d827d0bd4a1db 100644 (file)
 package org.cacert.gigi.util;
 
-import java.io.PrintWriter;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
 
-import org.cacert.gigi.User;
-import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.Assurance.AssuranceType;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.output.template.SprintfCommand;
 
 public class Notary {
-       public static void writeUserAgreement(int memid, String document,
-                       String method, String comment, boolean active, int secmemid)
-                       throws SQLException {
-               PreparedStatement q = DatabaseConnection
-                               .getInstance()
-                               .prepare(
-                                               "insert into `user_agreements` set `memid`=?, `secmemid`=?,"
-                                                               + " `document`=?,`date`=NOW(), `active`=?,`method`=?,`comment`=?");
-               q.setInt(1, memid);
-               q.setInt(2, secmemid);
-               q.setString(3, document);
-               q.setInt(4, active ? 1 : 0);
-               q.setString(5, method);
-               q.setString(6, comment);
-               q.execute();
-       }
-
-       public static boolean checkAssuranceIsPossible(User assurer, User target,
-                       PrintWriter errOut) {
-               if (assurer.getId() == target.getId()) {
-                       if (errOut != null) {
-                               errOut.println("Cannot assure myself.");
-                       }
-                       return false;
-               }
-               try {
-                       PreparedStatement ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "SELECT 1 FROM `notary` where `to`=? and `from`=? AND `deleted`=0");
-                       ps.setInt(1, target.getId());
-                       ps.setInt(2, assurer.getId());
-                       ResultSet rs = ps.executeQuery();
-                       if (rs.next()) {
-                               if (errOut != null) {
-                                       errOut.println("You already assured this person.");
-                               }
-                               rs.close();
-                               return false;
-                       }
-                       rs.close();
-                       if (!assurer.canAssure()) {
-                               if (errOut != null) {
-                                       errOut.println("You cannot assure.");
-                               }
-                               return false;
-                       }
-               } catch (SQLException e) {
-                       e.printStackTrace();
-               }
-               return true;
-       }
-
-       public synchronized static boolean assure(User assurer, User target,
-                       int awarded, String location, String date) throws SQLException {
-               if (!checkAssuranceIsPossible(assurer, target, null)) {
-                       return false;
-               }
-               User u = new User(target.getId());
-               if (!u.equals(target)) {
-                       return false;
-               }
-               System.out.println("Would now assure.");
-               if (awarded > assurer.getMaxAssurePoints()) {
-                       return false;
-               }
-
-               PreparedStatement ps = DatabaseConnection
-                               .getInstance()
-                               .prepare(
-                                               "INSERT INTO `notary` SET `from`=?, `to`=?, `points`=?, `location`=?, `date`=?");
-               ps.setInt(1, assurer.getId());
-               ps.setInt(2, target.getId());
-               ps.setInt(3, awarded);
-               ps.setString(4, location);
-               ps.setString(5, date);
-               ps.execute();
-               return true;
-       }
+
+    public static void writeUserAgreement(User member, String document, String method, String comment, boolean active, int secmemid) {
+        try (GigiPreparedStatement q = new GigiPreparedStatement("INSERT INTO `user_agreements` SET `memid`=?, `secmemid`=?," + " `document`=?,`date`=NOW(), `active`=?,`method`=?,`comment`=?")) {
+            q.setInt(1, member.getId());
+            q.setInt(2, secmemid);
+            q.setString(3, document);
+            q.setBoolean(4, active);
+            q.setString(5, method);
+            q.setString(6, comment);
+            q.execute();
+        }
+    }
+
+    public static void checkAssuranceIsPossible(User assurer, User target) throws GigiApiException {
+        if (assurer.getId() == target.getId()) {
+            throw new GigiApiException("You cannot assure yourself.");
+        }
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `notary` where `to`=? and `from`=? AND `deleted` IS NULL")) {
+            ps.setInt(1, target.getId());
+            ps.setInt(2, assurer.getId());
+            GigiResultSet rs = ps.executeQuery();
+            if (rs.next()) {
+                rs.close();
+                throw new GigiApiException("You have already assured this member.");
+            }
+        }
+        if ( !assurer.canAssure()) {
+            throw new GigiApiException("You are not an assurer.");
+        }
+    }
+
+    public static final Group ASSURER_BLOCKED = Group.getByString("blockedassurer");
+
+    public static final Group ASSUREE_BLOCKED = Group.getByString("blockedassuree");
+
+    /**
+     * This method assures another user.
+     * 
+     * @see User#canAssure() (for assurer)
+     * @see #checkAssuranceIsPossible(User, User) (for assurer or assuree)
+     * @param assurer
+     *            the person that wants to assure
+     * @param assuree
+     *            the person that should be assured
+     * @param assureeName
+     *            the Name that was personally verified
+     * @param dob
+     *            the Date of birth that the assurer verified
+     * @param awarded
+     *            the points that should be awarded in total
+     * @param location
+     *            the location where the assurance took place
+     * @param date
+     *            the date when the assurance took place
+     * @throws GigiApiException
+     *             if the assurance fails (for various reasons)
+     */
+    public synchronized static void assure(User assurer, User assuree, Name assureeName, DayDate dob, int awarded, String location, String date, AssuranceType type) throws GigiApiException {
+        may(assurer, assuree, AssuranceType.FACE_TO_FACE);
+        GigiApiException gae = new GigiApiException();
+        if ( !gae.isEmpty()) {
+            throw gae;
+        }
+        if (date == null || date.equals("")) {
+            gae.mergeInto(new GigiApiException("You must enter the date when you met the assuree."));
+        } else {
+            try {
+                Date d = DateSelector.getDateFormat().parse(date);
+                Calendar gc = GregorianCalendar.getInstance();
+                gc.setTimeInMillis(System.currentTimeMillis());
+                gc.add(Calendar.HOUR_OF_DAY, 12);
+                if (d.getTime() > gc.getTimeInMillis()) {
+                    gae.mergeInto(new GigiApiException("You must not enter a date in the future."));
+                }
+            } catch (ParseException e) {
+                gae.mergeInto(new GigiApiException("You must enter the date in this format: YYYY-MM-DD."));
+            }
+        }
+        // check location, min 3 characters
+        if (location == null || location.equals("")) {
+            gae.mergeInto(new GigiApiException("You failed to enter a location of your meeting."));
+        } else if (location.length() <= 2) {
+            gae.mergeInto(new GigiApiException("You must enter a location with at least 3 characters eg town and country."));
+        }
+        synchronized (assuree) {
+
+            try {
+                checkAssuranceIsPossible(assurer, assuree);
+            } catch (GigiApiException e) {
+                gae.mergeInto(e);
+            }
+
+            if ( !assuree.getName().equals(assureeName) || !assuree.getDoB().equals(dob)) {
+                gae.mergeInto(new GigiApiException("The person you are assuring changed his personal details."));
+            }
+            if (awarded < 0) {
+                gae.mergeInto(new GigiApiException("The points you are trying to award are out of range."));
+            } else {
+                if (type == AssuranceType.NUCLEUS) {
+                    if (awarded > 50) {
+                        gae.mergeInto(new GigiApiException("The points you are trying to award are out of range."));
+                    }
+                } else {
+                    if (awarded > assurer.getMaxAssurePoints()) {
+                        gae.mergeInto(new GigiApiException("The points you are trying to award are out of range."));
+                    }
+                }
+            }
+
+            if ( !gae.isEmpty()) {
+                throw gae;
+            }
+
+            if (type == AssuranceType.FACE_TO_FACE) {
+                assureF2F(assurer, assuree, awarded, location, date);
+            } else if (type == AssuranceType.NUCLEUS) {
+                assureNucleus(assurer, assuree, awarded, location, date);
+            } else if (type == AssuranceType.TTP_ASSISTED) {
+                assureTTP(assurer, assuree, awarded, location, date);
+            } else {
+                throw new GigiApiException(SprintfCommand.createSimple("Unknown Assurance type: {0}", type.toString()));
+            }
+            assurer.invalidateMadeAssurances();
+            assuree.invalidateReceivedAssurances();
+        }
+    }
+
+    private static void assureF2F(User assurer, User assuree, int awarded, String location, String date) throws GigiApiException {
+        may(assurer, assuree, AssuranceType.FACE_TO_FACE);
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `notary` SET `from`=?, `to`=?, `points`=?, `location`=?, `date`=?")) {
+            ps.setInt(1, assurer.getId());
+            ps.setInt(2, assuree.getId());
+            ps.setInt(3, awarded);
+            ps.setString(4, location);
+            ps.setString(5, date);
+            ps.execute();
+        }
+    }
+
+    private static void assureTTP(User assurer, User assuree, int awarded, String location, String date) throws GigiApiException {
+        may(assurer, assuree, AssuranceType.TTP_ASSISTED);
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `notary` SET `from`=?, `to`=?, `points`=?, `location`=?, `date`=?, `method`='TTP-Assisted'")) {
+            ps.setInt(1, assurer.getId());
+            ps.setInt(2, assuree.getId());
+            ps.setInt(3, awarded);
+            ps.setString(4, location);
+            ps.setString(5, date);
+            ps.execute();
+            assuree.revokeGroup(assurer, Group.TTP_APPLICANT);
+        }
+    }
+
+    public static void may(User assurer, User assuree, AssuranceType t) throws GigiApiException {
+        if (assuree.isInGroup(ASSUREE_BLOCKED)) {
+            throw new GigiApiException("The assuree is blocked.");
+        }
+        if (assurer.isInGroup(ASSURER_BLOCKED)) {
+            throw new GigiApiException("The assurer is blocked.");
+        }
+
+        if (t == AssuranceType.NUCLEUS) {
+            if ( !assurer.isInGroup(Group.NUCLEUS_ASSURER)) {
+                throw new GigiApiException("Assurer needs to be Nucleus Assurer.");
+            }
+            return;
+        } else if (t == AssuranceType.TTP_ASSISTED) {
+            if ( !assurer.isInGroup(Group.TTP_ASSURER)) {
+                throw new GigiApiException("Assurer needs to be TTP Assurer.");
+            }
+            if ( !assuree.isInGroup(Group.TTP_APPLICANT)) {
+                throw new GigiApiException("Assuree needs to be TTP Applicant.");
+            }
+            return;
+        } else if (t == AssuranceType.FACE_TO_FACE) {
+            return;
+        }
+        throw new GigiApiException("Assurance type not possible.");
+    }
+
+    private static void assureNucleus(User assurer, User assuree, int awarded, String location, String date) throws GigiApiException {
+        may(assurer, assuree, AssuranceType.NUCLEUS);
+        // Do up to 35 points as f2f
+        int f2fPoints = Math.min(assurer.getMaxAssurePoints(), awarded);
+        assureF2F(assurer, assuree, f2fPoints, location, date);
+
+        awarded -= f2fPoints;
+        if (awarded <= 0) {
+            return;
+        }
+
+        // Assure remaining points as "Nucleus Bonus"
+        // Valid for 4 Weeks = 28 days
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `notary` SET `from`=?, `to`=?, `points`=?, `location`=?, `date`=?, `method`='Nucleus Bonus', `expire` = CURRENT_TIMESTAMP + interval '28 days'")) {
+            ps.setInt(1, assurer.getId());
+            ps.setInt(2, assuree.getId());
+            ps.setInt(3, awarded);
+            ps.setString(4, location);
+            ps.setString(5, date);
+            ps.execute();
+        }
+    }
 }
diff --git a/src/org/cacert/gigi/util/PEM.java b/src/org/cacert/gigi/util/PEM.java
new file mode 100644 (file)
index 0000000..3be4653
--- /dev/null
@@ -0,0 +1,29 @@
+package org.cacert.gigi.util;
+
+import java.util.Base64;
+import java.util.regex.Pattern;
+
+public class PEM {
+
+    public static final Pattern LINE = Pattern.compile("(.{64})(?=.)");
+
+    public static String encode(String type, byte[] data) {
+        return "-----BEGIN " + type + "-----\n" + //
+                formatBase64(data) + //
+                "\n-----END " + type + "-----";
+    }
+
+    public static byte[] decode(String type, String data) {
+        data = data.replaceAll("-----BEGIN " + type + "-----", "");
+        // Remove the first and last lines
+        data = data.replaceAll("-----END " + type + "-----", "");
+        data = data.replace("\n", "").replace("\r", "").replace(" ", "").replace("\t", "");
+        // Base64 decode the data
+        return Base64.getDecoder().decode(data);
+
+    }
+
+    public static String formatBase64(byte[] bytes) {
+        return LINE.matcher(Base64.getEncoder().encodeToString(bytes)).replaceAll("$1\n");
+    }
+}
index edc1ad53a927782018c31e883be15a847192a243..6d598e77575f706fb8d6804eb9dcab748394b408 100644 (file)
@@ -1,30 +1,73 @@
 package org.cacert.gigi.util;
 
+import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
+import com.lambdaworks.crypto.SCryptUtil;
+
 public class PasswordHash {
-       public static boolean verifyHash(String password, String hash) {
-               String newhash = sha1(password);
-               return newhash.equals(hash);
-       }
 
-       private static String sha1(String password) {
-               try {
-                       MessageDigest md = MessageDigest.getInstance("SHA1");
-                       byte[] digest = md.digest(password.getBytes());
-                       StringBuffer res = new StringBuffer(digest.length * 2);
-                       for (int i = 0; i < digest.length; i++) {
-                               res.append(Integer.toHexString((digest[i] & 0xF0) >> 4));
-                               res.append(Integer.toHexString(digest[i] & 0xF));
-                       }
-                       return res.toString();
-               } catch (NoSuchAlgorithmException e) {
-                       throw new Error(e);
-               }
-       }
+    /**
+     * Verifies a password hash.
+     * 
+     * @param password
+     *            The password that should result in the given hash.
+     * @param hash
+     *            The hash to verify the password against.
+     * @return
+     *         <ul>
+     *         <li><code>null</code>, if the password was valid</li>
+     *         <li><code>hash</code>, if the password is valid and the hash
+     *         doesn't need to be updated</li>
+     *         <li>a new hash, if the password is valid but the hash in the
+     *         database needs to be updated.</li>
+     *         </ul>
+     */
+    public static String verifyHash(String password, String hash) {
+        if (password == null || password.isEmpty()) {
+            return null;
+        }
+        if (hash.contains("$")) {
+            if (SCryptUtil.check(password, hash)) {
+                return hash;
+            } else {
+                return null;
+            }
+        }
+        String newhash = sha1(password);
+        boolean match = true;
+        if (newhash.length() != hash.length()) {
+            match = false;
+        }
+        for (int i = 0; i < newhash.length(); i++) {
+            match &= newhash.charAt(i) == hash.charAt(i);
+        }
+        if (match) {
+            return hash(password);
+        } else {
+            return null;
+        }
+    }
+
+    public static String sha1(String password) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA1");
+            byte[] digest = md.digest(password.getBytes("UTF-8"));
+            StringBuffer res = new StringBuffer(digest.length * 2);
+            for (int i = 0; i < digest.length; i++) {
+                res.append(Integer.toHexString((digest[i] & 0xF0) >> 4));
+                res.append(Integer.toHexString(digest[i] & 0xF));
+            }
+            return res.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new Error(e);
+        } catch (UnsupportedEncodingException e) {
+            throw new Error(e);
+        }
+    }
 
-       public static String hash(String password) {
-               return sha1(password);
-       }
+    public static String hash(String password) {
+        return SCryptUtil.scrypt(password, 1 << 14, 8, 1);
+    }
 }
index 07898f263b681715f141d5405ce236bf4d07efd5..a1d2145075ceb94b69fef7442fa3588de4977f84 100644 (file)
@@ -2,80 +2,95 @@ package org.cacert.gigi.util;
 
 import java.util.regex.Pattern;
 
-import org.cacert.gigi.User;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Name;
 
 public class PasswordStrengthChecker {
-       static Pattern digits = Pattern.compile("\\d");
-       static Pattern lower = Pattern.compile("[a-z]");
-       static Pattern upper = Pattern.compile("[A-Z]");
-       static Pattern whitespace = Pattern.compile("\\s");
-       static Pattern special = Pattern.compile("(?!\\s)\\W");
-       private PasswordStrengthChecker() {
-       }
-       private static int checkpwlight(String pw) {
-               int points = 0;
-               if (pw.length() > 15) {
-                       points++;
-               }
-               if (pw.length() > 20) {
-                       points++;
-               }
-               if (pw.length() > 25) {
-                       points++;
-               }
-               if (pw.length() > 30) {
-                       points++;
-               }
-               if (digits.matcher(pw).find()) {
-                       points++;
-               }
-               if (lower.matcher(pw).find()) {
-                       points++;
-               }
-               if (upper.matcher(pw).find()) {
-                       points++;
-               }
-               if (special.matcher(pw).find()) {
-                       points++;
-               }
-               if (whitespace.matcher(pw).find()) {
-                       points++;
-               }
-               return points;
-       }
-       public static int checkpw(String pw, User u) {
-               if (pw == null) {
-                       return 0;
-               }
-               int light = checkpwlight(pw);
-               if (contained(pw, u.getEmail())) {
-                       light -= 2;
-               }
-               if (contained(pw, u.getFname())) {
-                       light -= 2;
-               }
-               if (contained(pw, u.getLname())) {
-                       light -= 2;
-               }
-               if (contained(pw, u.getMname())) {
-                       light -= 2;
-               }
-               if (contained(pw, u.getSuffix())) {
-                       light -= 2;
-               }
-               // TODO dictionary check
-               return light;
-       }
-       private static boolean contained(String pw, String check) {
-               if (check == null || check.equals("")) {
-                       return false;
-               }
-               if (pw.contains(check)) {
-                       return true;
-               }
-               if (check.contains(pw)) {
-                       return true;
-               }
-               return false;
-       }
+
+    private static Pattern digits = Pattern.compile("\\d");
+
+    private static Pattern lower = Pattern.compile("[a-z]");
+
+    private static Pattern upper = Pattern.compile("[A-Z]");
+
+    private static Pattern whitespace = Pattern.compile("\\s");
+
+    private static Pattern special = Pattern.compile("(?!\\s)\\W");
+
+    private PasswordStrengthChecker() {}
+
+    private static int checkpwlight(String pw) {
+        int points = 0;
+        if (pw.length() > 15) {
+            points++;
+        }
+        if (pw.length() > 20) {
+            points++;
+        }
+        if (pw.length() > 25) {
+            points++;
+        }
+        if (pw.length() > 30) {
+            points++;
+        }
+        if (digits.matcher(pw).find()) {
+            points++;
+        }
+        if (lower.matcher(pw).find()) {
+            points++;
+        }
+        if (upper.matcher(pw).find()) {
+            points++;
+        }
+        if (special.matcher(pw).find()) {
+            points++;
+        }
+        if (whitespace.matcher(pw).find()) {
+            points++;
+        }
+        return points;
+    }
+
+    public static int checkpw(String pw, Name name, String email) {
+        if (pw == null) {
+            return 0;
+        }
+        int light = checkpwlight(pw);
+        if (contained(pw, email)) {
+            light -= 2;
+        }
+        if (contained(pw, name.getFname())) {
+            light -= 2;
+        }
+        if (contained(pw, name.getLname())) {
+            light -= 2;
+        }
+        if (contained(pw, name.getMname())) {
+            light -= 2;
+        }
+        if (contained(pw, name.getSuffix())) {
+            light -= 2;
+        }
+        // TODO dictionary check
+        return light;
+    }
+
+    public static void assertStrongPassword(String pw, Name name, String email) throws GigiApiException {
+        if (checkpw(pw, name, email) < 3) {
+            throw new GigiApiException("The Pass Phrase you submitted failed to contain enough" + " differing characters and/or contained words from" + " your name and/or email address.");
+        }
+    }
+
+    private static boolean contained(String pw, String check) {
+        if (check == null || check.equals("")) {
+            return false;
+        }
+        if (pw.contains(check)) {
+            return true;
+        }
+        if (check.contains(pw)) {
+            return true;
+        }
+        return false;
+    }
 }
diff --git a/src/org/cacert/gigi/util/PublicSuffixes.java b/src/org/cacert/gigi/util/PublicSuffixes.java
new file mode 100644 (file)
index 0000000..51c2edf
--- /dev/null
@@ -0,0 +1,131 @@
+package org.cacert.gigi.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.IDN;
+import java.util.HashSet;
+
+public class PublicSuffixes {
+
+    HashSet<String> suffixes = new HashSet<>();
+
+    HashSet<String> wildcards = new HashSet<>();
+
+    HashSet<String> exceptions = new HashSet<>();
+
+    private static final String url = "https://publicsuffix.org/list/effective_tld_names.dat";
+
+    private static PublicSuffixes instance;
+
+    private static PublicSuffixes generateDefault() throws IOException {
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(PublicSuffixes.class.getResourceAsStream("effective_tld_names.dat"), "UTF-8"))) {
+            return new PublicSuffixes(br);
+        }
+    }
+
+    public synchronized static PublicSuffixes getInstance() {
+        if (instance == null) {
+            try {
+                instance = generateDefault();
+            } catch (IOException e) {
+                throw new Error(e);
+            }
+        }
+        return instance;
+    }
+
+    private PublicSuffixes(BufferedReader br) throws IOException {
+        String line;
+        while ((line = br.readLine()) != null) {
+            if (line.startsWith("//")) {
+                continue;
+            }
+            if (line.isEmpty()) {
+                continue;
+            }
+            String[] lineParts = line.split("\\s", 2);
+            if (lineParts.length == 0) {
+                throw new Error("split had strange behavior");
+            }
+            line = lineParts[0];
+            if (line.startsWith("*.")) {
+                String data = line.substring(2);
+                if (data.contains("*") || data.contains("!")) {
+                    System.out.println("Error! unparsable public suffix line: " + line);
+                    continue;
+                }
+                addWildcard(IDN.toASCII(data));
+            } else if (line.startsWith("!")) {
+                String data = line.substring(1);
+                if (data.contains("*") || data.contains("!")) {
+                    System.out.println("Error! unparsable public suffix line: " + line);
+                    continue;
+                }
+                addException(IDN.toASCII(data));
+            } else {
+                if (line.contains("*") || line.contains("!")) {
+                    System.out.println("Error! unparsable public suffix line: " + line);
+                    continue;
+                }
+                addSuffix(IDN.toASCII(line));
+            }
+        }
+    }
+
+    private void addWildcard(String data) {
+        wildcards.add(data);
+    }
+
+    private void addException(String data) {
+        exceptions.add(data);
+    }
+
+    private void addSuffix(String line) {
+        suffixes.add(line);
+    }
+
+    public String getRegistrablePart(String domain) {
+        if (domain == null) {
+            return null;
+        }
+        if (domain.startsWith(".")) {
+            return null;
+        }
+        if (isSuffix(domain) && !exceptions.contains(domain)) {
+            return null;
+        }
+        return getPublicSuffix0(domain);
+    }
+
+    private String getPublicSuffix0(String domain) {
+
+        int d = domain.indexOf('.');
+        if (d == -1) {
+            return null;
+        }
+        if (exceptions.contains(domain)) {
+            return domain;
+        }
+        String nextDomain = domain.substring(d + 1);
+        if (isSuffix(nextDomain)) {
+            return domain;
+        }
+
+        return getPublicSuffix0(nextDomain);
+    }
+
+    private boolean isSuffix(String domain) {
+        if (suffixes.contains(domain)) {
+            return true;
+        }
+        if (exceptions.contains(domain)) {
+            return false;
+        }
+        int idx = domain.indexOf('.');
+        if (idx != -1 && wildcards.contains(domain.substring(idx + 1))) {
+            return true;
+        }
+        return false;
+    }
+}
index 7d87b7bd7273c1f62f58b8d506cdcf8c409746d2..8a11c8f096abcb6868dd15f517382d4636f1a589 100644 (file)
@@ -3,23 +3,25 @@ package org.cacert.gigi.util;
 import java.security.SecureRandom;
 
 public class RandomToken {
-       static SecureRandom sr = new SecureRandom();
-       public static String generateToken(int length) {
-               StringBuffer token = new StringBuffer();
-               for (int i = 0; i < length; i++) {
-                       int rand = sr.nextInt(26 * 2 + 10);
-                       if (rand < 10) {
-                               token.append((char) ('0' + rand));
-                               continue;
-                       }
-                       rand -= 10;
-                       if (rand < 26) {
-                               token.append((char) ('a' + rand));
-                               continue;
-                       }
-                       rand -= 26;
-                       token.append((char) ('A' + rand));
-               }
-               return token.toString();
-       }
+
+    private static SecureRandom sr = new SecureRandom();
+
+    public static String generateToken(int length) {
+        StringBuffer token = new StringBuffer();
+        for (int i = 0; i < length; i++) {
+            int rand = sr.nextInt(26 * 2 + 10);
+            if (rand < 10) {
+                token.append((char) ('0' + rand));
+                continue;
+            }
+            rand -= 10;
+            if (rand < 26) {
+                token.append((char) ('a' + rand));
+                continue;
+            }
+            rand -= 26;
+            token.append((char) ('A' + rand));
+        }
+        return token.toString();
+    }
 }
diff --git a/src/org/cacert/gigi/util/RateLimit.java b/src/org/cacert/gigi/util/RateLimit.java
new file mode 100644 (file)
index 0000000..65c1668
--- /dev/null
@@ -0,0 +1,78 @@
+package org.cacert.gigi.util;
+
+import java.util.HashMap;
+import java.util.TreeSet;
+
+public class RateLimit {
+
+    private class Entry implements Comparable<Entry> {
+
+        long firstAccess;
+
+        int count = 1;
+
+        String feature;
+
+        public Entry(long firstAccess, String feature) {
+            this.firstAccess = firstAccess;
+            this.feature = feature;
+        }
+
+        public void access() {
+            count++;
+        }
+
+        @Override
+        public int compareTo(Entry o) {
+            return feature.compareTo(o.feature);
+        }
+
+        public boolean isExpired() {
+            return firstAccess + time < System.currentTimeMillis();
+        }
+
+    }
+
+    private final int maxcount;
+
+    private final long time;
+
+    TreeSet<Entry> set = new TreeSet<Entry>();
+
+    HashMap<String, Entry> feat = new HashMap<>();
+
+    public RateLimit(int maxcount, long time) {
+        this.maxcount = maxcount;
+        this.time = time;
+    }
+
+    public synchronized boolean isLimitExceeded(String feature) {
+        clean();
+        Entry e = feat.get(feature);
+        if (e == null) {
+            e = new Entry(System.currentTimeMillis(), feature);
+            set.add(e);
+            feat.put(feature, e);
+        } else {
+            e.access();
+        }
+        return e.count > maxcount;
+    }
+
+    private void clean() {
+        while (set.size() > 0) {
+            Entry e = set.last();
+            if (e.isExpired()) {
+                set.remove(e);
+                feat.remove(e.feature);
+            } else {
+                return;
+            }
+        }
+    }
+
+    public synchronized void bypass() {
+        set.clear();
+        feat.clear();
+    }
+}
index 82b124c42f5391a6f8a23273581ed515d0d2cfaf..5ae4e1b320435fbf40f7bce0f020b1d042fcf072 100644 (file)
@@ -1,5 +1,93 @@
 package org.cacert.gigi.util;
 
+import java.util.Properties;
+
 public class ServerConstants {
-       public static final String NORMAL_HOST_NAME = "http://www.cacert.org";
+
+    private static String wwwHostName = "www.cacert.local";
+
+    private static String secureHostName = "secure.cacert.local";
+
+    private static String staticHostName = "static.cacert.local";
+
+    private static String apiHostName = "api.cacert.local";
+
+    private static String securePort, port;
+
+    private static String suffix = "cacert.local";
+
+    public static void init(Properties conf) {
+        securePort = port = "";
+        if ( !conf.getProperty("https.port").equals("443")) {
+            securePort = ":" + conf.getProperty("https.port");
+        }
+        if ( !conf.getProperty("http.port").equals("80")) {
+            port = ":" + conf.getProperty("http.port");
+        }
+        wwwHostName = conf.getProperty("name.www");
+        secureHostName = conf.getProperty("name.secure");
+        staticHostName = conf.getProperty("name.static");
+        apiHostName = conf.getProperty("name.api");
+        suffix = conf.getProperty("name.suffix", conf.getProperty("name.www").substring(4));
+
+    }
+
+    public static String getSecureHostName() {
+        return secureHostName;
+    }
+
+    public static String getStaticHostName() {
+        return staticHostName;
+    }
+
+    public static String getWwwHostName() {
+        return wwwHostName;
+    }
+
+    public static String getApiHostName() {
+        return apiHostName;
+    }
+
+    public static String getSecureHostNamePort() {
+        return secureHostName + securePort;
+    }
+
+    public static String getStaticHostNamePortSecure() {
+        return staticHostName + securePort;
+    }
+
+    public static String getWwwHostNamePortSecure() {
+        return wwwHostName + securePort;
+    }
+
+    public static String getStaticHostNamePort() {
+        return staticHostName + port;
+    }
+
+    public static String getWwwHostNamePort() {
+        return wwwHostName + port;
+    }
+
+    public static String getApiHostNamePort() {
+        return apiHostName + securePort;
+    }
+
+    public static int getSecurePort() {
+        if (securePort.isEmpty()) {
+            return 443;
+        }
+        return Integer.parseInt(securePort.substring(1, securePort.length()));
+    }
+
+    public static int getPort() {
+        if (port.isEmpty()) {
+            return 80;
+        }
+        return Integer.parseInt(port.substring(1, port.length()));
+    }
+
+    public static String getSuffix() {
+        return suffix;
+    }
+
 }
diff --git a/static/menu.js b/static/menu.js
deleted file mode 100644 (file)
index b73827e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-(function() {
-       function explodeMenu(e) {
-               if (document.getElementById(e).className == 'menu hidden') {
-                       document.getElementById(e).className = 'menu';
-               } else {
-                       document.getElementById(e).className = 'menu hidden';
-               }
-       }
-
-       function initMenu() {
-               var Nodes = document.getElementsByTagName('ul');
-               var max = Nodes.length;
-               for (var i = 0; i < max; i++) {
-                       var nodeObj = Nodes.item(i);
-                       if (nodeObj.className.indexOf("menu") > -1 && nodeObj.id != "recom") {
-                               nodeObj.previousSibling.previousSibling.onclick = (function(nid) {
-                                       return function() {
-                                               explodeMenu(nid);
-                                       };
-                               })(nodeObj.id);
-                       }
-               }
-       }
-       (function(oldLoad) {
-               if (oldLoad == undefined) {
-                       window.onload = initMenu;
-               } else {
-                       window.onload = function() {
-                               initMenu();
-                               oldLoad();
-                       }
-               }
-       })(window.onload);
-
-})();
\ No newline at end of file
diff --git a/static/static/css/bootstrap.min.css b/static/static/css/bootstrap.min.css
new file mode 100644 (file)
index 0000000..03cfc06
--- /dev/null
@@ -0,0 +1,14 @@
+/*!
+ * Bootstrap v3.3.5 (http://getbootstrap.com)
+ * Copyright 2011-2015 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+/*!
+ * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=df01705b9285b995c77b)
+ * Config saved to config.json and https://gist.github.com/df01705b9285b995c77b
+ *//*!
+ * Bootstrap v3.3.5 (http://getbootstrap.com)
+ * Copyright 2011-2015 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{background:transparent !important;color:#000 !important;-webkit-box-shadow:none !important;box-shadow:none !important;text-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover,a.text-primary:focus{color:#286090}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#31708f}a.text-info:hover,a.text-info:focus{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover,a.text-warning:focus{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover,a.bg-primary:focus{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:0.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.333333px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);-webkit-background-clip:padding-box;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#337ab7}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:3;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);-webkit-background-clip:padding-box;background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:14px;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,0.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}
\ No newline at end of file
diff --git a/static/static/css/cacert.css b/static/static/css/cacert.css
new file mode 100644 (file)
index 0000000..b944397
--- /dev/null
@@ -0,0 +1,19 @@
+.container {
+  margin-bottom: 30px;
+}
+.quest {
+  font-weight: bold;
+}
+.quest:after {
+  content:"\A";
+  white-space:pre;
+}
+.subtext {
+  color: #c0c0c0;
+}
+.tooltip-inner {
+    white-space:pre-wrap;
+}
+.experthidden{
+       display: none;
+}
similarity index 85%
rename from static/default.css
rename to static/static/default.css
index ac122d6528e92aeb47b907f492210ecab0f2d155..83e9e01d0ff7f980b40c1f4a0d50aa568c18a28b 100644 (file)
@@ -35,14 +35,14 @@ a:hover {
 h1 {
        font: bold 120% Arial ,sans-serif;
        color: #334d55;
-       margin: 0px;
+       margin: 10px;
        padding: 0px;
 }
 
 h2 {
        font: bold 114% Arial ,sans-serif;
        color: #006699;
-       margin: 0px;
+       margin: 10px;
        padding: 0px;
 }
 
@@ -409,27 +409,27 @@ a.glink:hover {
 }
 
 
-/*************** story styles ******************/
+/*************** content styles ******************/
 
-.story {
+.content {
        padding: 10px 0px 0px 10px;
        font-size: 80%;
        min-height: 450px;
 }
 
-.story h3 {
+.content h3 {
        font: bold 125% Arial,sans-serif;
        color: #000000;
 }
 
-.story a.capsule {
+.content a.capsule {
        font: bold 1em Arial,sans-serif;
        color: #005FA9;
        display: block;
        padding-bottom: 5px;
 }
 
-.story a.capsule:hover {
+.content a.capsule:hover {
        text-decoration: underline;
 }
 
@@ -496,6 +496,19 @@ td.storyLeft {
        display: block;
 }
 
+/*** hide top menu ***/
+#pageNav > div > ul.menu {
+       padding-left: 0;
+       border-left: none;
+       margin: 0;
+}
+#pageNav > div{
+       padding: 0;
+}
+
+#pageNav > div > h3.pointer {
+       display: none;
+}
 
 /**************** advert styles *****************/
 
@@ -509,30 +522,6 @@ td.storyLeft {
 
 
 /********************* end **********************/
-
-.DataTD input, .DataTD textarea {
-       font-size: 92%;
-}
-
-.DataTD select, .DataTD option {
-       font-size: 92%;
-}
-
-.DataTD {
-       background-color: #e2e2e2;
-       border-style: inset;
-       border-width: 1px;
-       font-size: 8pt;
-       color: #000000;
-       font-family: Arial, Tahoma, Verdana, Helvetica, sans-serif;
-
-       background: #ffffff;
-       padding: 1px 5px 1px 5px;
-       border: 1px #cfcfcf solid;
-       border-left: 1px #cfcfcf dotted;
-       border-right: 1px #cfcfcf dotted;
-}
-
 .DataTDGrey {
        background-color: #EFEDED;
        border-style: inset;
@@ -579,9 +568,33 @@ td.storyLeft {
        border-collapse: collapse;
        font-family: verdana, sans-serif;
        font-size: 11px;
-       text-align: center;
        margin-left:auto;
        margin-right:auto;
+       width: 700px;
+}
+
+.wrapper .check{
+       text-align: center;
+}
+.wrapper td.radio input[type=radio]{
+       float: left;
+       width: 13px;
+       margin: 3px;
+}
+.wrapper td.radio span.name{
+       font-weight: bold;
+       padding-bottom: 4px;
+       margin-top: 3px;
+/*     display: inline;*/
+}
+.wrapper td.radio div.addinfo{
+/*     display: inline;*/
+       padding-left: 19px;
+}
+
+.wrapper td.radio div.elements{
+       clear: both;
+       height: 10px;
 }
 
 td.greytxt {
@@ -734,4 +747,84 @@ ul.menu.hidden{
 }
 img{
        border: 0;
+}
+formMandatory{
+       color: red;
+}
+.experthidden{
+       display:none;   
+}
+.expertoff{
+       display:none;   
+}
+pre{
+       word-wrap: break-word;
+       white-space: pre-wrap;
+}
+
+.dataTable td {
+       background-color: #e2e2e2;
+       border-style: inset;
+       border-width: 1px;
+       font-size: 8pt;
+       color: #000000;
+       font-family: Arial, Tahoma, Verdana, Helvetica, sans-serif;
+
+       background: #ffffff;
+       padding: 1px 5px 1px 5px;
+       border: 1px #cfcfcf solid;
+       border-left: 1px #cfcfcf dotted;
+       border-right: 1px #cfcfcf dotted;
+}
+
+.dataTable td input {
+       width: 98%;
+       margin-left: 1%;
+       margin-right: 1%;
+}
+.dataTable td nobr input {
+       width: auto;
+}
+.centertext {
+       text-align: center;
+}
+.dataTable th {
+       background: #e2e2e2;
+       font-weight: bold;
+       padding: 1px 5px 1px 5px;
+       border: 1px solid #cfcfcf;
+       border-bottom: 3px double #cfcfcf;
+       border-top: 1px solid #656565;
+       text-align: center;
+}
+
+.dataTable input, .dataTable textarea {
+       font-size: 92%;
+}
+
+.dataTable select, .dataTable option {
+       font-size: 92%;
+}
+.dataTable textarea{
+       width: 100%;
+}
+
+pre.string{
+       display: inline;
+}
+
+.loginbox {background:#F5F7F7;border:2px solid #cccccc;margin:0px auto;height:auto;width:300px;padding:1em;text-align:center;}
+.loginbox .smalltext {font-size:10px;}
+.loginbox label {width:100px;display:block;float:left;}
+.loginbox text {width:166px;display:block;float:left;}
+.loginbox br {clear:left;}
+.loginbox h1 {font-size:1.9em;text-align:center;}
+
+
+.domainPinglogFirstCell{
+       width: 15px;
+}
+textarea.csr{
+       width: 400px;
+       height: 400px;
 }
\ No newline at end of file
diff --git a/static/static/images/bit.png b/static/static/images/bit.png
new file mode 100644 (file)
index 0000000..5597e3b
Binary files /dev/null and b/static/static/images/bit.png differ
diff --git a/static/static/images/cacert4-test.png b/static/static/images/cacert4-test.png
new file mode 100644 (file)
index 0000000..d742232
Binary files /dev/null and b/static/static/images/cacert4-test.png differ
diff --git a/static/static/images/cacert4.png b/static/static/images/cacert4.png
new file mode 100644 (file)
index 0000000..e4650a0
Binary files /dev/null and b/static/static/images/cacert4.png differ
diff --git a/static/static/images/nlnet.png b/static/static/images/nlnet.png
new file mode 100644 (file)
index 0000000..c7e5c46
Binary files /dev/null and b/static/static/images/nlnet.png differ
diff --git a/static/static/images/oan.png b/static/static/images/oan.png
new file mode 100644 (file)
index 0000000..548bb12
Binary files /dev/null and b/static/static/images/oan.png differ
diff --git a/static/static/images/tunix.png b/static/static/images/tunix.png
new file mode 100644 (file)
index 0000000..29adb60
Binary files /dev/null and b/static/static/images/tunix.png differ
diff --git a/static/static/js/bootstrap.min.js b/static/static/js/bootstrap.min.js
new file mode 100644 (file)
index 0000000..437f0a0
--- /dev/null
@@ -0,0 +1,12 @@
+/*!
+ * Bootstrap v3.3.5 (http://getbootstrap.com)
+ * Copyright 2011-2015 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+/*!
+ * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=df01705b9285b995c77b)
+ * Config saved to config.json and https://gist.github.com/df01705b9285b995c77b
+ */
+if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(t){"use strict";var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var i=t(this),n=i.data("bs.alert");n||i.data("bs.alert",n=new o(this)),"string"==typeof e&&n[e].call(i)})}var i='[data-dismiss="alert"]',o=function(e){t(e).on("click",i,this.close)};o.VERSION="3.3.5",o.TRANSITION_DURATION=150,o.prototype.close=function(e){function i(){a.detach().trigger("closed.bs.alert").remove()}var n=t(this),s=n.attr("data-target");s||(s=n.attr("href"),s=s&&s.replace(/.*(?=#[^\s]*$)/,""));var a=t(s);e&&e.preventDefault(),a.length||(a=n.closest(".alert")),a.trigger(e=t.Event("close.bs.alert")),e.isDefaultPrevented()||(a.removeClass("in"),t.support.transition&&a.hasClass("fade")?a.one("bsTransitionEnd",i).emulateTransitionEnd(o.TRANSITION_DURATION):i())};var n=t.fn.alert;t.fn.alert=e,t.fn.alert.Constructor=o,t.fn.alert.noConflict=function(){return t.fn.alert=n,this},t(document).on("click.bs.alert.data-api",i,o.prototype.close)}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.button"),s="object"==typeof e&&e;n||o.data("bs.button",n=new i(this,s)),"toggle"==e?n.toggle():e&&n.setState(e)})}var i=function(e,o){this.$element=t(e),this.options=t.extend({},i.DEFAULTS,o),this.isLoading=!1};i.VERSION="3.3.5",i.DEFAULTS={loadingText:"loading..."},i.prototype.setState=function(e){var i="disabled",o=this.$element,n=o.is("input")?"val":"html",s=o.data();e+="Text",null==s.resetText&&o.data("resetText",o[n]()),setTimeout(t.proxy(function(){o[n](null==s[e]?this.options[e]:s[e]),"loadingText"==e?(this.isLoading=!0,o.addClass(i).attr(i,i)):this.isLoading&&(this.isLoading=!1,o.removeClass(i).removeAttr(i))},this),0)},i.prototype.toggle=function(){var t=!0,e=this.$element.closest('[data-toggle="buttons"]');if(e.length){var i=this.$element.find("input");"radio"==i.prop("type")?(i.prop("checked")&&(t=!1),e.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==i.prop("type")&&(i.prop("checked")!==this.$element.hasClass("active")&&(t=!1),this.$element.toggleClass("active")),i.prop("checked",this.$element.hasClass("active")),t&&i.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var o=t.fn.button;t.fn.button=e,t.fn.button.Constructor=i,t.fn.button.noConflict=function(){return t.fn.button=o,this},t(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(i){var o=t(i.target);o.hasClass("btn")||(o=o.closest(".btn")),e.call(o,"toggle"),t(i.target).is('input[type="radio"]')||t(i.target).is('input[type="checkbox"]')||i.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(e){t(e.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(e.type))})}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.carousel"),s=t.extend({},i.DEFAULTS,o.data(),"object"==typeof e&&e),a="string"==typeof e?e:s.slide;n||o.data("bs.carousel",n=new i(this,s)),"number"==typeof e?n.to(e):a?n[a]():s.interval&&n.pause().cycle()})}var i=function(e,i){this.$element=t(e),this.$indicators=this.$element.find(".carousel-indicators"),this.options=i,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",t.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",t.proxy(this.pause,this)).on("mouseleave.bs.carousel",t.proxy(this.cycle,this))};i.VERSION="3.3.5",i.TRANSITION_DURATION=600,i.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},i.prototype.keydown=function(t){if(!/input|textarea/i.test(t.target.tagName)){switch(t.which){case 37:this.prev();break;case 39:this.next();break;default:return}t.preventDefault()}},i.prototype.cycle=function(e){return e||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(t.proxy(this.next,this),this.options.interval)),this},i.prototype.getItemIndex=function(t){return this.$items=t.parent().children(".item"),this.$items.index(t||this.$active)},i.prototype.getItemForDirection=function(t,e){var i=this.getItemIndex(e),o="prev"==t&&0===i||"next"==t&&i==this.$items.length-1;if(o&&!this.options.wrap)return e;var n="prev"==t?-1:1,s=(i+n)%this.$items.length;return this.$items.eq(s)},i.prototype.to=function(t){var e=this,i=this.getItemIndex(this.$active=this.$element.find(".item.active"));return t>this.$items.length-1||0>t?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(t>i?"next":"prev",this.$items.eq(t))},i.prototype.pause=function(e){return e||(this.paused=!0),this.$element.find(".next, .prev").length&&t.support.transition&&(this.$element.trigger(t.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},i.prototype.next=function(){return this.sliding?void 0:this.slide("next")},i.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},i.prototype.slide=function(e,o){var n=this.$element.find(".item.active"),s=o||this.getItemForDirection(e,n),a=this.interval,r="next"==e?"left":"right",l=this;if(s.hasClass("active"))return this.sliding=!1;var h=s[0],d=t.Event("slide.bs.carousel",{relatedTarget:h,direction:r});if(this.$element.trigger(d),!d.isDefaultPrevented()){if(this.sliding=!0,a&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var p=t(this.$indicators.children()[this.getItemIndex(s)]);p&&p.addClass("active")}var c=t.Event("slid.bs.carousel",{relatedTarget:h,direction:r});return t.support.transition&&this.$element.hasClass("slide")?(s.addClass(e),s[0].offsetWidth,n.addClass(r),s.addClass(r),n.one("bsTransitionEnd",function(){s.removeClass([e,r].join(" ")).addClass("active"),n.removeClass(["active",r].join(" ")),l.sliding=!1,setTimeout(function(){l.$element.trigger(c)},0)}).emulateTransitionEnd(i.TRANSITION_DURATION)):(n.removeClass("active"),s.addClass("active"),this.sliding=!1,this.$element.trigger(c)),a&&this.cycle(),this}};var o=t.fn.carousel;t.fn.carousel=e,t.fn.carousel.Constructor=i,t.fn.carousel.noConflict=function(){return t.fn.carousel=o,this};var n=function(i){var o,n=t(this),s=t(n.attr("data-target")||(o=n.attr("href"))&&o.replace(/.*(?=#[^\s]+$)/,""));if(s.hasClass("carousel")){var a=t.extend({},s.data(),n.data()),r=n.attr("data-slide-to");r&&(a.interval=!1),e.call(s,a),r&&s.data("bs.carousel").to(r),i.preventDefault()}};t(document).on("click.bs.carousel.data-api","[data-slide]",n).on("click.bs.carousel.data-api","[data-slide-to]",n),t(window).on("load",function(){t('[data-ride="carousel"]').each(function(){var i=t(this);e.call(i,i.data())})})}(jQuery),+function(t){"use strict";function e(e){var i=e.attr("data-target");i||(i=e.attr("href"),i=i&&/#[A-Za-z]/.test(i)&&i.replace(/.*(?=#[^\s]*$)/,""));var o=i&&t(i);return o&&o.length?o:e.parent()}function i(i){i&&3===i.which||(t(n).remove(),t(s).each(function(){var o=t(this),n=e(o),s={relatedTarget:this};n.hasClass("open")&&(i&&"click"==i.type&&/input|textarea/i.test(i.target.tagName)&&t.contains(n[0],i.target)||(n.trigger(i=t.Event("hide.bs.dropdown",s)),i.isDefaultPrevented()||(o.attr("aria-expanded","false"),n.removeClass("open").trigger("hidden.bs.dropdown",s))))}))}function o(e){return this.each(function(){var i=t(this),o=i.data("bs.dropdown");o||i.data("bs.dropdown",o=new a(this)),"string"==typeof e&&o[e].call(i)})}var n=".dropdown-backdrop",s='[data-toggle="dropdown"]',a=function(e){t(e).on("click.bs.dropdown",this.toggle)};a.VERSION="3.3.5",a.prototype.toggle=function(o){var n=t(this);if(!n.is(".disabled, :disabled")){var s=e(n),a=s.hasClass("open");if(i(),!a){"ontouchstart"in document.documentElement&&!s.closest(".navbar-nav").length&&t(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(t(this)).on("click",i);var r={relatedTarget:this};if(s.trigger(o=t.Event("show.bs.dropdown",r)),o.isDefaultPrevented())return;n.trigger("focus").attr("aria-expanded","true"),s.toggleClass("open").trigger("shown.bs.dropdown",r)}return!1}},a.prototype.keydown=function(i){if(/(38|40|27|32)/.test(i.which)&&!/input|textarea/i.test(i.target.tagName)){var o=t(this);if(i.preventDefault(),i.stopPropagation(),!o.is(".disabled, :disabled")){var n=e(o),a=n.hasClass("open");if(!a&&27!=i.which||a&&27==i.which)return 27==i.which&&n.find(s).trigger("focus"),o.trigger("click");var r=" li:not(.disabled):visible a",l=n.find(".dropdown-menu"+r);if(l.length){var h=l.index(i.target);38==i.which&&h>0&&h--,40==i.which&&h<l.length-1&&h++,~h||(h=0),l.eq(h).trigger("focus")}}}};var r=t.fn.dropdown;t.fn.dropdown=o,t.fn.dropdown.Constructor=a,t.fn.dropdown.noConflict=function(){return t.fn.dropdown=r,this},t(document).on("click.bs.dropdown.data-api",i).on("click.bs.dropdown.data-api",".dropdown form",function(t){t.stopPropagation()}).on("click.bs.dropdown.data-api",s,a.prototype.toggle).on("keydown.bs.dropdown.data-api",s,a.prototype.keydown).on("keydown.bs.dropdown.data-api",".dropdown-menu",a.prototype.keydown)}(jQuery),+function(t){"use strict";function e(e,o){return this.each(function(){var n=t(this),s=n.data("bs.modal"),a=t.extend({},i.DEFAULTS,n.data(),"object"==typeof e&&e);s||n.data("bs.modal",s=new i(this,a)),"string"==typeof e?s[e](o):a.show&&s.show(o)})}var i=function(e,i){this.options=i,this.$body=t(document.body),this.$element=t(e),this.$dialog=this.$element.find(".modal-dialog"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,t.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};i.VERSION="3.3.5",i.TRANSITION_DURATION=300,i.BACKDROP_TRANSITION_DURATION=150,i.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},i.prototype.toggle=function(t){return this.isShown?this.hide():this.show(t)},i.prototype.show=function(e){var o=this,n=t.Event("show.bs.modal",{relatedTarget:e});this.$element.trigger(n),this.isShown||n.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',t.proxy(this.hide,this)),this.$dialog.on("mousedown.dismiss.bs.modal",function(){o.$element.one("mouseup.dismiss.bs.modal",function(e){t(e.target).is(o.$element)&&(o.ignoreBackdropClick=!0)})}),this.backdrop(function(){var n=t.support.transition&&o.$element.hasClass("fade");o.$element.parent().length||o.$element.appendTo(o.$body),o.$element.show().scrollTop(0),o.adjustDialog(),n&&o.$element[0].offsetWidth,o.$element.addClass("in"),o.enforceFocus();var s=t.Event("shown.bs.modal",{relatedTarget:e});n?o.$dialog.one("bsTransitionEnd",function(){o.$element.trigger("focus").trigger(s)}).emulateTransitionEnd(i.TRANSITION_DURATION):o.$element.trigger("focus").trigger(s)}))},i.prototype.hide=function(e){e&&e.preventDefault(),e=t.Event("hide.bs.modal"),this.$element.trigger(e),this.isShown&&!e.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),t(document).off("focusin.bs.modal"),this.$element.removeClass("in").off("click.dismiss.bs.modal").off("mouseup.dismiss.bs.modal"),this.$dialog.off("mousedown.dismiss.bs.modal"),t.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",t.proxy(this.hideModal,this)).emulateTransitionEnd(i.TRANSITION_DURATION):this.hideModal())},i.prototype.enforceFocus=function(){t(document).off("focusin.bs.modal").on("focusin.bs.modal",t.proxy(function(t){this.$element[0]===t.target||this.$element.has(t.target).length||this.$element.trigger("focus")},this))},i.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",t.proxy(function(t){27==t.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},i.prototype.resize=function(){this.isShown?t(window).on("resize.bs.modal",t.proxy(this.handleUpdate,this)):t(window).off("resize.bs.modal")},i.prototype.hideModal=function(){var t=this;this.$element.hide(),this.backdrop(function(){t.$body.removeClass("modal-open"),t.resetAdjustments(),t.resetScrollbar(),t.$element.trigger("hidden.bs.modal")})},i.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},i.prototype.backdrop=function(e){var o=this,n=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var s=t.support.transition&&n;if(this.$backdrop=t(document.createElement("div")).addClass("modal-backdrop "+n).appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",t.proxy(function(t){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(t.target===t.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),s&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!e)return;s?this.$backdrop.one("bsTransitionEnd",e).emulateTransitionEnd(i.BACKDROP_TRANSITION_DURATION):e()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var a=function(){o.removeBackdrop(),e&&e()};t.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",a).emulateTransitionEnd(i.BACKDROP_TRANSITION_DURATION):a()}else e&&e()},i.prototype.handleUpdate=function(){this.adjustDialog()},i.prototype.adjustDialog=function(){var t=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},i.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},i.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth<t,this.scrollbarWidth=this.measureScrollbar()},i.prototype.setScrollbar=function(){var t=parseInt(this.$body.css("padding-right")||0,10);this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing&&this.$body.css("padding-right",t+this.scrollbarWidth)},i.prototype.resetScrollbar=function(){this.$body.css("padding-right",this.originalBodyPad)},i.prototype.measureScrollbar=function(){var t=document.createElement("div");t.className="modal-scrollbar-measure",this.$body.append(t);var e=t.offsetWidth-t.clientWidth;return this.$body[0].removeChild(t),e};var o=t.fn.modal;t.fn.modal=e,t.fn.modal.Constructor=i,t.fn.modal.noConflict=function(){return t.fn.modal=o,this},t(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(i){var o=t(this),n=o.attr("href"),s=t(o.attr("data-target")||n&&n.replace(/.*(?=#[^\s]+$)/,"")),a=s.data("bs.modal")?"toggle":t.extend({remote:!/#/.test(n)&&n},s.data(),o.data());o.is("a")&&i.preventDefault(),s.one("show.bs.modal",function(t){t.isDefaultPrevented()||s.one("hidden.bs.modal",function(){o.is(":visible")&&o.trigger("focus")})}),e.call(s,a,this)})}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.tooltip"),s="object"==typeof e&&e;(n||!/destroy|hide/.test(e))&&(n||o.data("bs.tooltip",n=new i(this,s)),"string"==typeof e&&n[e]())})}var i=function(t,e){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init("tooltip",t,e)};i.VERSION="3.3.5",i.TRANSITION_DURATION=150,i.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},i.prototype.init=function(e,i,o){if(this.enabled=!0,this.type=e,this.$element=t(i),this.options=this.getOptions(o),this.$viewport=this.options.viewport&&t(t.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var n=this.options.trigger.split(" "),s=n.length;s--;){var a=n[s];if("click"==a)this.$element.on("click."+this.type,this.options.selector,t.proxy(this.toggle,this));else if("manual"!=a){var r="hover"==a?"mouseenter":"focusin",l="hover"==a?"mouseleave":"focusout";this.$element.on(r+"."+this.type,this.options.selector,t.proxy(this.enter,this)),this.$element.on(l+"."+this.type,this.options.selector,t.proxy(this.leave,this))}}this.options.selector?this._options=t.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},i.prototype.getDefaults=function(){return i.DEFAULTS},i.prototype.getOptions=function(e){return e=t.extend({},this.getDefaults(),this.$element.data(),e),e.delay&&"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),e},i.prototype.getDelegateOptions=function(){var e={},i=this.getDefaults();return this._options&&t.each(this._options,function(t,o){i[t]!=o&&(e[t]=o)}),e},i.prototype.enter=function(e){var i=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i)),e instanceof t.Event&&(i.inState["focusin"==e.type?"focus":"hover"]=!0),i.tip().hasClass("in")||"in"==i.hoverState?void(i.hoverState="in"):(clearTimeout(i.timeout),i.hoverState="in",i.options.delay&&i.options.delay.show?void(i.timeout=setTimeout(function(){"in"==i.hoverState&&i.show()},i.options.delay.show)):i.show())},i.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},i.prototype.leave=function(e){var i=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i)),e instanceof t.Event&&(i.inState["focusout"==e.type?"focus":"hover"]=!1),i.isInStateTrue()?void 0:(clearTimeout(i.timeout),i.hoverState="out",i.options.delay&&i.options.delay.hide?void(i.timeout=setTimeout(function(){"out"==i.hoverState&&i.hide()},i.options.delay.hide)):i.hide())},i.prototype.show=function(){var e=t.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(e);var o=t.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(e.isDefaultPrevented()||!o)return;var n=this,s=this.tip(),a=this.getUID(this.type);this.setContent(),s.attr("id",a),this.$element.attr("aria-describedby",a),this.options.animation&&s.addClass("fade");var r="function"==typeof this.options.placement?this.options.placement.call(this,s[0],this.$element[0]):this.options.placement,l=/\s?auto?\s?/i,h=l.test(r);h&&(r=r.replace(l,"")||"top"),s.detach().css({top:0,left:0,display:"block"}).addClass(r).data("bs."+this.type,this),this.options.container?s.appendTo(this.options.container):s.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var d=this.getPosition(),p=s[0].offsetWidth,c=s[0].offsetHeight;if(h){var f=r,u=this.getPosition(this.$viewport);r="bottom"==r&&d.bottom+c>u.bottom?"top":"top"==r&&d.top-c<u.top?"bottom":"right"==r&&d.right+p>u.width?"left":"left"==r&&d.left-p<u.left?"right":r,s.removeClass(f).addClass(r)}var g=this.getCalculatedOffset(r,d,p,c);this.applyPlacement(g,r);var m=function(){var t=n.hoverState;n.$element.trigger("shown.bs."+n.type),n.hoverState=null,"out"==t&&n.leave(n)};t.support.transition&&this.$tip.hasClass("fade")?s.one("bsTransitionEnd",m).emulateTransitionEnd(i.TRANSITION_DURATION):m()}},i.prototype.applyPlacement=function(e,i){var o=this.tip(),n=o[0].offsetWidth,s=o[0].offsetHeight,a=parseInt(o.css("margin-top"),10),r=parseInt(o.css("margin-left"),10);isNaN(a)&&(a=0),isNaN(r)&&(r=0),e.top+=a,e.left+=r,t.offset.setOffset(o[0],t.extend({using:function(t){o.css({top:Math.round(t.top),left:Math.round(t.left)})}},e),0),o.addClass("in");var l=o[0].offsetWidth,h=o[0].offsetHeight;"top"==i&&h!=s&&(e.top=e.top+s-h);var d=this.getViewportAdjustedDelta(i,e,l,h);d.left?e.left+=d.left:e.top+=d.top;var p=/top|bottom/.test(i),c=p?2*d.left-n+l:2*d.top-s+h,f=p?"offsetWidth":"offsetHeight";o.offset(e),this.replaceArrow(c,o[0][f],p)},i.prototype.replaceArrow=function(t,e,i){this.arrow().css(i?"left":"top",50*(1-t/e)+"%").css(i?"top":"left","")},i.prototype.setContent=function(){var t=this.tip(),e=this.getTitle();t.find(".tooltip-inner")[this.options.html?"html":"text"](e),t.removeClass("fade in top bottom left right")},i.prototype.hide=function(e){function o(){"in"!=n.hoverState&&s.detach(),n.$element.removeAttr("aria-describedby").trigger("hidden.bs."+n.type),e&&e()}var n=this,s=t(this.$tip),a=t.Event("hide.bs."+this.type);return this.$element.trigger(a),a.isDefaultPrevented()?void 0:(s.removeClass("in"),t.support.transition&&s.hasClass("fade")?s.one("bsTransitionEnd",o).emulateTransitionEnd(i.TRANSITION_DURATION):o(),this.hoverState=null,this)},i.prototype.fixTitle=function(){var t=this.$element;(t.attr("title")||"string"!=typeof t.attr("data-original-title"))&&t.attr("data-original-title",t.attr("title")||"").attr("title","")},i.prototype.hasContent=function(){return this.getTitle()},i.prototype.getPosition=function(e){e=e||this.$element;var i=e[0],o="BODY"==i.tagName,n=i.getBoundingClientRect();null==n.width&&(n=t.extend({},n,{width:n.right-n.left,height:n.bottom-n.top}));var s=o?{top:0,left:0}:e.offset(),a={scroll:o?document.documentElement.scrollTop||document.body.scrollTop:e.scrollTop()},r=o?{width:t(window).width(),height:t(window).height()}:null;return t.extend({},n,a,r,s)},i.prototype.getCalculatedOffset=function(t,e,i,o){return"bottom"==t?{top:e.top+e.height,left:e.left+e.width/2-i/2}:"top"==t?{top:e.top-o,left:e.left+e.width/2-i/2}:"left"==t?{top:e.top+e.height/2-o/2,left:e.left-i}:{top:e.top+e.height/2-o/2,left:e.left+e.width}},i.prototype.getViewportAdjustedDelta=function(t,e,i,o){var n={top:0,left:0};if(!this.$viewport)return n;var s=this.options.viewport&&this.options.viewport.padding||0,a=this.getPosition(this.$viewport);if(/right|left/.test(t)){var r=e.top-s-a.scroll,l=e.top+s-a.scroll+o;r<a.top?n.top=a.top-r:l>a.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;h<a.left?n.left=a.left-h:d>a.right&&(n.left=a.left+a.width-d)}return n},i.prototype.getTitle=function(){var t,e=this.$element,i=this.options;return t=e.attr("data-original-title")||("function"==typeof i.title?i.title.call(e[0]):i.title)},i.prototype.getUID=function(t){do t+=~~(1e6*Math.random());while(document.getElementById(t));return t},i.prototype.tip=function(){if(!this.$tip&&(this.$tip=t(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},i.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},i.prototype.enable=function(){this.enabled=!0},i.prototype.disable=function(){this.enabled=!1},i.prototype.toggleEnabled=function(){this.enabled=!this.enabled},i.prototype.toggle=function(e){var i=this;e&&(i=t(e.currentTarget).data("bs."+this.type),i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i))),e?(i.inState.click=!i.inState.click,i.isInStateTrue()?i.enter(i):i.leave(i)):i.tip().hasClass("in")?i.leave(i):i.enter(i)},i.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null})};var o=t.fn.tooltip;t.fn.tooltip=e,t.fn.tooltip.Constructor=i,t.fn.tooltip.noConflict=function(){return t.fn.tooltip=o,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.popover"),s="object"==typeof e&&e;(n||!/destroy|hide/.test(e))&&(n||o.data("bs.popover",n=new i(this,s)),"string"==typeof e&&n[e]())})}var i=function(t,e){this.init("popover",t,e)};if(!t.fn.tooltip)throw new Error("Popover requires tooltip.js");i.VERSION="3.3.5",i.DEFAULTS=t.extend({},t.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),i.prototype=t.extend({},t.fn.tooltip.Constructor.prototype),i.prototype.constructor=i,i.prototype.getDefaults=function(){return i.DEFAULTS},i.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();t.find(".popover-title")[this.options.html?"html":"text"](e),t.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof i?"html":"append":"text"](i),t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},i.prototype.hasContent=function(){return this.getTitle()||this.getContent()},i.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},i.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var o=t.fn.popover;t.fn.popover=e,t.fn.popover.Constructor=i,t.fn.popover.noConflict=function(){return t.fn.popover=o,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.tab");n||o.data("bs.tab",n=new i(this)),"string"==typeof e&&n[e]()})}var i=function(e){this.element=t(e)};i.VERSION="3.3.5",i.TRANSITION_DURATION=150,i.prototype.show=function(){var e=this.element,i=e.closest("ul:not(.dropdown-menu)"),o=e.data("target");if(o||(o=e.attr("href"),o=o&&o.replace(/.*(?=#[^\s]*$)/,"")),!e.parent("li").hasClass("active")){var n=i.find(".active:last a"),s=t.Event("hide.bs.tab",{relatedTarget:e[0]}),a=t.Event("show.bs.tab",{relatedTarget:n[0]});if(n.trigger(s),e.trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){var r=t(o);this.activate(e.closest("li"),i),this.activate(r,r.parent(),function(){n.trigger({type:"hidden.bs.tab",relatedTarget:e[0]}),e.trigger({type:"shown.bs.tab",relatedTarget:n[0]})})}}},i.prototype.activate=function(e,o,n){function s(){a.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),e.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),r?(e[0].offsetWidth,e.addClass("in")):e.removeClass("fade"),e.parent(".dropdown-menu").length&&e.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),n&&n()}var a=o.find("> .active"),r=n&&t.support.transition&&(a.length&&a.hasClass("fade")||!!o.find("> .fade").length);a.length&&r?a.one("bsTransitionEnd",s).emulateTransitionEnd(i.TRANSITION_DURATION):s(),a.removeClass("in")};var o=t.fn.tab;t.fn.tab=e,t.fn.tab.Constructor=i,t.fn.tab.noConflict=function(){return t.fn.tab=o,this};var n=function(i){i.preventDefault(),e.call(t(this),"show")};t(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',n).on("click.bs.tab.data-api",'[data-toggle="pill"]',n)}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),n=o.data("bs.affix"),s="object"==typeof e&&e;n||o.data("bs.affix",n=new i(this,s)),"string"==typeof e&&n[e]()})}var i=function(e,o){this.options=t.extend({},i.DEFAULTS,o),this.$target=t(this.options.target).on("scroll.bs.affix.data-api",t.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",t.proxy(this.checkPositionWithEventLoop,this)),this.$element=t(e),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};i.VERSION="3.3.5",i.RESET="affix affix-top affix-bottom",i.DEFAULTS={offset:0,target:window},i.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return i>n?"top":!1;if("bottom"==this.affixed)return null!=i?n+this.unpin<=s.top?!1:"bottom":t-o>=n+a?!1:"bottom";var r=null==this.affixed,l=r?n:s.top,h=r?a:e;return null!=i&&i>=n?"top":null!=o&&l+h>=t-o?"bottom":!1},i.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(i.RESET).addClass("affix");var t=this.$target.scrollTop(),e=this.$element.offset();return this.pinnedOffset=e.top-t},i.prototype.checkPositionWithEventLoop=function(){setTimeout(t.proxy(this.checkPosition,this),1)},i.prototype.checkPosition=function(){if(this.$element.is(":visible")){var e=this.$element.height(),o=this.options.offset,n=o.top,s=o.bottom,a=Math.max(t(document).height(),t(document.body).height());"object"!=typeof o&&(s=n=o),"function"==typeof n&&(n=o.top(this.$element)),"function"==typeof s&&(s=o.bottom(this.$element));var r=this.getState(a,e,n,s);if(this.affixed!=r){null!=this.unpin&&this.$element.css("top","");var l="affix"+(r?"-"+r:""),h=t.Event(l+".bs.affix");if(this.$element.trigger(h),h.isDefaultPrevented())return;this.affixed=r,this.unpin="bottom"==r?this.getPinnedOffset():null,this.$element.removeClass(i.RESET).addClass(l).trigger(l.replace("affix","affixed")+".bs.affix")}"bottom"==r&&this.$element.offset({top:a-e-s})}};var o=t.fn.affix;t.fn.affix=e,t.fn.affix.Constructor=i,t.fn.affix.noConflict=function(){return t.fn.affix=o,this},t(window).on("load",function(){t('[data-spy="affix"]').each(function(){var i=t(this),o=i.data();o.offset=o.offset||{},null!=o.offsetBottom&&(o.offset.bottom=o.offsetBottom),null!=o.offsetTop&&(o.offset.top=o.offsetTop),e.call(i,o)})})}(jQuery),+function(t){"use strict";function e(e){var i,o=e.attr("data-target")||(i=e.attr("href"))&&i.replace(/.*(?=#[^\s]+$)/,"");return t(o)}function i(e){return this.each(function(){var i=t(this),n=i.data("bs.collapse"),s=t.extend({},o.DEFAULTS,i.data(),"object"==typeof e&&e);!n&&s.toggle&&/show|hide/.test(e)&&(s.toggle=!1),n||i.data("bs.collapse",n=new o(this,s)),"string"==typeof e&&n[e]()})}var o=function(e,i){this.$element=t(e),this.options=t.extend({},o.DEFAULTS,i),this.$trigger=t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};o.VERSION="3.3.5",o.TRANSITION_DURATION=350,o.DEFAULTS={toggle:!0},o.prototype.dimension=function(){var t=this.$element.hasClass("width");return t?"width":"height"},o.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var e,n=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(n&&n.length&&(e=n.data("bs.collapse"),e&&e.transitioning))){var s=t.Event("show.bs.collapse");if(this.$element.trigger(s),!s.isDefaultPrevented()){n&&n.length&&(i.call(n,"hide"),e||n.data("bs.collapse",null));var a=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[a](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var r=function(){this.$element.removeClass("collapsing").addClass("collapse in")[a](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!t.support.transition)return r.call(this);var l=t.camelCase(["scroll",a].join("-"));this.$element.one("bsTransitionEnd",t.proxy(r,this)).emulateTransitionEnd(o.TRANSITION_DURATION)[a](this.$element[0][l]);
+}}}},o.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var e=t.Event("hide.bs.collapse");if(this.$element.trigger(e),!e.isDefaultPrevented()){var i=this.dimension();this.$element[i](this.$element[i]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var n=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return t.support.transition?void this.$element[i](0).one("bsTransitionEnd",t.proxy(n,this)).emulateTransitionEnd(o.TRANSITION_DURATION):n.call(this)}}},o.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},o.prototype.getParent=function(){return t(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(t.proxy(function(i,o){var n=t(o);this.addAriaAndCollapsedClass(e(n),n)},this)).end()},o.prototype.addAriaAndCollapsedClass=function(t,e){var i=t.hasClass("in");t.attr("aria-expanded",i),e.toggleClass("collapsed",!i).attr("aria-expanded",i)};var n=t.fn.collapse;t.fn.collapse=i,t.fn.collapse.Constructor=o,t.fn.collapse.noConflict=function(){return t.fn.collapse=n,this},t(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(o){var n=t(this);n.attr("data-target")||o.preventDefault();var s=e(n),a=s.data("bs.collapse"),r=a?"toggle":n.data();i.call(s,r)})}(jQuery),+function(t){"use strict";function e(i,o){this.$body=t(document.body),this.$scrollElement=t(t(i).is(document.body)?window:i),this.options=t.extend({},e.DEFAULTS,o),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",t.proxy(this.process,this)),this.refresh(),this.process()}function i(i){return this.each(function(){var o=t(this),n=o.data("bs.scrollspy"),s="object"==typeof i&&i;n||o.data("bs.scrollspy",n=new e(this,s)),"string"==typeof i&&n[i]()})}e.VERSION="3.3.5",e.DEFAULTS={offset:10},e.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},e.prototype.refresh=function(){var e=this,i="offset",o=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),t.isWindow(this.$scrollElement[0])||(i="position",o=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var e=t(this),n=e.data("target")||e.attr("href"),s=/^#./.test(n)&&t(n);return s&&s.length&&s.is(":visible")&&[[s[i]().top+o,n]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){e.offsets.push(this[0]),e.targets.push(this[1])})},e.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),e>=o)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e<n[0])return this.activeTarget=null,this.clear();for(t=n.length;t--;)a!=s[t]&&e>=n[t]&&(void 0===n[t+1]||e<n[t+1])&&this.activate(s[t])},e.prototype.activate=function(e){this.activeTarget=e,this.clear();var i=this.selector+'[data-target="'+e+'"],'+this.selector+'[href="'+e+'"]',o=t(i).parents("li").addClass("active");o.parent(".dropdown-menu").length&&(o=o.closest("li.dropdown").addClass("active")),o.trigger("activate.bs.scrollspy")},e.prototype.clear=function(){t(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var o=t.fn.scrollspy;t.fn.scrollspy=i,t.fn.scrollspy.Constructor=e,t.fn.scrollspy.noConflict=function(){return t.fn.scrollspy=o,this},t(window).on("load.bs.scrollspy.data-api",function(){t('[data-spy="scroll"]').each(function(){var e=t(this);i.call(e,e.data())})})}(jQuery),+function(t){"use strict";function e(){var t=document.createElement("bootstrap"),e={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var i in e)if(void 0!==t.style[i])return{end:e[i]};return!1}t.fn.emulateTransitionEnd=function(e){var i=!1,o=this;t(this).one("bsTransitionEnd",function(){i=!0});var n=function(){i||t(o).trigger(t.support.transition.end)};return setTimeout(n,e),this},t(function(){t.support.transition=e(),t.support.transition&&(t.event.special.bsTransitionEnd={bindType:t.support.transition.end,delegateType:t.support.transition.end,handle:function(e){return t(e.target).is(this)?e.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery);
\ No newline at end of file
diff --git a/static/static/js/expert.js b/static/static/js/expert.js
new file mode 100644 (file)
index 0000000..a4d14ac
--- /dev/null
@@ -0,0 +1,32 @@
+(function() {
+       function showExpert(isExpert)
+       {
+         var elements = document.getElementsByClassName("expert");
+         for(var i = 0; elements.length > i; i++)
+         {
+           if(!isExpert) {
+               elements[i].setAttribute("class","expert experthidden");
+           } else {
+               elements[i].setAttribute("class","expert");
+           }
+         }
+       }
+       function init(){
+               showExpert(false);
+               var expert = document.getElementById("expertbox");
+               if(expert !== null) {
+                       expert.onchange = (function(expert){return function(){showExpert(expert.checked)}})(expert);
+               }
+       }
+       (function(oldLoad) {
+               if (oldLoad == undefined) {
+                       window.onload = init;
+               } else {
+                       window.onload = function() {
+                               init();
+                               oldLoad();
+                       }
+               }
+       })(window.onload);
+
+})();
diff --git a/static/static/js/jquery.min.js b/static/static/js/jquery.min.js
new file mode 100644 (file)
index 0000000..49990d6
--- /dev/null
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\f]' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function qa(){}qa.prototype=d.filters=d.pseudos,d.setFilters=new qa,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function ra(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){
+return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var aa=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ia={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qa[0].contentDocument,b.write(),b.close(),c=sa(a,b),qa.detach()),ra[a]=c),c}var ua=/^margin/,va=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wa=function(b){return b.ownerDocument.defaultView.opener?b.ownerDocument.defaultView.getComputedStyle(b,null):a.getComputedStyle(b,null)};function xa(a,b,c){var d,e,f,g,h=a.style;return c=c||wa(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),va.test(g)&&ua.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function ya(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),f.removeChild(c),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var za=/^(none|table(?!-c[ea]).+)/,Aa=new RegExp("^("+Q+")(.*)$","i"),Ba=new RegExp("^([+-])=("+Q+")","i"),Ca={position:"absolute",visibility:"hidden",display:"block"},Da={letterSpacing:"0",fontWeight:"400"},Ea=["Webkit","O","Moz","ms"];function Fa(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Ea.length;while(e--)if(b=Ea[e]+c,b in a)return b;return d}function Ga(a,b,c){var d=Aa.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Ha(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ia(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wa(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xa(a,b,f),(0>e||null==e)&&(e=a.style[b]),va.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Ha(a,b,c||(g?"border":"content"),d,f)+"px"}function Ja(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",ta(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xa(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fa(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Ba.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fa(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xa(a,b,d)),"normal"===e&&b in Da&&(e=Da[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?za.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Ca,function(){return Ia(a,b,d)}):Ia(a,b,d):void 0},set:function(a,c,d){var e=d&&wa(a);return Ga(a,c,d?Ha(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=ya(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xa,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ua.test(a)||(n.cssHooks[a+b].set=Ga)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wa(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Ja(this,!0)},hide:function(){return Ja(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Ka(a,b,c,d,e){return new Ka.prototype.init(a,b,c,d,e)}n.Tween=Ka,Ka.prototype={constructor:Ka,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Ka.propHooks[this.prop];return a&&a.get?a.get(this):Ka.propHooks._default.get(this)},run:function(a){var b,c=Ka.propHooks[this.prop];return this.options.duration?this.pos=b=n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ka.propHooks._default.set(this),this}},Ka.prototype.init.prototype=Ka.prototype,Ka.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Ka.propHooks.scrollTop=Ka.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Ka.prototype.init,n.fx.step={};var La,Ma,Na=/^(?:toggle|show|hide)$/,Oa=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pa=/queueHooks$/,Qa=[Va],Ra={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Oa.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Oa.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sa(){return setTimeout(function(){La=void 0}),La=n.now()}function Ta(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ua(a,b,c){for(var d,e=(Ra[b]||[]).concat(Ra["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Va(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||ta(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Na.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?ta(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ua(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wa(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xa(a,b,c){var d,e,f=0,g=Qa.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=La||Sa(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:La||Sa(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wa(k,j.opts.specialEasing);g>f;f++)if(d=Qa[f].call(j,a,k,j.opts))return d;return n.map(k,Ua,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xa,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Ra[c]=Ra[c]||[],Ra[c].unshift(b)},prefilter:function(a,b){b?Qa.unshift(a):Qa.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xa(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pa.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Ta(b,!0),a,d,e)}}),n.each({slideDown:Ta("show"),slideUp:Ta("hide"),slideToggle:Ta("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(La=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),La=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Ma||(Ma=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Ma),Ma=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Ya,Za,$a=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Za:Ya)),
+void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Za={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$a[b]||n.find.attr;$a[b]=function(a,b,d){var e,f;return d||(f=$a[b],$a[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$a[b]=f),e}});var _a=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_a.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ab=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ab," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ab," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ab," ").indexOf(b)>=0)return!0;return!1}});var bb=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bb,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cb=n.now(),db=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var eb=/#.*$/,fb=/([?&])_=[^&]*/,gb=/^(.*?):[ \t]*([^\r\n]*)$/gm,hb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,ib=/^(?:GET|HEAD)$/,jb=/^\/\//,kb=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,lb={},mb={},nb="*/".concat("*"),ob=a.location.href,pb=kb.exec(ob.toLowerCase())||[];function qb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function rb(a,b,c,d){var e={},f=a===mb;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function sb(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function tb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function ub(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ob,type:"GET",isLocal:hb.test(pb[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":nb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?sb(sb(a,n.ajaxSettings),b):sb(n.ajaxSettings,a)},ajaxPrefilter:qb(lb),ajaxTransport:qb(mb),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=gb.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||ob)+"").replace(eb,"").replace(jb,pb[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=kb.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===pb[1]&&h[2]===pb[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(pb[3]||("http:"===pb[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),rb(lb,k,b,v),2===t)return v;i=n.event&&k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!ib.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(db.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=fb.test(d)?d.replace(fb,"$1_="+cb++):d+(db.test(d)?"&":"?")+"_="+cb++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+nb+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=rb(mb,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=tb(k,v,f)),u=ub(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var vb=/%20/g,wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&").replace(vb,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Bb=0,Cb={},Db={0:200,1223:204},Eb=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Cb)Cb[a]()}),k.cors=!!Eb&&"withCredentials"in Eb,k.ajax=Eb=!!Eb,n.ajaxTransport(function(a){var b;return k.cors||Eb&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Bb;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Cb[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Db[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Cb[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Fb=[],Gb=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Fb.pop()||n.expando+"_"+cb++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Gb.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Gb.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Gb,"$1"+e):b.jsonp!==!1&&(b.url+=(db.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Fb.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Hb=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Hb)return Hb.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Ib=a.document.documentElement;function Jb(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Jb(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Ib;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Ib})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Jb(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=ya(k.pixelPosition,function(a,c){return c?(c=xa(a,b),va.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Kb=a.jQuery,Lb=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Lb),b&&a.jQuery===n&&(a.jQuery=Kb),n},typeof b===U&&(a.jQuery=a.$=n),n});
diff --git a/static/static/js/localDate.js b/static/static/js/localDate.js
new file mode 100644 (file)
index 0000000..0935810
--- /dev/null
@@ -0,0 +1,29 @@
+(function() {
+       function init(){
+               var elems = document.getElementsByTagName("time");
+               for(var i = 0; i < elems.length; i++){
+                       elems[i].setAttribute("title", elems[i].textContent);
+                       elems[i].removeChild(elems[i].firstChild);
+                       var t = elems[i].getAttribute("datetime");
+                       elems[i].appendChild(document.createTextNode(new Date(t).toLocaleString(undefined, {timeZoneName: "short",
+                               year: "numeric",
+                               month: "2-digit",
+                               day: "2-digit",
+                               hour: "2-digit",
+                               minute: "2-digit",
+                               second: "2-digit"})
+                               ));
+               }
+       }
+       (function(oldLoad) {
+               if (oldLoad == undefined) {
+                       window.onload = init;
+               } else {
+                       window.onload = function() {
+                               init();
+                               oldLoad();
+                       }
+               }
+       })(window.onload);
+
+})();
diff --git a/static/static/keygenIE.js b/static/static/keygenIE.js
new file mode 100644 (file)
index 0000000..4c15b23
--- /dev/null
@@ -0,0 +1,611 @@
+/*
+LibreSSL - CAcert web application
+Copyright (C) 2004-2012  CAcert Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; version 2 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+var CAcert_keygen_IE = function () {
+
+       /// Makes a new DOM text node
+       var textnode = function (text) {
+               return document.createTextNode(text);
+       }
+
+       /// makes a new <p> element
+       var paragraph = function (text) {
+               var paragraph = document.createElement("p");
+               paragraph.appendChild(textnode(text));
+               return paragraph;
+       }
+
+       /// makes a new <pre> elemtent
+       var pre = function (text) {
+               var pre = document.createElement("pre");
+               pre.appendChild(textnode(text));
+               return pre;
+       }
+
+       /// makes a new <option> element
+       var option = function (text, value) {
+               var option = document.createElement("option");
+               if (value !== undefined) {
+                       option.setAttribute("value", value);
+               }
+               option.appendChild(textnode(text));
+               return option;
+       }
+
+       /// Removes all child nodes from the element
+       var removeChildren = function (element) {
+               element.innerHTML = "";
+       }
+
+       /// Show error message to user from exception
+       var showError = function (message, exception) {
+               window.alert(
+                       message +
+                       "\n\nError: " + exception.message +
+                       " (0x" + (0xFFFFFFFF + exception.number + 1).toString(16) +
+                       " / " + exception.number + ")"
+                       );
+       }
+
+       // Get important elements from the DOM
+       var form = document.getElementById("CertReqForm");
+       var securityLevel = document.getElementById("SecurityLevel");
+       var customSettings = document.getElementById("customSettings");
+       var provider = document.getElementById("CspProvider");
+       var algorithm = document.getElementById("algorithm");
+       var algorithmParagraph = document.getElementById("algorithmParagraph");
+       var keySize = document.getElementById("keySize");
+       var keySizeMin = document.getElementById("keySizeMin");
+       var keySizeMax = document.getElementById("keySizeMax");
+       var keySizeStep = document.getElementById("keySizeStep");
+       var genReq = document.getElementById("GenReq");
+       var csr = document.getElementById("CSR");
+       var noActiveX = document.getElementById("noActiveX");
+       var generatingKeyNotice = document.getElementById("generatingKeyNotice");
+       var createRequestErrorChooseAlgorithm = document.getElementById("createRequestErrorChooseAlgorithm");
+       var createRequestErrorConfirmDialogue = document.getElementById("createRequestErrorConfirmDialogue");
+       var createRequestErrorConnectDevice = document.getElementById("createRequestErrorConnectDevice");
+       var createRequestError = document.getElementById("createRequestError");
+       var invalidKeySizeError = document.getElementById("invalidKeySizeError");
+       var unsupportedPlatformError = document.getElementById("unsupportedPlatformError");
+
+       /// Initialise the CertEnroll code (Vista and higher)
+       /// returns false if initialisation fails
+       var initCertEnroll = function () {
+               var factory = null;
+               var providerList = null;
+               var cspStats = null;
+
+               // Try to initialise the ActiveX element. Requires permissions by the user
+               try {
+                       factory = new ActiveXObject("X509Enrollment.CX509EnrollmentWebClassFactory");
+                       if (!factory) {
+                               throw {
+                                       name: "NoObjectError",
+                                       message: "Got null at object creation"
+                                       };
+                       }
+
+                       // also try to create a useless object here so the library gets
+                       // initialised and we don't need to check everytime later
+                       factory.CreateObject("X509Enrollment.CObjectId");
+
+                       form.style.display = "";
+                       noActiveX.style.display = "none";
+               } catch (e) {
+                       return false;
+               }
+
+               /// Get the selected provider
+               var getProvider = function () {
+                       var providerIndex = provider.options[provider.selectedIndex].value;
+                       return providerList.ItemByIndex(providerIndex);
+               }
+
+               /// Get the selected algorithm
+               var getAlgorithm = function () {
+                       var algorithmIndex = algorithm.options[algorithm.selectedIndex].value;
+                       return alg = cspStats.ItemByIndex(algorithmIndex).CspAlgorithm;
+               }
+
+               /// Get the selected key size
+               var getKeySize = function () {
+                       var alg = getAlgorithm();
+
+                       var bits = parseInt(keySize.value, 10);
+                       if (
+                               (bits < alg.MinLength) ||
+                               (bits > alg.MaxLength) ||
+                               (
+                                       alg.IncrementLength &&
+                                       ((bits - alg.MinLength) % alg.IncrementLength !== 0)
+                               )
+                       ) {
+                               return false;
+                       }
+
+                       return bits;
+               }
+
+               /// Fill the key size list
+               var getKeySizeList = function () {
+                       if (!cspStats) {
+                               return false;
+                       }
+
+                       var alg = getAlgorithm();
+
+                       // HTML5 attributes
+                       keySize.setAttribute("min", alg.MinLength);
+                       keySize.setAttribute("max", alg.MaxLength);
+                       keySize.setAttribute("step", alg.IncrementLength);
+                       keySize.setAttribute("value", alg.DefaultLength);
+                       keySize.value = ""+alg.DefaultLength;
+
+                       // ugly, but buggy otherwise if done with text nodes
+                       keySizeMin.innerHTML = alg.MinLength;
+                       keySizeMax.innerHTML = alg.MaxLength;
+                       keySizeStep.innerHTML = alg.IncrementLength;
+
+                       return true;
+               }
+
+               /// Fill the algorithm list
+               var getAlgorithmList = function () {
+                       var i;
+                       
+                       if (!providerList) {
+                               return false;
+                       }
+
+                       var csp = getProvider();
+
+                       cspStats = providerList.GetCspStatusesFromOperations(
+                               0x1c, //XCN_NCRYPT_ANY_ASYMMETRIC_OPERATION
+                               //0x10, //XCN_NCRYPT_SIGNATURE_OPERATION
+                               //0x8, //XCN_NCRYPT_SECRET_AGREEMENT_OPERATION
+                               //0x4, //XCN_NCRYPT_ASYMMETRIC_ENCRYPTION_OPERATION
+                               csp
+                               );
+
+                       removeChildren(algorithm);
+                       for (i = 0; i < cspStats.Count; i++) {
+                               var alg = cspStats.ItemByIndex(i).CspAlgorithm;
+                               algorithm.appendChild(option(alg.Name, i));
+                       }
+
+                       return getKeySizeList();
+               }
+
+               /// Fill the crypto provider list
+               var getProviderList = function () {
+                       var i;
+                       
+                       var csps = factory.CreateObject("X509Enrollment.CCspInformations");
+
+                       // Get provider information
+                       csps.AddAvailableCsps();
+
+                       removeChildren(provider);
+
+                       for (i = 0; i < csps.Count; i++) {
+                               var csp = csps.ItemByIndex(i);
+                               provider.appendChild(option(csp.Name, i));
+                       }
+
+                       providerList = csps;
+
+                       return getAlgorithmList();
+               }
+
+               /// Generate a key and create and submit the actual CSR
+               var createCSR = function () {
+                       var providerName, algorithmOid, bits;
+
+                       var level = securityLevel.options[securityLevel.selectedIndex];
+                       if (level.value === "custom") {
+                               providerName = getProvider().Name;
+                               var alg = getAlgorithm();
+                               algorithmOid = alg.GetAlgorithmOid(0, 0)
+                               bits = getKeySize();
+                               if (!bits) {
+                                       window.alert(invalidKeySizeError.innerHTML);
+                                       return false;
+                               }
+                       } else {
+                               providerName = "Microsoft Software Key Storage Provider";
+
+                               algorithmOid = factory.CreateObject("X509Enrollment.CObjectId");
+                               algorithmOid.InitializeFromValue("1.2.840.113549.1.1.1"); // RSA
+                               // "1.2.840.10040.4.1" == DSA
+                               // "1.2.840.10046.2.1" == DH
+
+                               if (level.value === "high") {
+                                       bits = 4096;
+                               } else { // medium
+                                       bits = 2048;
+                               }
+                       }
+
+                       var privateKey = factory.CreateObject("X509Enrollment.CX509PrivateKey");
+                       privateKey.ProviderName = providerName;
+                       privateKey.Algorithm = algorithmOid;
+                       privateKey.Length = bits;
+                       privateKey.KeyUsage = 0xffffff; // XCN_NCRYPT_ALLOW_ALL_USAGES
+                       privateKey.ExportPolicy = 0x1; // XCN_NCRYPT_ALLOW_EXPORT_FLAG
+
+                       var request = factory.CreateObject("X509Enrollment.CX509CertificateRequestPkcs10");
+                       request.InitializeFromPrivateKey(
+                               1, // ContextUser
+                               privateKey,
+                               "" // don't use a template
+                               );
+
+                       var enroll = factory.CreateObject("X509Enrollment.CX509Enrollment");
+                       enroll.InitializeFromRequest(request);
+
+                       generatingKeyNotice.style.display = "";
+
+                       // The request needs to be created after we return so the "please wait"
+                       // message gets rendered
+                       var createCSRHandler = function () {
+                               try {
+                                       csr.value = enroll.CreateRequest(0x1); //XCN_CRYPT_STRING_BASE64
+                                       form.submit();
+                               } catch (e) {
+                                       showError(createRequestErrorChooseAlgorithm.innerHTML, e);
+                               }
+
+                               generatingKeyNotice.style.display = "none";
+                       }
+
+                       window.setTimeout(createCSRHandler, 0);
+
+                       // Always return false, form is submitted by deferred method
+                       return false;
+               }
+
+               /// Call if securityLevel has changed
+               var refreshSecurityLevel = function () {
+                       var level = securityLevel.options[securityLevel.selectedIndex];
+                       if (level.value === "custom") {
+                               getProviderList();
+                               customSettings.style.display = "";
+                       } else {
+                               customSettings.style.display = "none";
+                       }
+               }
+
+               securityLevel.onchange = refreshSecurityLevel;
+               provider.onchange = getAlgorithmList;
+               algorithm.onchange = getKeySizeList;
+               genReq.onclick = createCSR;
+
+               return true;
+       } // end of initCertEnroll()
+
+       /// Initialise Xenroll code (XP and lower)
+       /// returns false if initialisation fails
+       var initXEnroll = function () {
+               cenroll = null;
+
+               providerTypes = Array(
+                                1, //PROV_RSA_FULL
+                                2, //PROV_RSA_SIG
+                                3, //PROV_DSS
+                                4, //PROV_FORTEZZA
+                                5, //PROV_MS_EXCHANGE
+                                6, //PROV_SSL
+                               12, //PROV_RSA_SCHANNEL
+                               13, //PROV_DSS_DH
+                               14, //PROV_EC_ECDSA_SIG
+                               15, //PROV_EC_ECNRA_SIG
+                               16, //PROV_EC_ECDSA_FULL
+                               17, //PROV_EC_ECNRA_FULL
+                               18, //PROV_DH_SCHANNEL
+                               20, //PROV_SPYRUS_LYNKS
+                               21, //PROV_RNG
+                               22, //PROV_INTEL_SEC
+                               23, //PROV_REPLACE_OWF
+                               24  //PROV_RSA_AES
+                       );
+
+               algClasses = Array(
+                       1 << 13, //ALG_CLASS_SIGNATURE
+                       //2 << 13, //ALG_CLASS_MSG_ENCRYPT
+                       //3 << 13, //ALG_CLASS_DATA_ENCRYPT
+                       //4 << 13, //ALG_CLASS_HASH
+                       5 << 13  //ALG_CLASS_KEY_EXCHANGE
+                       );
+
+               // Try to initialise the ActiveX element.
+               try {
+                       cenroll = new ActiveXObject("CEnroll.CEnroll");
+
+                       if (!cenroll) {
+                               throw {
+                                       name: "NoObjectError",
+                                       message: "Got null at object creation"
+                               };
+                       }
+
+                       form.style.display = "";
+                       algorithm.disabled = true;
+                       noActiveX.style.display = "none";
+               } catch (e) {
+                       return false;
+               }
+
+               /// Get the name of the selected provider
+               var getProviderName = function () {
+                       return provider.options[provider.selectedIndex].text;
+               }
+
+               /// Get the type of the selected provider
+               var getProviderType = function () {
+                       return parseInt(provider.options[provider.selectedIndex].value, 10);
+               }
+
+               var refreshProvider = function () {
+                       cenroll.ProviderName = getProviderName();
+                       cenroll.ProviderType = getProviderType();
+               }
+
+               /// Get the ID of the selected algorithm
+               var getAlgorithmId = function () {
+                       return parseInt(algorithm.options[algorithm.selectedIndex].value, 10);
+               }
+
+               /// Minimum bit length for exchange keys
+               var getMinExKeyLength = function () {
+                       refreshProvider();
+
+                       try {
+                               return cenroll.GetKeyLen(true, true);
+                       } catch (e) {
+                               return false;
+                       }
+               }
+
+               /// Maximum bit length for exchange keys
+               var getMaxExKeyLength = function () {
+                       refreshProvider();
+
+                       try {
+                               return cenroll.GetKeyLen(false, true);
+                       } catch (e) {
+                               return false;
+                       }
+               }
+
+               /// Step size for exchange keys
+               /// This might not be available on older platforms
+               var getStepExKeyLength = function () {
+                       refreshProvider();
+
+                       try {
+                               return cenroll.GetKeyLenEx(3, 1);
+                       } catch (e) {
+                               return false;
+                       }
+               }
+
+               /// Minimum bit length for signature keys
+               var getMinSigKeyLength = function () {
+                       refreshProvider();
+
+                       try {
+                               return cenroll.GetKeyLen(true, false);
+                       } catch (e) {
+                               return false;
+                       }
+               }
+
+               /// Maximum bit length for signature keys
+               var getMaxSigKeyLength = function () {
+                       refreshProvider();
+
+                       try {
+                               return cenroll.GetKeyLen(false, false);
+                       } catch (e) {
+                               return false;
+                       }
+               }
+
+               /// Step size for signature keys
+               /// This might not be available on older platforms
+               var getStepSigKeyLength = function () {
+                       refreshProvider();
+
+                       try {
+                               return cenroll.GetKeyLenEx(3, 2);
+                       } catch (e) {
+                               return false;
+                       }
+               }
+
+               /// Get the selected key size
+               var getKeySize = function () {
+                       var bits = parseInt(keySize.value, 10);
+                       if (
+                               (bits < getMinSigKeyLength()) ||
+                               (bits > getMaxSigKeyLength()) ||
+                               (
+                                       getStepSigKeyLength() &&
+                                       ((bits - getMinSigKeyLength()) % getStepSigKeyLength() !== 0)
+                               )
+                       ) {
+                               return false;
+                       }
+
+                       return bits;
+               }
+
+               var getKeySizeLimits = function () {
+                       // HTML5 attributes
+                       keySize.setAttribute("min", getMinSigKeyLength());
+                       keySize.setAttribute("max", getMaxSigKeyLength());
+                       if (getStepSigKeyLength()) {
+                               keySize.setAttribute("step", getStepSigKeyLength());
+                       }
+
+                       // ugly, but buggy otherwise if done with text nodes
+                       keySizeMin.innerHTML = getMinSigKeyLength();
+                       keySizeMax.innerHTML = getMaxSigKeyLength();
+                       keySizeStep.innerHTML = getStepSigKeyLength();
+
+                       if (getMinSigKeyLength() === getMaxSigKeyLength()) {
+                               keySize.value = getMaxSigKeyLength();
+                       }
+
+                       return true;
+               }
+
+               /// Fill the algorithm selection box
+               var getAlgorithmList = function () {
+                       var i, j;
+                       
+                       refreshProvider();
+
+                       removeChildren(algorithm);
+
+                       for (i = 0; i < algClasses.length; ++i) {
+                               for (j = 0; true; ++j) {
+                                       try {
+                                               var algId = cenroll.EnumAlgs(j, algClasses[i]);
+                                               var algName = cenroll.GetAlgName(algId);
+                                               algorithm.appendChild(option(algName, algId));
+                                       } catch (e) {
+                                               break;
+                                       }
+                               }
+                       }
+
+                       getKeySizeLimits();
+               }
+
+               /// Fill the provider selection box
+               var getProviderList = function () {
+                       var i, j;
+                       
+                       removeChildren(provider);
+
+                       for (i = 0; i < providerTypes.length; ++i) {
+                               cenroll.providerType = providerTypes[i];
+
+                               var providerName = "invalid";
+                               for (j = 0; true; ++j) {
+                                       try {
+                                               providerName = cenroll.enumProviders(j, 0);
+                                               provider.appendChild(option(providerName, providerTypes[i]));
+                                       } catch (e) {
+                                               break;
+                                       }
+                               }
+                       }
+
+                       return getAlgorithmList();
+               }
+
+               var createCSR = function () {
+                       var providerName, bits;
+
+                       var level = securityLevel.options[securityLevel.selectedIndex];
+                       if (level.value === "custom") {
+                               refreshProvider();
+
+                               bits = getKeySize();
+                               if (bits === false) {
+                                       window.alert(invalidKeySizeError.innerHTML);
+                                       return false;
+                               }
+                       } else {
+                               cenroll.ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0";
+                               cenroll.ProviderType = 1; //PROV_RSA_FULL
+
+                               if (level.value === "high") {
+                                       bits = 4096;
+                               } else { // medium
+                                       bits = 2048;
+                               }
+                       }
+
+                       cenroll.GenKeyFlags = bits << 16; // keysize is encoded in the uper 16 bits
+                       // Allow exporting the private key
+                       cenroll.GenKeyFlags = cenroll.GenKeyFlags | 0x1; //CRYPT_EXPORTABLE
+
+                       generatingKeyNotice.style.display = "";
+
+                       // The request needs to be created after we return so the "please wait"
+                       // message gets rendered
+                       var createCSRHandler = function () {
+                               try {
+                                       csr.value = cenroll.createPKCS10("", "1.3.6.1.5.5.7.3.2");
+                                       form.submit();
+                               } catch (e) {
+                                       if (e.number === -2147023673) {
+                                               // 0x800704c7 => dialogue declined
+                                               showError(createRequestErrorConfirmDialogue.innerHTML, e);
+                                       } else if (e.number === -2146435043) {
+                                               // 0x8010001d => crypto-device not connected
+                                               showError(createRequestErrorConnectDevice.innerHTML, e);
+                                       } else {
+                                               showError(createRequestError.innerHTML, e);
+                                       }
+                               }
+
+                               generatingKeyNotice.style.display = "none";
+                               cenroll.Reset();
+                       }
+
+                       window.setTimeout(createCSRHandler, 0);
+
+                       // Always return false, form is submitted by deferred method
+                       return false;
+               }
+
+               /// Call if securityLevel has changed
+               var refreshSecurityLevel = function () {
+                       var level = securityLevel.options[securityLevel.selectedIndex];
+                       if (level.value === "custom") {
+                               getProviderList();
+                               customSettings.style.display = "";
+                       } else {
+                               customSettings.style.display = "none";
+                       }
+               }
+
+               securityLevel.onchange = refreshSecurityLevel;
+               provider.onchange = getAlgorithmList;
+               algorithm.onchange = getKeySizeLimits;
+               genReq.onclick = createCSR;
+
+               return true;
+       };
+
+       // Run the init functions until one is successful
+       if (initCertEnroll()) {
+               form.style.display = "";
+               noActiveX.style.display = "none";
+       } else if (initXEnroll()) {
+               form.style.display = "";
+               noActiveX.style.display = "none";
+       } else {
+               window.alert(unsupportedPlatformError.innerHTML);
+       }
+} ();
diff --git a/static/www/policy/TermsOfService.html b/static/www/policy/TermsOfService.html
new file mode 100644 (file)
index 0000000..26ddd52
--- /dev/null
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+<head><title>Terms of Service</title></head>
+<body>
+<h1>Terms of Service</h1>
+
+</body>
+</html>
diff --git a/templates/base.html b/templates/base.html
deleted file mode 100644 (file)
index 7b0eafa..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<title>$title$</title>
-<link rel="stylesheet" href="/static/default.css" type="text/css">
-<script src="/static/menu.js"></script>
-</head>
-<body>
-       <div id="pagecell1">
-               <div id="pageName">
-                       <br>
-                       <div id="pageLogo">
-                               <a href="/"><img src="https://www.cacert.org/images/cacert4.png"
-                                       alt="CAcert.org logo"></a>
-                       </div>
-                       <div id="googlead">
-                               <h2>Kostenlose Digitale Zertifikate!</h2>
-                       </div>
-               </div>
-               <div id="pageNav">
-                       <div>
-                               <h3>CAcert.org beitreten</h3>
-                               <a href="/register">Beitreten</a> <a
-                                       href="/policy/CAcertCommunityAgreement.php">Vereinbarung der
-                                       Gemeinschaft (Community Agreement)</a> <a href="/index.php?id=3">Stammzertifikat</a>
-                       </div>
-                       <div>
-                               <h3 class="pointer">Mein Konto</h3>
-                               <a href="/login">Anmelden mit
-                                       Kennwort</a> <a href="">Kennwort
-                                       vergessen</a> <a
-                                       href="https://www.cacert.org/index.php?id=4&amp;noauto=1">Anmelden
-                                       im Net-Cafe</a> <a href="/login">Anmelden
-                                       mit Zertifikat</a>
-                       </div>
-                       <div>
-                               <h3 class="pointer">+ Über CAcert.org</h3>
-                               <ul class="menu hidden" id="misc">
-                                       <li><a href="http://blog.cacert.org/">CAcert-Nachrichten</a></li>
-                                       <li><a href="http://wiki.CAcert.org/">Wiki-Dokumentation</a></li>
-                                       <li><a href="/policy/">Richtlinien</a></li>
-                                       <li><a href="//wiki.cacert.org/FAQ/Privileges">Punkte-System</a></li>
-                                       <li><a href="http://bugs.CAcert.org/">Fehler-Datenbank</a></li>
-                                       <li><a href="/stats.php">CAcert-Statistik</a></li>
-                                       <li><a href="http://blog.CAcert.org/feed/">RSS-Nachrichten-Feed</a></li>
-                                       <li><a href="//wiki.cacert.org/Board">CAcert-Vorstand</a></li>
-                                       <li><a href="https://lists.cacert.org/wws">Mailinglisten</a></li>
-                                       <li><a href="/src-lic.php">Quellcode</a></li>
-                               </ul>
-                       </div>
-
-                       <div>
-                               <h3 class="pointer">+ Übersetzungen</h3>
-                               <ul class="menu hidden" id="trans">
-                                       <li><a href="/index.php?id=0&amp;lang=ar">العربية</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=bg">Български</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=cs">Čeština</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=da">Dansk</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=de">Deutsch</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=el">Ελληνικά</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=en">English</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=es">Español</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=fi">Suomi</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=fr">Français</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=hu">Magyar</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=it">Italiano</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=ja">日本語</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=lv">Latviešu</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=nl">Nederlands</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=pl">Polski</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=pt">Português</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=pt-br">Português
-                                                       Brasileiro</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=ru">Русский</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=sv">Svenska</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=tr">Türkçe</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=zh-cn">中文(简体)</a></li>
-                                       <li><a href="/index.php?id=0&amp;lang=zh-tw">中文(臺灣)</a></li>
-                               </ul>
-                       </div>
-                       <div>
-                               <h3 class="pointer">Werbung</h3>
-                               <ul class="menu hidden" id="recom"></ul>
-                       </div>
-               </div>
-               <div id="content">
-                       <h1>$title$</h1>
-                       <div class="story">$content$</div>
-               </div>
-               <div class="sponsorinfo">
-                       Der CAcert-Betrieb wird gesponsert von
-                       <a href="http://www.bit.nl/" target="_blank">
-                               <img class="sponsorlogo" src="https://www.cacert.org/images/bit.png" alt="[BIT logo]">
-                       </a>
-                       <a href="http://www.tunix.nl/" target="_blank">
-                               <img class="sponsorlogo" src="https://www.cacert.org/images/tunix.png" alt="[TUNIX logo]">
-                       </a>
-                       <a href="http://www.nlnet.nl/" target="_blank">
-                               <img class="sponsorlogo" src="https://www.cacert.org/images/nlnet.png" alt="[NLnet logo]">
-                       </a>
-                       <a href="http://www.openarchitecturenetwork.org/" target="_blank">
-                               <img class="sponsorlogo" src="https://www.cacert.org/images/oan.png" alt="[OAN logo]">
-                       </a>
-               </div>
-
-               <div id="siteInfo">
-                       <a href="//wiki.cacert.org/FAQ/AboutUs">Über uns</a> |
-                       <a href="/index.php?id=13">Spenden</a> |
-                       <a href="http://wiki.cacert.org/wiki/CAcertIncorporated">Vereins-Mitgliedschaft</a> |
-                       <a href="/policy/PrivacyPolicy.html">Datenschutzrichtlinien</a> |
-                       <a href="/index.php?id=51">Unsere Ziele</a> |
-                       <a href="/index.php?id=11">Kontakt</a> |
-                       ©2002-$year$ von CAcert
-               </div>
-       </div>
-</body>
-</html>
\ No newline at end of file
diff --git a/tests/com/lambdaworks/crypto/test/CryptoTestUtil.java b/tests/com/lambdaworks/crypto/test/CryptoTestUtil.java
new file mode 100644 (file)
index 0000000..9b1e7bb
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+public class CryptoTestUtil {
+
+    public static byte[] decode(String str) {
+        byte[] bytes = new byte[str.length() / 2];
+        int index = 0;
+
+        for (int i = 0; i < str.length(); i += 2) {
+            int high = hexValue(str.charAt(i));
+            int low = hexValue(str.charAt(i + 1));
+            bytes[index++] = (byte) ((high << 4) + low);
+        }
+
+        return bytes;
+    }
+
+    public static int hexValue(char c) {
+        return c >= 'a' ? c - 87 : c - 48;
+    }
+}
diff --git a/tests/com/lambdaworks/crypto/test/PBKDFTest.java b/tests/com/lambdaworks/crypto/test/PBKDFTest.java
new file mode 100644 (file)
index 0000000..001a622
--- /dev/null
@@ -0,0 +1,142 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+import com.lambdaworks.crypto.PBKDF;
+import org.junit.Test;
+
+import java.math.BigInteger;
+
+import static org.junit.Assert.*;
+import static com.lambdaworks.crypto.test.CryptoTestUtil.*;
+
+public class PBKDFTest {
+
+    @Test
+    public void pbkdf2_hmac_sha1_rfc6070() throws Exception {
+        String alg = "HmacSHA1";
+        byte[] P, S;
+        int c, dkLen;
+        String DK;
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 1;
+        dkLen = 20;
+        DK = "0c60c80f961f0e71f3a9b524af6012062fe037a6";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 2;
+        dkLen = 20;
+        DK = "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 20;
+        DK = "4b007901b765489abead49d926f721d065a429c1";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 16777216;
+        dkLen = 20;
+        DK = "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "passwordPASSWORDpassword".getBytes("UTF-8");
+        S = "saltSALTsaltSALTsaltSALTsaltSALTsalt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 25;
+        DK = "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "pass\0word".getBytes("UTF-8");
+        S = "sa\0lt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 16;
+        DK = "56fa6aa75548099dcc37d7f03425e0c3";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+    }
+
+    @Test
+    public void pbkdf2_hmac_sha1_rfc3962() throws Exception {
+        String alg = "HmacSHA1";
+        byte[] P, S;
+        int c, dkLen;
+        String DK;
+
+        P = "password".getBytes("UTF-8");
+        S = "ATHENA.MIT.EDUraeburn".getBytes("UTF-8");
+        c = 1;
+
+        dkLen = 16;
+        DK = "cdedb5281bb2f801565a1122b2563515";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "ATHENA.MIT.EDUraeburn".getBytes("UTF-8");
+        c = 2;
+
+        dkLen = 16;
+        DK = "01dbee7f4a9e243e988b62c73cda935d";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = "ATHENA.MIT.EDUraeburn".getBytes("UTF-8");
+        c = 1200;
+
+        dkLen = 16;
+        DK = "5c08eb61fdf71e4e4ec3cf6ba1f5512b";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        P = "password".getBytes("UTF-8");
+        S = new BigInteger("1234567878563412", 16).toByteArray();
+        c = 5;
+
+        dkLen = 16;
+        DK = "d1daa78615f287e6a1c8b120d7062a49";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+
+        dkLen = 32;
+        DK = "d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee";
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+    }
+
+    @Test
+    public void pbkdf2_hmac_sha256_scrypt() throws Exception {
+        String alg = "HmacSHA256";
+        byte[] P, S;
+        int c, dkLen;
+        String DK;
+
+        P = "password".getBytes("UTF-8");
+        S = "salt".getBytes("UTF-8");
+        c = 4096;
+        dkLen = 32;
+        DK = "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a";
+
+        assertArrayEquals(decode(DK), PBKDF.pbkdf2(alg, P, S, c, dkLen));
+    }
+}
diff --git a/tests/com/lambdaworks/crypto/test/SCryptTest.java b/tests/com/lambdaworks/crypto/test/SCryptTest.java
new file mode 100644 (file)
index 0000000..fe2242f
--- /dev/null
@@ -0,0 +1,85 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+import com.lambdaworks.crypto.SCrypt;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static com.lambdaworks.crypto.test.CryptoTestUtil.*;
+import static com.lambdaworks.crypto.SCrypt.*;
+
+public class SCryptTest {
+
+    @Test
+    public void scrypt_paper_appendix_b() throws Exception {
+        byte[] P, S;
+        int N, r, p, dkLen;
+        String DK;
+
+        // empty key & salt test missing because unsupported by JCE
+
+        P = "password".getBytes("UTF-8");
+        S = "NaCl".getBytes("UTF-8");
+        N = 1024;
+        r = 8;
+        p = 16;
+        dkLen = 64;
+        DK = "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640";
+
+        assertArrayEquals(decode(DK), SCrypt.scrypt(P, S, N, r, p, dkLen));
+
+        P = "pleaseletmein".getBytes("UTF-8");
+        S = "SodiumChloride".getBytes("UTF-8");
+        N = 16384;
+        r = 8;
+        p = 1;
+        dkLen = 64;
+        DK = "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887";
+
+        assertArrayEquals(decode(DK), scrypt(P, S, N, r, p, dkLen));
+
+        P = "pleaseletmein".getBytes("UTF-8");
+        S = "SodiumChloride".getBytes("UTF-8");
+        N = 1048576;
+        r = 8;
+        p = 1;
+        dkLen = 64;
+        DK = "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4";
+
+        assertArrayEquals(decode(DK), SCrypt.scrypt(P, S, N, r, p, dkLen));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void scrypt_invalid_N_zero() throws Exception {
+        byte[] P = "pleaseletmein".getBytes("UTF-8");
+        byte[] S = "SodiumChloride".getBytes("UTF-8");
+        scrypt(P, S, 0, 1, 1, 64);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void scrypt_invalid_N_odd() throws Exception {
+        byte[] P = "pleaseletmein".getBytes("UTF-8");
+        byte[] S = "SodiumChloride".getBytes("UTF-8");
+        scrypt(P, S, 3, 1, 1, 64);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void scrypt_invalid_N_large() throws Exception {
+        byte[] P = "pleaseletmein".getBytes("UTF-8");
+        byte[] S = "SodiumChloride".getBytes("UTF-8");
+        int r = 8;
+        int N = Integer.MAX_VALUE / 128;
+        scrypt(P, S, N, r, 1, 64);
+    }
+
+    // @Test(expected = IllegalArgumentException.class)
+    // public void scrypt_invalid_r_large() throws Exception {
+    // byte[] P = "pleaseletmein".getBytes("UTF-8");
+    // byte[] S = "SodiumChloride".getBytes("UTF-8");
+    // int N = 1024;
+    // int r = Integer.MAX_VALUE / 128 + 1;
+    // int p = 0;
+    // scrypt(P, S, N, r, p, 64);
+    // }
+}
diff --git a/tests/com/lambdaworks/crypto/test/SCryptUtilTest.java b/tests/com/lambdaworks/crypto/test/SCryptUtilTest.java
new file mode 100644 (file)
index 0000000..cac9c43
--- /dev/null
@@ -0,0 +1,63 @@
+// Copyright (C) 2011 - Will Glozer. All rights reserved.
+
+package com.lambdaworks.crypto.test;
+
+import java.util.Base64;
+
+import com.lambdaworks.crypto.SCryptUtil;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class SCryptUtilTest {
+
+    String passwd = "secret";
+
+    @Test
+    public void scrypt() {
+        int N = 16384;
+        int r = 8;
+        int p = 1;
+
+        String hashed = SCryptUtil.scrypt(passwd, N, r, p);
+        String[] parts = hashed.split("\\$");
+
+        assertEquals(5, parts.length);
+        assertEquals("s0", parts[1]);
+        Assert.assertEquals(16, Base64.getDecoder().decode(parts[3]).length);
+        assertEquals(32, Base64.getDecoder().decode(parts[4]).length);
+
+        int params = Integer.valueOf(parts[2], 16);
+
+        assertEquals(N, (int) Math.pow(2, params >> 16 & 0xffff));
+        assertEquals(r, params >> 8 & 0xff);
+        assertEquals(p, params >> 0 & 0xff);
+    }
+
+    @Test
+    public void check() {
+        String hashed = SCryptUtil.scrypt(passwd, 16384, 8, 1);
+
+        assertTrue(SCryptUtil.check(passwd, hashed));
+        assertFalse(SCryptUtil.check("s3cr3t", hashed));
+    }
+
+    @Test
+    public void format_0_rp_max() throws Exception {
+        int N = 2;
+        int r = 255;
+        int p = 255;
+
+        String hashed = SCryptUtil.scrypt(passwd, N, r, p);
+        assertTrue(SCryptUtil.check(passwd, hashed));
+
+        String[] parts = hashed.split("\\$");
+        int params = Integer.valueOf(parts[2], 16);
+
+        assertEquals(N, (int) Math.pow(2, params >>> 16 & 0xffff));
+        assertEquals(r, params >> 8 & 0xff);
+        assertEquals(p, params >> 0 & 0xff);
+    }
+}
diff --git a/tests/org/cacert/gigi/DomainVerification.java b/tests/org/cacert/gigi/DomainVerification.java
new file mode 100644 (file)
index 0000000..cd31f8e
--- /dev/null
@@ -0,0 +1,81 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import org.cacert.gigi.dbObjects.Domain;
+import org.junit.Test;
+
+public class DomainVerification {
+
+    @Test
+    public void testDomainPart() {
+        assertTrue(Domain.isVaildDomainPart("cacert", false));
+        assertTrue(Domain.isVaildDomainPart("de", false));
+        assertTrue(Domain.isVaildDomainPart("ha2-a", false));
+        assertTrue(Domain.isVaildDomainPart("ha2--a", false));
+        assertTrue(Domain.isVaildDomainPart("h--a", false));
+        assertFalse(Domain.isVaildDomainPart("xn--bla", false));
+        assertFalse(Domain.isVaildDomainPart("-xnbla", false));
+        assertFalse(Domain.isVaildDomainPart("xnbla-", false));
+        assertFalse(Domain.isVaildDomainPart("", false));
+        assertTrue(Domain.isVaildDomainPart("2xnbla", false));
+        assertTrue(Domain.isVaildDomainPart("xnbla2", false));
+        assertTrue(Domain.isVaildDomainPart("123", false));
+        assertTrue(Domain.isVaildDomainPart("abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy1234567890123", false));
+        assertFalse(Domain.isVaildDomainPart("abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy12345678901234", false));
+    }
+
+    @Test
+    public void testDomainCertifyable() {
+        isCertifyableDomain(true, "cacert.org", false);
+        isCertifyableDomain(true, "cacert.de", false);
+        isCertifyableDomain(true, "cacert.org", false);
+        isCertifyableDomain(true, "cacert.org", false);
+        isCertifyableDomain(true, "1234.org", false);
+        isCertifyableDomain(false, "a.cacert.org", true);
+        isCertifyableDomain(false, "gigi.local", true);
+        isCertifyableDomain(false, "org", true);
+        isCertifyableDomain(false, "'a.org", true);
+        isCertifyableDomain(false, ".org", true);
+        isCertifyableDomain(false, ".org.", true);
+        // non-real-punycode
+        isCertifyableDomain(true, "xna-ae.de", false);
+        isCertifyableDomain(true, "xn-aae.de", false);
+
+        // illegal punycode:
+        // illegal ace prefix
+        isCertifyableDomain(false, "aa--b.com", true);
+        isCertifyableDomain(false, "xm--ae-a.de", true);
+
+        // illegal punycode content
+        isCertifyableDomain(false, "xn--ae-a.com", true);
+        isCertifyableDomain(false, "xn--ae.de", true);
+        isCertifyableDomain(false, "xn--ae-a.org", true);
+        isCertifyableDomain(false, "xn--ae-a.de", true);
+        // valid punycode requires permission
+        isCertifyableDomain(true, "xn--4ca0bs.de", true);
+        isCertifyableDomain(false, "xn--4ca0bs.de", false);
+        isCertifyableDomain(true, "xn--a-zfa9cya.de", true);
+        isCertifyableDomain(false, "xn--a-zfa9cya.de", false);
+
+        // valid punycode does not help under .com
+        isCertifyableDomain(false, "xn--a-zfa9cya.com", true);
+        isCertifyableDomain(true, "zfa9cya.com", true);
+
+        isCertifyableDomain(false, "127.0.0.1", false);
+        isCertifyableDomain(false, "::1", false);
+        isCertifyableDomain(false, "127.0.0.1", true);
+        isCertifyableDomain(false, "::1", true);
+
+    }
+
+    private void isCertifyableDomain(boolean b, String string, boolean puny) {
+        try {
+            Domain.checkCertifyableDomain(string, puny);
+            assertTrue(b);
+        } catch (GigiApiException e) {
+            assertFalse(e.getMessage(), b);
+        }
+    }
+
+}
index 729a5988e8053f4f4cf4e3b26828acec9b801a63..cb575caf78190d3fb97bf33e0326beb242af57b6 100644 (file)
@@ -1,39 +1,61 @@
 package org.cacert.gigi;
 
-import java.io.IOException;
+import static org.hamcrest.CoreMatchers.*;
 import static org.junit.Assert.*;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLEncoder;
 
+import java.io.IOException;
+import java.net.URLConnection;
+
+import org.cacert.gigi.testUtils.IOUtils;
 import org.cacert.gigi.testUtils.ManagedTest;
 import org.junit.Test;
 
 public class LoginTest extends ManagedTest {
-       public static final String secureReference = "/account/certs/email";
-       @Test
-       public void testLoginUnverified() throws IOException {
-               long uniq = System.currentTimeMillis();
-               String email = "system" + uniq + "@testmail.org";
-               String pw = "1'aAaA";
-               registerUser("an", "bn", email, pw);
-               waitForMail();
-               assertFalse(isLoggedin(login(email, pw)));
-       }
-       @Test
-       public void testLoginVerified() throws IOException {
-               long uniq = System.currentTimeMillis();
-               String email = "system2" + uniq + "@testmail.org";
-               String pw = "1'aAaA";
-               createVerifiedUser("an", "bn", email, pw);
-               assertTrue(isLoggedin(login(email, pw)));
-       }
-       public boolean isLoggedin(String cookie) throws IOException {
-               URL u = new URL("https://" + getServerName() + secureReference);
-               HttpURLConnection huc = (HttpURLConnection) u.openConnection();
-               huc.addRequestProperty("Cookie", cookie);
-               return huc.getResponseCode() == 200;
-       }
+
+    @Test
+    public void testLoginUnverified() throws IOException {
+        String email = createUniqueName() + "@testmail.org";
+        registerUser("an", "bn", email, TEST_PASSWORD);
+        getMailReciever().receive();
+        assertFalse(isLoggedin(login(email, TEST_PASSWORD)));
+    }
+
+    @Test
+    public void testLoginVerified() throws IOException {
+        String email = createUniqueName() + "@testmail.org";
+        createVerifiedUser("an", "bn", email, TEST_PASSWORD);
+        assertTrue(isLoggedin(login(email, TEST_PASSWORD)));
+    }
+
+    @Test
+    public void testLoginWrongPassword() throws IOException {
+        String email = createUniqueName() + "@testmail.org";
+        createVerifiedUser("an", "bn", email, TEST_PASSWORD);
+        assertFalse(isLoggedin(login(email, TEST_PASSWORD + "b")));
+    }
+
+    @Test
+    public void testLogoutVerified() throws IOException {
+        String email = createUniqueName() + "@testmail.org";
+        createVerifiedUser("an", "bn", email, TEST_PASSWORD);
+        String cookie = login(email, TEST_PASSWORD);
+        assertTrue(isLoggedin(cookie));
+        logout(cookie);
+        assertFalse(isLoggedin(cookie));
+    }
+
+    private void logout(String cookie) throws IOException {
+        get(cookie, "/logout").getHeaderField("Location");
+    }
+
+    @Test
+    public void testLoginMethodDisplay() throws IOException {
+        String email = createUniqueName() + "@testmail.org";
+        createVerifiedUser("an", "bn", email, TEST_PASSWORD);
+        String l = login(email, TEST_PASSWORD);
+        URLConnection c = get(l, "");
+        String readURL = IOUtils.readURL(c);
+        assertThat(readURL, containsString("Password"));
+    }
 
 }
diff --git a/tests/org/cacert/gigi/TestCalendarUtil.java b/tests/org/cacert/gigi/TestCalendarUtil.java
new file mode 100644 (file)
index 0000000..47ce3b9
--- /dev/null
@@ -0,0 +1,78 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+import org.cacert.gigi.util.CalendarUtil;
+import org.cacert.gigi.util.DayDate;
+import org.junit.Test;
+
+public class TestCalendarUtil {
+
+    @Test
+    public void testGetDateFromComponents() {
+
+        Calendar now = Calendar.getInstance();
+        now.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+        int year = now.get(Calendar.YEAR);
+        int month = now.get(Calendar.MONTH) + 1;
+        int days = now.get(Calendar.DATE);
+        now.setTimeInMillis(0);
+        now.set(year, month - 1, days, 0, 0, 0);
+
+        DayDate dob = CalendarUtil.getDateFromComponents(year, month, days);
+        DayDate d = new DayDate(now.getTimeInMillis());
+
+        assertEquals(d.getTime(), dob.getTime());
+        dob = CalendarUtil.getDateFromComponents(year + 1, month, days);
+
+        assertNotEquals(d.getTime(), dob.getTime());
+
+    }
+
+    @Test
+    public void testIsOfAge() {
+
+        Calendar now = Calendar.getInstance();
+        int year = now.get(Calendar.YEAR);
+        int month = now.get(Calendar.MONTH) + 1;
+        int days = now.get(Calendar.DATE);
+
+        DayDate dob = CalendarUtil.getDateFromComponents(year - 14, month, days);
+
+        assertTrue(CalendarUtil.isOfAge(dob, 13));
+
+        assertTrue(CalendarUtil.isOfAge(dob, 14));
+
+        dob = CalendarUtil.getDateFromComponents(year - 14, month, days + 1);
+        assertFalse(CalendarUtil.isOfAge(dob, 14));
+
+    }
+
+    static {
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    }
+
+    @Test
+    public void testIsDateValid() {
+        assertTrue(CalendarUtil.isDateValid(2016, 2, 28));
+        assertTrue(CalendarUtil.isDateValid(2016, 2, 29));
+        assertFalse(CalendarUtil.isDateValid(2016, 2, 30));
+        assertFalse(CalendarUtil.isDateValid(2016, 4, 31));
+
+        assertTrue(CalendarUtil.isDateValid(2000, 2, 28));
+        assertTrue(CalendarUtil.isDateValid(2000, 2, 29));
+        assertFalse(CalendarUtil.isDateValid(2000, 2, 30));
+        assertFalse(CalendarUtil.isDateValid(2000, 4, 31));
+
+        assertTrue(CalendarUtil.isDateValid(2015, 2, 28));
+        assertFalse(CalendarUtil.isDateValid(2015, 2, 29));
+        assertFalse(CalendarUtil.isDateValid(2015, 2, 30));
+        assertFalse(CalendarUtil.isDateValid(2015, 4, 31));
+
+    }
+
+}
diff --git a/tests/org/cacert/gigi/TestCertificate.java b/tests/org/cacert/gigi/TestCertificate.java
new file mode 100644 (file)
index 0000000..dd14e8d
--- /dev/null
@@ -0,0 +1,150 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.List;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.Certificate.SANType;
+import org.cacert.gigi.dbObjects.Certificate.SubjectAlternateName;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.account.certs.Certificates;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+import sun.security.x509.GeneralNameInterface;
+
+public class TestCertificate extends ManagedTest {
+
+    User u = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "@example.com", TEST_PASSWORD));
+
+    @Test
+    public void testClientCertLoginStates() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException {
+        KeyPair kp = generateKeypair();
+        String key1 = generatePEMCSR(kp, "CN=testmail@example.com");
+        Certificate c = new Certificate(u, u, Certificate.buildDN("CN", "testmail@example.com"), Digest.SHA256, key1, CSRType.CSR, CertificateProfile.getById(1));
+        final PrivateKey pk = kp.getPrivate();
+        c.issue(null, "2y", u).waitFor(60000);
+        final X509Certificate ce = c.cert();
+        assertNotNull(login(pk, ce));
+    }
+
+    @Test
+    public void testSANs() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException {
+        KeyPair kp = generateKeypair();
+        String key = generatePEMCSR(kp, "CN=testmail@example.com");
+        Certificate c = new Certificate(u, u, Certificate.buildDN("CN", "testmail@example.com"), Digest.SHA256, key, CSRType.CSR, CertificateProfile.getById(1),//
+                new SubjectAlternateName(SANType.EMAIL, "testmail@example.com"), new SubjectAlternateName(SANType.DNS, "testmail.example.com"));
+
+        testFails(CertificateStatus.DRAFT, c);
+        c.issue(null, "2y", u).waitFor(60000);
+        X509Certificate cert = c.cert();
+        Collection<List<?>> sans = cert.getSubjectAlternativeNames();
+        assertEquals(2, sans.size());
+        boolean hadDNS = false;
+        boolean hadEmail = false;
+        for (List<?> list : sans) {
+            assertEquals(2, list.size());
+            Integer type = (Integer) list.get(0);
+            switch (type) {
+            case GeneralNameInterface.NAME_RFC822:
+                hadEmail = true;
+                assertEquals("testmail@example.com", list.get(1));
+                break;
+            case GeneralNameInterface.NAME_DNS:
+                hadDNS = true;
+                assertEquals("testmail.example.com", list.get(1));
+                break;
+            default:
+                fail("Unknown type");
+
+            }
+        }
+        assertTrue(hadDNS);
+        assertTrue(hadEmail);
+
+        testFails(CertificateStatus.ISSUED, c);
+
+        Certificate c2 = Certificate.getBySerial(c.getSerial());
+        assertNotNull(c2);
+        assertEquals(2, c2.getSANs().size());
+        assertEquals(c.getSANs().get(0).getName(), c2.getSANs().get(0).getName());
+        assertEquals(c.getSANs().get(0).getType(), c2.getSANs().get(0).getType());
+        assertEquals(c.getSANs().get(1).getName(), c2.getSANs().get(1).getName());
+        assertEquals(c.getSANs().get(1).getType(), c2.getSANs().get(1).getType());
+
+        try {
+            c2.getSANs().remove(0);
+            fail("the list should not be modifiable");
+        } catch (UnsupportedOperationException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testCertLifeCycle() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException {
+        KeyPair kp = generateKeypair();
+        String key = generatePEMCSR(kp, "CN=testmail@example.com");
+        Certificate c = new Certificate(u, u, Certificate.buildDN("CN", "testmail@example.com"), Digest.SHA256, key, CSRType.CSR, CertificateProfile.getById(1));
+        final PrivateKey pk = kp.getPrivate();
+
+        testFails(CertificateStatus.DRAFT, c);
+        c.issue(null, "2y", u).waitFor(60000);
+
+        String cookie = login(u.getEmail(), TEST_PASSWORD);
+        testFails(CertificateStatus.ISSUED, c);
+        X509Certificate cert = c.cert();
+        assertNotNull(login(pk, cert));
+        assertEquals(1, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH)), "<td>(?:REVOKED|ISSUED)</td>"));
+        assertEquals(1, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH + "?withRevoked")), "<td>(?:REVOKED|ISSUED)</td>"));
+        c.revoke().waitFor(60000);
+
+        testFails(CertificateStatus.REVOKED, c);
+        assertNull(login(pk, cert));
+
+        assertEquals(0, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH)), "<td>(?:REVOKED|ISSUED)</td>"));
+        assertEquals(1, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH + "?withRevoked")), "<td>(?:REVOKED|ISSUED)</td>"));
+    }
+
+    private void testFails(CertificateStatus status, Certificate c) throws IOException, GeneralSecurityException, SQLException, GigiApiException {
+        assertEquals(status, c.getStatus());
+        if (status != CertificateStatus.ISSUED) {
+            try {
+                c.revoke();
+                fail(status + " is in invalid state");
+            } catch (IllegalStateException ise) {
+
+            }
+        }
+        if (status != CertificateStatus.DRAFT) {
+            try {
+                c.issue(null, "2y", u);
+                fail(status + " is in invalid state");
+            } catch (IllegalStateException ise) {
+
+            }
+        }
+        if (status != CertificateStatus.ISSUED) {
+            try {
+                c.cert();
+                if (status != CertificateStatus.REVOKED) {
+                    fail(status + " is in invalid state");
+                }
+            } catch (IllegalStateException ise) {
+
+            }
+        }
+    }
+}
diff --git a/tests/org/cacert/gigi/TestCrossDomainAccess.java b/tests/org/cacert/gigi/TestCrossDomainAccess.java
new file mode 100644 (file)
index 0000000..95f2380
--- /dev/null
@@ -0,0 +1,77 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.sql.SQLException;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.cacert.gigi.util.ServerConstants;
+import org.junit.Test;
+
+public class TestCrossDomainAccess extends ManagedTest {
+
+    @Test
+    public void testNoOriginHeader() throws MalformedURLException, IOException {
+        URLConnection con = new URL("https://" + ServerConstants.getWwwHostNamePortSecure() + "/login").openConnection();
+        assertTrue( !IOUtils.readURL(con).contains("No cross domain access allowed."));
+    }
+
+    @Test
+    public void testCorrectOriginHeaderFromHttpsToHttps() throws MalformedURLException, IOException {
+        URLConnection con = new URL("https://" + ServerConstants.getWwwHostNamePortSecure() + "/login").openConnection();
+        con.setRequestProperty("Origin", "https://" + ServerConstants.getWwwHostNamePortSecure());
+        assertTrue( !IOUtils.readURL(con).contains("No cross domain access allowed."));
+    }
+
+    @Test
+    public void testCorrectOriginHeaderFromHttpToHttps() throws MalformedURLException, IOException {
+        URLConnection con = new URL("https://" + ServerConstants.getWwwHostNamePortSecure() + "/login").openConnection();
+        con.setRequestProperty("Origin", "http://" + ServerConstants.getWwwHostNamePort());
+        assertTrue( !IOUtils.readURL(con).contains("No cross domain access allowed."));
+    }
+
+    @Test
+    public void testCorrectOriginHeaderFromHttpsToSecure() throws MalformedURLException, IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException {
+        User u = User.getById(createVerifiedUser("fn", "ln", "testmail@example.com", TEST_PASSWORD));
+        KeyPair kp = generateKeypair();
+        String key = generatePEMCSR(kp, "CN=testmail@example.com");
+        Certificate c = new Certificate(u, u, Certificate.buildDN("CN", "testmail@example.com"), Digest.SHA256, key, CSRType.CSR, CertificateProfile.getById(1));
+        final PrivateKey pk = kp.getPrivate();
+        c.issue(null, "2y", u).waitFor(60000);
+
+        URLConnection con = new URL("https://" + ServerConstants.getSecureHostNamePort()).openConnection();
+        authenticateClientCert(pk, c.cert(), (HttpURLConnection) con);
+        con.setRequestProperty("Origin", "https://" + ServerConstants.getWwwHostNamePortSecure());
+        String contains = IOUtils.readURL(con);
+        assertTrue( !contains.contains("No cross domain access allowed."));
+    }
+
+    @Test
+    public void testCorrectOriginHeaderFromHttpsToHttp() throws MalformedURLException, IOException {
+        URLConnection con = new URL("http://" + ServerConstants.getWwwHostNamePort()).openConnection();
+        con.setRequestProperty("Origin", "https://" + ServerConstants.getWwwHostNamePortSecure());
+        assertTrue( !IOUtils.readURL(con).contains("No cross domain access allowed."));
+    }
+
+    @Test
+    public void testIncorrectOriginHeader() throws MalformedURLException, IOException {
+        HttpURLConnection con = (HttpURLConnection) new URL("https://" + ServerConstants.getWwwHostNamePortSecure() + "/login").openConnection();
+        con.setRequestProperty("Origin", "https://evilpageandatleastnotcacert.com");
+        assertTrue(IOUtils.readURL(con).contains("No cross domain access allowed."));
+    }
+
+}
diff --git a/tests/org/cacert/gigi/TestDomain.java b/tests/org/cacert/gigi/TestDomain.java
new file mode 100644 (file)
index 0000000..91f29c7
--- /dev/null
@@ -0,0 +1,79 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+public class TestDomain extends ManagedTest {
+
+    private User us;
+
+    public TestDomain() {
+        int uid = createVerifiedUser("fn", "ln", createUniqueName() + "pr@test-email.de", TEST_PASSWORD);
+        us = User.getById(uid);
+    }
+
+    @Test
+    public void testDomain() throws InterruptedException, GigiApiException {
+        assertEquals(0, us.getDomains().length);
+        Domain d = new Domain(us, us, "v1example.org");
+        Domain[] domains = us.getDomains();
+        assertEquals(1, domains.length);
+        assertEquals("v1example.org", domains[0].getSuffix());
+        assertEquals(domains[0].getOwner().getId(), us.getId());
+        assertNotEquals(0, domains[0].getId());
+        assertNotEquals(0, d.getId());
+        assertEquals(d.getId(), domains[0].getId());
+
+        new Domain(us, us, "v2-example.org");
+
+        domains = us.getDomains();
+        assertEquals(2, domains.length);
+        if ( !domains[1].getSuffix().equals("v2-example.org")) {
+            Domain d1 = domains[0];
+            domains[0] = domains[1];
+            domains[1] = d1;
+        }
+        assertEquals("v2-example.org", domains[1].getSuffix());
+        assertEquals(domains[0].getOwner().getId(), us.getId());
+        assertEquals(domains[1].getOwner().getId(), us.getId());
+        assertNotEquals(0, domains[0].getId());
+        assertNotEquals(0, d.getId());
+        assertEquals(d.getId(), domains[0].getId());
+
+    }
+
+    @Test
+    public void testDoubleDomain() throws InterruptedException, GigiApiException {
+        new Domain(us, us, "dub-example.org");
+        try {
+            new Domain(us, us, "dub-example.org");
+            fail("expected exception, was able to insert domain (with different case) a second time");
+        } catch (GigiApiException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testDoubleDomainCase() throws InterruptedException, GigiApiException {
+        Domain d = new Domain(us, us, "dub2-ExaMple.Org");
+        assertEquals("dub2-example.org", d.getSuffix());
+        try {
+            new Domain(us, us, "duB2-eXample.oRG");
+            fail("expected exception, was able to insert domain (with different case) a second time");
+        } catch (GigiApiException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testDoubleDomainDelete() throws InterruptedException, GigiApiException {
+        Domain d = new Domain(us, us, "delexample.org");
+        d.delete();
+        new Domain(us, us, "delexample.org");
+    }
+
+}
diff --git a/tests/org/cacert/gigi/TestLanguage.java b/tests/org/cacert/gigi/TestLanguage.java
new file mode 100644 (file)
index 0000000..912cab6
--- /dev/null
@@ -0,0 +1,82 @@
+package org.cacert.gigi;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Assume;
+import org.junit.Test;
+
+public class TestLanguage extends ManagedTest {
+
+    @Test
+    public void testSignupNoLanguage() {
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        assertEquals(Locale.ENGLISH, u.getPreferredLocale());
+    }
+
+    @Test
+    public void testSignupDE() {
+        setAcceptLanguage("de");
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        assertEquals(Locale.GERMAN, u.getPreferredLocale());
+    }
+
+    @Test
+    public void testSignupMulti() {
+        setAcceptLanguage("de,en");
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        assertEquals(Locale.GERMAN, u.getPreferredLocale());
+    }
+
+    @Test
+    public void testSignupFallback() {
+        setAcceptLanguage("ma,de");
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        assertEquals(Locale.GERMAN, u.getPreferredLocale());
+    }
+
+    @Test
+    public void testSignupProjection() {
+        setAcceptLanguage("de-de,en");
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        assertEquals(Locale.GERMAN, u.getPreferredLocale());
+    }
+
+    @Test
+    public void testSelectStandard() throws IOException {
+        String content = IOUtils.readURL(get("cook", "/"));
+        assertThat(content, containsString("Language"));
+    }
+
+    @Test
+    public void testSelectGerman() throws IOException {
+        String content = IOUtils.readURL(get("", "/?lang=de"));
+        assertThat(content, containsString(Language.getInstance(Locale.GERMAN).getTranslation("Language")));
+    }
+
+    @Test
+    public void testLanguageAfterLogin() throws IOException {
+        setAcceptLanguage("de,en");
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        String cookie = login(u.getEmail(), TEST_PASSWORD);
+        String content = IOUtils.readURL(get(cookie, "/"));
+        assertThat(content, containsString(Language.getInstance(Locale.GERMAN).getTranslation("Language")));
+    }
+
+    @Test
+    public void testOtherLanguageAfterLogin() throws IOException {
+        Assume.assumeNotNull(Language.getInstance(Locale.FRENCH));
+        setAcceptLanguage("fr,de,en");
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        String cookie = login(u.getEmail(), TEST_PASSWORD);
+        String content = IOUtils.readURL(get(cookie, "/"));
+        assertThat(content, containsString(Language.getInstance(Locale.FRENCH).getTranslation("Language")));
+    }
+}
diff --git a/tests/org/cacert/gigi/TestName.java b/tests/org/cacert/gigi/TestName.java
new file mode 100644 (file)
index 0000000..b431166
--- /dev/null
@@ -0,0 +1,44 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import org.cacert.gigi.dbObjects.Name;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestName {
+
+    Name n = new Name("fn", "ln", "mn", "sf");
+
+    @Before
+    public void setUp() throws Exception {}
+
+    @Test
+    public void testHashCode() {
+        assertEquals(new Name("fname", "lname", null, null).hashCode(), new Name("fname", "lname", null, null).hashCode());
+        assertNotEquals(new Name("fname", "lname", null, null).hashCode(), new Name("fname", "lname", null, "b").hashCode());
+        assertNotEquals(new Name("fname", "lname", null, null).hashCode(), new Name("fname", "lname", "b", null).hashCode());
+        assertNotEquals(new Name("fname", "lname", null, null).hashCode(), new Name("fname", "name", null, null).hashCode());
+        assertNotEquals(new Name("fname", "lname", null, null).hashCode(), new Name("name", "lname", null, null).hashCode());
+    }
+
+    @Test
+    public void testEqualsObject() {
+        assertFalse(n.equals(null));
+        assertFalse(n.equals("blargh"));
+        Name nullname = new Name(null, null, null, null);
+        assertFalse(n.equals(nullname));
+        assertFalse(nullname.equals(n));
+        assertTrue(nullname.equals(nullname));
+        assertTrue(n.equals(n));
+    }
+
+    @Test
+    public void testMatches() {
+        assertTrue(n.matches("fn ln"));
+        assertTrue(n.matches("fn ln sf"));
+        assertTrue(n.matches("fn mn ln sf"));
+        assertFalse(n.matches("blargh"));
+    }
+
+}
diff --git a/tests/org/cacert/gigi/TestObjectCache.java b/tests/org/cacert/gigi/TestObjectCache.java
new file mode 100644 (file)
index 0000000..d2a0459
--- /dev/null
@@ -0,0 +1,52 @@
+package org.cacert.gigi;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.sql.SQLException;
+import java.util.Calendar;
+import java.util.Locale;
+
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.cacert.gigi.util.DayDate;
+import org.junit.Test;
+
+public class TestObjectCache extends ManagedTest {
+
+    int uid = createVerifiedUser("fname", "lname", createUniqueName() + "@example.com", TEST_PASSWORD);
+
+    @Test
+    public void testUserCache() throws SQLException, GigiApiException {
+        assertThat(User.getById(uid), is(sameInstance(User.getById(uid))));
+
+        Calendar c = Calendar.getInstance();
+        c.set(1950, 1, 1, 0, 0, 0);
+        c.set(Calendar.MILLISECOND, 0);
+        User u = new User(createUniqueName() + "@example.org", TEST_PASSWORD, new Name("fname", "lname", "mname", "suffix"), new DayDate(c.getTime().getTime()), Locale.ENGLISH);
+
+        assertThat(u, is(sameInstance(User.getById(u.getId()))));
+        assertThat(User.getById(u.getId()), is(sameInstance(User.getById(u.getId()))));
+
+    }
+
+    @Test
+    public void testDomainCache() throws GigiApiException {
+        User u = User.getById(uid);
+        Domain d = new Domain(u, u, "example.org");
+
+        assertThat(d, is(sameInstance(Domain.getById(d.getId()))));
+        assertThat(Domain.getById(d.getId()), is(sameInstance(Domain.getById(d.getId()))));
+    }
+
+    @Test
+    public void testEmailCache() throws GigiApiException {
+        EmailAddress em = new EmailAddress(User.getById(uid), createUniqueName() + "@example.org", Locale.ENGLISH);
+
+        assertThat(em, is(sameInstance(EmailAddress.getById(em.getId()))));
+        assertThat(EmailAddress.getById(em.getId()), is(sameInstance(EmailAddress.getById(em.getId()))));
+    }
+}
diff --git a/tests/org/cacert/gigi/TestOrga.java b/tests/org/cacert/gigi/TestOrga.java
new file mode 100644 (file)
index 0000000..91afa2e
--- /dev/null
@@ -0,0 +1,46 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+public class TestOrga extends ManagedTest {
+
+    @Test
+    public void testAddRm() throws GigiApiException, IOException {
+        User u1 = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@email.org", TEST_PASSWORD));
+        u1.grantGroup(u1, Group.ORGASSURER);
+        User u2 = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@email.org", TEST_PASSWORD));
+        u2.grantGroup(u1, Group.ORGASSURER);
+        User u3 = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@email.org", TEST_PASSWORD));
+        u3.grantGroup(u1, Group.ORGASSURER);
+        User u4 = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@email.org", TEST_PASSWORD));
+        u4.grantGroup(u1, Group.ORGASSURER);
+        Organisation o1 = new Organisation("name", "ST", "prov", "city", "email", "optional name", "postal address", u1);
+        assertEquals(0, o1.getAllAdmins().size());
+        o1.addAdmin(u2, u1, false);
+        assertEquals(1, o1.getAllAdmins().size());
+        o1.addAdmin(u2, u1, false); // Insert double should be ignored
+        assertEquals(1, o1.getAllAdmins().size());
+        o1.addAdmin(u3, u1, false);
+        assertEquals(2, o1.getAllAdmins().size());
+        o1.addAdmin(u4, u1, false);
+        assertEquals(3, o1.getAllAdmins().size());
+        o1.removeAdmin(u3, u1);
+        assertEquals(2, o1.getAllAdmins().size());
+        o1.addAdmin(u3, u1, false); // add again
+        assertEquals(3, o1.getAllAdmins().size());
+        o1.removeAdmin(u3, u1);
+        assertEquals(2, o1.getAllAdmins().size());
+        o1.removeAdmin(u4, u1);
+        o1.removeAdmin(u2, u1);
+        assertEquals(0, o1.getAllAdmins().size());
+    }
+
+}
diff --git a/tests/org/cacert/gigi/TestPasswordReset.java b/tests/org/cacert/gigi/TestPasswordReset.java
new file mode 100644 (file)
index 0000000..1840210
--- /dev/null
@@ -0,0 +1,73 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.util.RandomToken;
+import org.junit.Test;
+
+public class TestPasswordReset extends ClientTest {
+
+    String pub = RandomToken.generateToken(32);
+
+    String priv = RandomToken.generateToken(32);
+
+    int id = u.generatePasswordResetTicket(u, pub, priv);
+
+    @Test
+    public void testInternal() throws IOException, GigiApiException {
+        User u2 = User.getResetWithToken(id, pub);
+        assertSame(u, u2);
+        assertNotNull(login(u.getEmail(), TEST_PASSWORD));
+        u2.consumePasswordResetTicket(id, priv, TEST_PASSWORD + "'");
+        assertEquals("", login(u.getEmail(), TEST_PASSWORD));
+        assertNotNull(login(u.getEmail(), TEST_PASSWORD + "'"));
+    }
+
+    @Test
+    public void testDoubleUse() throws IOException, GigiApiException {
+        User u2 = User.getResetWithToken(id, pub);
+        assertSame(u, u2);
+        assertNotNull(login(u.getEmail(), TEST_PASSWORD));
+        u2.consumePasswordResetTicket(id, priv, TEST_PASSWORD + "'");
+        assertEquals("", login(u.getEmail(), TEST_PASSWORD));
+        assertNotNull(login(u.getEmail(), TEST_PASSWORD + "'"));
+        try {
+            u2.consumePasswordResetTicket(id, priv, TEST_PASSWORD + "''");
+            fail("Exception expected.");
+        } catch (GigiApiException e) {
+            // expected
+        }
+        assertNotNull(login(u.getEmail(), TEST_PASSWORD + "'"));
+    }
+
+    @Test
+    public void testInternalWrongTk() throws IOException, GigiApiException {
+        User u2 = User.getResetWithToken(id, pub + "'");
+        assertNull(u2);
+    }
+
+    @Test
+    public void testInternalWrongId() throws IOException, GigiApiException {
+        User u2 = User.getResetWithToken(id + 1, pub);
+        assertNull(u2);
+    }
+
+    @Test(expected = GigiApiException.class)
+    public void testInternalWeak() throws IOException, GigiApiException {
+        u.consumePasswordResetTicket(id, priv, "");
+    }
+
+    @Test(expected = GigiApiException.class)
+    public void testInternalWrongPriv() throws IOException, GigiApiException {
+        u.consumePasswordResetTicket(id, priv + "'", TEST_PASSWORD);
+    }
+
+    @Test(expected = GigiApiException.class)
+    public void testInternalWrongIdSetting() throws IOException, GigiApiException {
+        u.consumePasswordResetTicket(id + 1, priv, TEST_PASSWORD);
+    }
+}
diff --git a/tests/org/cacert/gigi/TestSQL.java b/tests/org/cacert/gigi/TestSQL.java
new file mode 100644 (file)
index 0000000..f7f6df7
--- /dev/null
@@ -0,0 +1,30 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.testUtils.ConfiguredTest;
+import org.junit.Test;
+
+public class TestSQL extends ConfiguredTest {
+
+    @Test
+    public void testPrepared() {
+        GigiPreparedStatement[] ps = new GigiPreparedStatement[DatabaseConnection.MAX_CACHED_INSTANCES];
+        String stmt = "SELECT 1 FROM `users`;";
+        for (int i = 0; i < ps.length; i++) {
+            assertEquals(i, DatabaseConnection.getInstance().getNumberOfLockedStatements());
+            ps[i] = new GigiPreparedStatement(stmt);
+        }
+        assertEquals(DatabaseConnection.MAX_CACHED_INSTANCES, DatabaseConnection.getInstance().getNumberOfLockedStatements());
+        for (int i = ps.length - 1; i >= 0; i--) {
+            ps[i].close();
+            assertEquals(i, DatabaseConnection.getInstance().getNumberOfLockedStatements());
+        }
+        for (int i = 0; i < ps.length; i++) {
+            assertEquals(i, DatabaseConnection.getInstance().getNumberOfLockedStatements());
+            ps[i] = new GigiPreparedStatement(stmt);
+        }
+    }
+}
index 9756a7f3f4e2a044acd5860d1b58c000bf84fee5..74806eef8bd74170e5bbdbce74230e2fe03fe3d1 100644 (file)
@@ -18,90 +18,92 @@ import org.cacert.gigi.testUtils.ManagedTest;
 import org.junit.Test;
 
 public class TestSSL extends ManagedTest {
-       private ByteBuffer in;
-       private ByteBuffer inC;
-       private ByteBuffer outC;
-       private ByteBuffer out;
-       static {
-               InitTruststore.run();
-       }
-       @Test
-       public void testClientIntitiatedRenegotiation()
-                       throws NoSuchAlgorithmException, IOException {
-               SSLContext sc = SSLContext.getDefault();
-               SSLEngine se = sc.createSSLEngine();
-               String[] serverParts = getServerName().split(":", 2);
-               SocketChannel s = SocketChannel.open(new InetSocketAddress(
-                               serverParts[0], Integer.parseInt(serverParts[1])));
-
-               in = ByteBuffer.allocate(se.getSession().getApplicationBufferSize());
-               inC = ByteBuffer.allocate(se.getSession().getPacketBufferSize());
-               inC.limit(0);
-               out = ByteBuffer.allocate(se.getSession().getApplicationBufferSize());
-               outC = ByteBuffer.allocate(se.getSession().getPacketBufferSize());
-               outC.limit(0);
-               se.setUseClientMode(true);
-               se.beginHandshake();
-
-               work(se, s);
-               se.beginHandshake();
-               try {
-                       work(se, s);
-                       throw new Error(
-                                       "Client re-negotiation succeded (possible DoS vulnerability");
-               } catch (EOFException e) {
-                       // Cool, server closed connection
-               }
-
-       }
-       private void work(SSLEngine se, SocketChannel s) throws SSLException,
-                       IOException {
-               while (se.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING
-                               && se.getHandshakeStatus() != HandshakeStatus.FINISHED) {
-                       switch (se.getHandshakeStatus()) {
-                               case NEED_WRAP :
-                                       wrap(se, s);
-                                       break;
-                               case NEED_UNWRAP :
-                                       unwrap(se, s);
-                                       break;
-                               case NEED_TASK :
-                                       se.getDelegatedTask().run();
-                                       break;
-                               default :
-                                       System.out.println(se.getHandshakeStatus());
-                       }
-               }
-       }
-       private SSLEngineResult unwrap(SSLEngine se, SocketChannel s)
-                       throws IOException, SSLException {
-               if (inC.remaining() == 0) {
-                       inC.clear();
-                       s.read(inC);
-                       inC.flip();
-               }
-               SSLEngineResult result = se.unwrap(inC, in);
-               if (result.getStatus() == javax.net.ssl.SSLEngineResult.Status.BUFFER_UNDERFLOW) {
-                       int pos = inC.position();
-                       int limit = inC.limit();
-                       inC.limit(inC.capacity());
-                       inC.position(limit);
-                       int read = s.read(inC);
-                       if (read <= 0) {
-                               throw new EOFException();
-                       }
-                       inC.limit(inC.position());
-                       inC.position(pos);
-               }
-               return result;
-       }
-       private SSLEngineResult wrap(SSLEngine se, SocketChannel s)
-                       throws SSLException, IOException {
-               outC.clear();
-               SSLEngineResult result = se.wrap(out, outC);
-               outC.flip();
-               s.write(outC);
-
-               return result;
-       }
+
+    private ByteBuffer in;
+
+    private ByteBuffer inC;
+
+    private ByteBuffer outC;
+
+    private ByteBuffer out;
+    static {
+        InitTruststore.run();
+    }
+
+    @Test
+    public void testClientIntitiatedRenegotiation() throws NoSuchAlgorithmException, IOException {
+        SSLContext sc = SSLContext.getDefault();
+        SSLEngine se = sc.createSSLEngine();
+        String[] serverParts = getServerName().split(":", 2);
+        try (SocketChannel s = SocketChannel.open(new InetSocketAddress(serverParts[0], Integer.parseInt(serverParts[1])))) {
+
+            in = ByteBuffer.allocate(se.getSession().getApplicationBufferSize());
+            inC = ByteBuffer.allocate(se.getSession().getPacketBufferSize());
+            inC.limit(0);
+            out = ByteBuffer.allocate(se.getSession().getApplicationBufferSize());
+            outC = ByteBuffer.allocate(se.getSession().getPacketBufferSize());
+            outC.limit(0);
+            se.setUseClientMode(true);
+            se.beginHandshake();
+
+            work(se, s);
+            se.beginHandshake();
+            try {
+                work(se, s);
+                throw new Error("Client re-negotiation succeded (possible DoS vulnerability");
+            } catch (EOFException e) {
+                // Cool, server closed connection
+            }
+        }
+
+    }
+
+    private void work(SSLEngine se, SocketChannel s) throws SSLException, IOException {
+        while (se.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING && se.getHandshakeStatus() != HandshakeStatus.FINISHED) {
+            switch (se.getHandshakeStatus()) {
+            case NEED_WRAP:
+                wrap(se, s);
+                break;
+            case NEED_UNWRAP:
+                unwrap(se, s);
+                break;
+            case NEED_TASK:
+                se.getDelegatedTask().run();
+                break;
+            default:
+                System.out.println(se.getHandshakeStatus());
+            }
+        }
+    }
+
+    private SSLEngineResult unwrap(SSLEngine se, SocketChannel s) throws IOException, SSLException {
+        if (inC.remaining() == 0) {
+            inC.clear();
+            s.read(inC);
+            inC.flip();
+        }
+        SSLEngineResult result = se.unwrap(inC, in);
+        if (result.getStatus() == javax.net.ssl.SSLEngineResult.Status.BUFFER_UNDERFLOW) {
+            int pos = inC.position();
+            int limit = inC.limit();
+            inC.limit(inC.capacity());
+            inC.position(limit);
+            int read = s.read(inC);
+            if (read <= 0) {
+                throw new EOFException();
+            }
+            inC.limit(inC.position());
+            inC.position(pos);
+        }
+        return result;
+    }
+
+    private SSLEngineResult wrap(SSLEngine se, SocketChannel s) throws SSLException, IOException {
+        outC.clear();
+        SSLEngineResult result = se.wrap(out, outC);
+        outC.flip();
+        s.write(outC);
+
+        return result;
+    }
 }
diff --git a/tests/org/cacert/gigi/TestSecurityHeaders.java b/tests/org/cacert/gigi/TestSecurityHeaders.java
new file mode 100644 (file)
index 0000000..8149d32
--- /dev/null
@@ -0,0 +1,29 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+public class TestSecurityHeaders extends ManagedTest {
+
+    @Test
+    public void testSTS() throws IOException {
+        HttpURLConnection uc = get(null, "/");
+        assertNotNull(uc.getHeaderField("Strict-Transport-Security"));
+    }
+
+    public void testCSP() throws IOException {
+        HttpURLConnection uc = get(null, "/");
+        assertNotNull(uc.getHeaderField("Content-Security-Policy"));
+    }
+
+    public void testAllowOrigin() throws IOException {
+        HttpURLConnection uc = get(null, "/");
+        assertNotNull(uc.getHeaderField("Access-Control-Allow-Origin"));
+
+    }
+}
diff --git a/tests/org/cacert/gigi/TestSeparateSessionScope.java b/tests/org/cacert/gigi/TestSeparateSessionScope.java
new file mode 100644 (file)
index 0000000..9d2ff10
--- /dev/null
@@ -0,0 +1,77 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.sql.SQLException;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.Job;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+public class TestSeparateSessionScope extends ManagedTest {
+
+    @Test
+    public void testSeparateScope() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException {
+        String mail = "thisgo" + createUniqueName() + "@example.com";
+        int user = createAssuranceUser("test", "tugo", mail, TEST_PASSWORD);
+        String cookie = login(mail, TEST_PASSWORD);
+        KeyPair kp = generateKeypair();
+        String csr = generatePEMCSR(kp, "CN=hans");
+        User u = User.getById(user);
+        Certificate c = new Certificate(u, u, Certificate.buildDN("CN", "hans"), Digest.SHA256, csr, CSRType.CSR, CertificateProfile.getById(1));
+        final PrivateKey pk = kp.getPrivate();
+        c.issue(null, "2y", u).waitFor(60000);
+        final X509Certificate ce = c.cert();
+        String scookie = login(pk, ce);
+
+        assertTrue(isLoggedin(cookie));
+        assertFalse(isLoggedin(scookie));
+
+        checkCertLogin(c, pk, scookie, 200);
+        checkCertLogin(c, pk, cookie, 302);
+    }
+
+    @Test
+    public void testSerialSteal() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException {
+        String mail = "thisgo" + createUniqueName() + "@example.com";
+        int user = createAssuranceUser("test", "tugo", mail, TEST_PASSWORD);
+        KeyPair kp = generateKeypair();
+        String csr = generatePEMCSR(kp, "CN=hans");
+        User u = User.getById(user);
+        Certificate c = new Certificate(u, u, Certificate.buildDN("CN", "hans"), Digest.SHA256, csr, CSRType.CSR, CertificateProfile.getById(1));
+        Certificate c2 = new Certificate(u, u, Certificate.buildDN("CN", "hans"), Digest.SHA256, csr, CSRType.CSR, CertificateProfile.getById(1));
+        final PrivateKey pk = kp.getPrivate();
+        Job j1 = c.issue(null, "2y", u);
+        c2.issue(null, "2y", u).waitFor(60000);
+        j1.waitFor(60000);
+        final X509Certificate ce = c.cert();
+        String scookie = login(pk, ce);
+
+        checkCertLogin(c, pk, scookie, 200);
+        checkCertLogin(c2, pk, scookie, 403);
+        checkCertLogin(c, pk, scookie, 302);
+
+    }
+
+    private void checkCertLogin(Certificate c2, final PrivateKey pk, String scookie, int expected) throws IOException, NoSuchAlgorithmException, KeyManagementException, GeneralSecurityException {
+        URL u = new URL("https://" + getServerName().replaceAll("^www", "secure") + SECURE_REFERENCE);
+        HttpURLConnection huc = (HttpURLConnection) u.openConnection();
+        authenticateClientCert(pk, c2.cert(), huc);
+        huc.setRequestProperty("Cookie", scookie);
+        assertEquals(expected, huc.getResponseCode());
+    }
+}
diff --git a/tests/org/cacert/gigi/TestUser.java b/tests/org/cacert/gigi/TestUser.java
new file mode 100644 (file)
index 0000000..a3dfb77
--- /dev/null
@@ -0,0 +1,138 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Locale;
+
+import org.cacert.gigi.dbObjects.Assurance;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.cacert.gigi.util.DayDate;
+import org.junit.Test;
+
+public class TestUser extends ManagedTest {
+
+    @Test
+    public void testStoreAndLoad() throws SQLException, GigiApiException {
+        long dob = System.currentTimeMillis();
+        dob -= dob % (1000 * 60 * 60 * 24);
+        User u = new User(createUniqueName() + "a@email.org", "password", new Name("user", "last", "", ""), new DayDate(dob), Locale.ENGLISH);
+        int id = u.getId();
+        User u2 = User.getById(id);
+        assertEquals(u.getName(), u2.getName());
+        assertEquals(u.getDoB().toString(), u2.getDoB().toString());
+        assertEquals(u.getEmail(), u2.getEmail());
+    }
+
+    @Test
+    public void testWebStoreAndLoad() throws SQLException {
+        int id = createVerifiedUser("aä", "b", createUniqueName() + "a@email.org", TEST_PASSWORD);
+
+        Name u = User.getById(id).getName();
+
+        assertEquals("aä", u.getFname());
+        assertEquals("b", u.getLname());
+        assertEquals("", u.getMname());
+    }
+
+    @Test
+    public void testAssurerUtilMethods() throws SQLException {
+        int id = createAssuranceUser("aä", "b", createUniqueName() + "a@email.org", TEST_PASSWORD);
+
+        User u = User.getById(id);
+        assertTrue(u.canAssure());
+        int assurancePoints = u.getAssurancePoints();
+        int expPoints = u.getExperiencePoints();
+        assertEquals(100, assurancePoints);
+        assertEquals(2, expPoints);
+        assertTrue(u.hasPassedCATS());
+        assertEquals(10, u.getMaxAssurePoints());
+        Name name = u.getName();
+        assertEquals("aä", name.getFname());
+        assertEquals("b", name.getLname());
+        assertEquals("", name.getMname());
+    }
+
+    @Test
+    public void testMatcherMethods() throws SQLException, GigiApiException, IOException {
+        String uq = createUniqueName();
+        int id = createVerifiedUser("aä", "b", uq + "a@email.org", TEST_PASSWORD);
+
+        User u = User.getById(id);
+        new EmailAddress(u, uq + "b@email.org", Locale.ENGLISH);
+        getMailReciever().receive().verify();
+        new EmailAddress(u, uq + "c@email.org", Locale.ENGLISH);
+        getMailReciever().receive();// no-verify
+        verify(new Domain(u, u, uq + "a-testdomain.org"));
+        verify(new Domain(u, u, uq + "b-testdomain.org"));
+        verify(new Domain(u, u, uq + "c-testdomain.org"));
+        assertEquals(3, u.getEmails().length);
+        assertEquals(3, u.getDomains().length);
+        assertTrue(u.isValidDomain(uq + "a-testdomain.org"));
+        assertTrue(u.isValidDomain(uq + "b-testdomain.org"));
+        assertTrue(u.isValidDomain(uq + "c-testdomain.org"));
+        assertTrue(u.isValidDomain("a." + uq + "a-testdomain.org"));
+        assertTrue(u.isValidDomain("*." + uq + "a-testdomain.org"));
+        assertFalse(u.isValidDomain("a" + uq + "a-testdomain.org"));
+        assertFalse(u.isValidDomain("b" + uq + "a-testdomain.org"));
+
+        assertTrue(u.isValidEmail(uq + "a@email.org"));
+        assertTrue(u.isValidEmail(uq + "b@email.org"));
+        assertFalse(u.isValidEmail(uq + "b+6@email.org"));
+        assertFalse(u.isValidEmail(uq + "b*@email.org"));
+        assertFalse(u.isValidEmail(uq + "c@email.org"));
+
+        assertTrue(u.isValidName("aä b"));
+        assertFalse(u.isValidName("aä c"));
+        assertFalse(u.isValidName("aä d b"));
+
+    }
+
+    @Test
+    public void testDoubleInsert() throws GigiApiException {
+        long d = System.currentTimeMillis();
+        d -= d % DayDate.MILLI_DAY;
+        User u = new User(createUniqueName() + "@example.org", TEST_PASSWORD, new Name("f", "k", "m", "s"), new DayDate(d + 1000L * 60 * 60 * 24 * 365), Locale.ENGLISH);
+        Assurance[] ma = u.getMadeAssurances();
+        Assurance[] ma2 = u.getMadeAssurances();
+        Assurance[] ra = u.getReceivedAssurances();
+        Assurance[] ra2 = u.getReceivedAssurances();
+        assertEquals(0, u.getCertificates(false).length);
+        assertEquals(0, ma.length);
+        assertEquals(0, ma2.length);
+        assertEquals(0, ra.length);
+        assertEquals(0, ra2.length);
+        assertSame(ma, ma2);
+        assertSame(ra, ra2);
+    }
+
+    @Test
+    public void testGetByMail() {
+        String email = createUniqueName() + "a@email.org";
+        int id = createVerifiedUser("aä", "b", email, TEST_PASSWORD);
+        User emailUser = User.getByEmail(email);
+        User u = User.getById(id);
+        assertSame(u, emailUser);
+    }
+
+    @Test
+    public void testNoCats() {
+        String email = createUniqueName() + "a@email.org";
+        createVerifiedUser("aä", "b", email, TEST_PASSWORD);
+        User emailUser = User.getByEmail(email);
+        assertFalse(emailUser.hasPassedCATS());
+    }
+
+    @Test
+    public void testGetByMailFail() {
+        String email = createUniqueName() + "d@email.org";
+        User emailUser = User.getByEmail(email);
+        assertNull(emailUser);
+    }
+
+}
diff --git a/tests/org/cacert/gigi/TestUserGroupMembership.java b/tests/org/cacert/gigi/TestUserGroupMembership.java
new file mode 100644 (file)
index 0000000..7ac06ff
--- /dev/null
@@ -0,0 +1,126 @@
+package org.cacert.gigi;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.ObjectCache;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+public class TestUserGroupMembership extends ManagedTest {
+
+    private final Group ttpGroup = Group.getByString("ttp-assurer");
+
+    private final Group supporter = Group.getByString("supporter");
+
+    @Test
+    public void testAddObject() throws GigiApiException, SQLException {
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+
+        User granter = User.getById(createVerifiedUser("grFname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+        assertBehavesEmpty(u);
+
+        u.grantGroup(granter, ttpGroup);
+        assertBehavesTtpGroup(u);
+
+        ObjectCache.clearAllCaches();
+        User u2 = User.getById(u.getId());
+
+        assertThat(u2, is(not(sameInstance(u))));
+        assertBehavesTtpGroup(u2);
+
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT * FROM `user_groups` WHERE `user`=?")) {
+            ps.setInt(1, u.getId());
+            GigiResultSet rs = ps.executeQuery();
+
+            assertTrue(rs.next());
+            assertEquals(0, rs.getInt("revokedby"));
+            assertEquals(granter.getId(), rs.getInt("grantedby"));
+            assertEquals(ttpGroup.getDatabaseName(), rs.getString("permission"));
+
+            assertNull(rs.getDate("deleted"));
+            assertNotNull(rs.getDate("granted"));
+
+            assertFalse(rs.next());
+        }
+    }
+
+    @Test
+    public void testRemoveObject() throws GigiApiException, SQLException {
+        User u = User.getById(createVerifiedUser("fname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+
+        User granter = User.getById(createVerifiedUser("grFname", "lname", createUniqueName() + "@example.org", TEST_PASSWORD));
+
+        assertBehavesEmpty(u);
+        u.grantGroup(granter, ttpGroup);
+        assertBehavesTtpGroup(u);
+        u.revokeGroup(granter, ttpGroup);
+        assertBehavesEmpty(u);
+
+        ObjectCache.clearAllCaches();
+        User u2 = User.getById(u.getId());
+        assertThat(u2, is(not(sameInstance(u))));
+        assertBehavesEmpty(u);
+
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT * FROM `user_groups` WHERE `user`=?")) {
+            ps.setInt(1, u.getId());
+            GigiResultSet rs = ps.executeQuery();
+            assertTrue(rs.next());
+            assertEquals(granter.getId(), rs.getInt("revokedby"));
+            assertEquals(granter.getId(), rs.getInt("grantedby"));
+            assertEquals(ttpGroup.getDatabaseName(), rs.getString("permission"));
+
+            assertNotNull(rs.getDate("deleted"));
+            assertNotNull(rs.getDate("granted"));
+
+            assertFalse(rs.next());
+        }
+    }
+
+    private void assertBehavesEmpty(User u) {
+        assertEquals(Collections.emptySet(), u.getGroups());
+        assertFalse(u.isInGroup(ttpGroup));
+        assertFalse(u.isInGroup(supporter));
+    }
+
+    private void assertBehavesTtpGroup(User u) {
+        assertEquals(new HashSet<>(Arrays.asList(ttpGroup)), u.getGroups());
+        assertTrue(u.isInGroup(ttpGroup));
+        assertFalse(u.isInGroup(supporter));
+    }
+
+    @Test
+    public void testListGroup() {
+        Group g = Group.getByString("supporter");
+        int start = g.getMembers(0, 10).length;
+        User ux = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "@example.org", TEST_PASSWORD));
+        User ux2 = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "@example.org", TEST_PASSWORD));
+        assertEquals(0, g.getMembers(0, 10).length + start);
+        ux.grantGroup(ux, g);
+        assertEquals(1, g.getMembers(0, 10).length + start);
+        ux2.grantGroup(ux, g);
+        assertEquals(2, g.getMembers(0, 10).length + start);
+        ux2.revokeGroup(ux, g);
+        assertEquals(1, g.getMembers(0, 10).length + start);
+        ux.revokeGroup(ux, g);
+        assertEquals(0, g.getMembers(0, 10).length + start);
+
+    }
+
+    @Test
+    public void testGroupEquals() {
+        assertTrue(ttpGroup.equals(ttpGroup));
+        assertFalse(ttpGroup.equals(null));
+        assertFalse(ttpGroup.equals(""));
+        assertFalse(ttpGroup.equals(supporter));
+    }
+}
diff --git a/tests/org/cacert/gigi/api/ImportCATSResult.java b/tests/org/cacert/gigi/api/ImportCATSResult.java
new file mode 100644 (file)
index 0000000..db19380
--- /dev/null
@@ -0,0 +1,124 @@
+package org.cacert.gigi.api;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.CATS;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.Certificate.SANType;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.junit.Test;
+
+public class ImportCATSResult extends ClientTest {
+
+    private PrivateKey pk;
+
+    private X509Certificate ce;
+
+    public ImportCATSResult() throws IOException, GeneralSecurityException, InterruptedException, GigiApiException {
+        makeAssurer(id);
+
+        grant(u.getEmail(), Group.ORGASSURER);
+        clearCaches();
+        u = User.getById(u.getId());
+        Organisation o = new Organisation(Organisation.SELF_ORG_NAME, "NA", "NA", "NA", "contact@cacert.org", "", "", u);
+        assertTrue(o.isSelfOrganisation());
+        KeyPair kp = generateKeypair();
+        String key1 = generatePEMCSR(kp, "EMAIL=cats@cacert.org");
+        Certificate c = new Certificate(o, u, Certificate.buildDN("EMAIL", "cats@cacert.org"), Digest.SHA256, key1, CSRType.CSR, CertificateProfile.getByName("client-orga"), new Certificate.SubjectAlternateName(SANType.EMAIL, "cats@cacert.org"));
+        pk = kp.getPrivate();
+        c.issue(null, "2y", u).waitFor(60000);
+        ce = c.cert();
+    }
+
+    @Test
+    public void testLookupSerial() throws GigiApiException, IOException, GeneralSecurityException, InterruptedException {
+        Certificate target2 = new Certificate(u, u, Certificate.buildDN("EMAIL", u.getEmail()), Digest.SHA256, generatePEMCSR(generateKeypair(), "EMAIL=" + u.getEmail()), CSRType.CSR, CertificateProfile.getByName("client"), new Certificate.SubjectAlternateName(SANType.EMAIL, "cats@cacert.org"));
+        target2.issue(null, "2y", u).waitFor(60000);
+
+        assertEquals(u.getId(), Integer.parseInt(apiLookup(target2)));
+    }
+
+    @Test
+    public void testImportCATS() throws GigiApiException, IOException, GeneralSecurityException, InterruptedException {
+
+        assertEquals(1, u.getTrainings().length);
+        apiImport(u, "Test Training");
+        assertEquals(2, u.getTrainings().length);
+
+        User u2 = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "@example.com", TEST_PASSWORD));
+        assertEquals(0, u2.getTrainings().length);
+        assertFalse(u2.hasPassedCATS());
+        apiImport(u2, "Test Training");
+        assertEquals(1, u2.getTrainings().length);
+        assertFalse(u2.hasPassedCATS());
+        apiImport(u2, CATS.ASSURER_CHALLENGE_NAME);
+        assertEquals(2, u2.getTrainings().length);
+        assertTrue(u2.hasPassedCATS());
+
+    }
+
+    @Test
+    public void testImportCATSFailures() throws GigiApiException, IOException, GeneralSecurityException, InterruptedException {
+        assertEquals(1, u.getTrainings().length);
+        assertNotEquals(200, executeImportQuery("").getResponseCode());
+        assertNotEquals(200, executeImportQuery("mid=" + u.getId()).getResponseCode());
+        assertNotEquals(200, executeImportQuery("mid=" + u.getId() + "&variant=Test+Training").getResponseCode());
+        assertNotEquals(200, executeImportQuery("mid=" + u.getId() + "&variant=Test+Training&date=" + System.currentTimeMillis()).getResponseCode());
+        assertNotEquals(200, executeImportQuery("mid=" + u.getId() + "&variant=Test+Training&date=" + System.currentTimeMillis() + "&language=en").getResponseCode());
+        assertNotEquals(200, executeImportQuery("mid=" + u.getId() + "&variant=Test+Training&date=" + System.currentTimeMillis() + "&version=1.0").getResponseCode());
+        assertEquals(1, u.getTrainings().length);
+        apiImport(u, "Test Training");
+        assertEquals(2, u.getTrainings().length);
+
+    }
+
+    private void apiImport(User target, String test) throws IOException, MalformedURLException, NoSuchAlgorithmException, KeyManagementException, UnsupportedEncodingException, GeneralSecurityException {
+        HttpURLConnection connection = executeImportQuery("mid=" + target.getId() + "&variant=" + URLEncoder.encode(test, "UTF-8") + "&date=" + System.currentTimeMillis() + "&language=en&version=1.0");
+        if (connection.getResponseCode() != 200) {
+            throw new Error(connection.getResponseMessage());
+        }
+    }
+
+    private HttpURLConnection executeImportQuery(String query) throws IOException, MalformedURLException, NoSuchAlgorithmException, KeyManagementException, UnsupportedEncodingException, Error {
+        HttpURLConnection connection = (HttpURLConnection) new URL("https://" + getServerName().replaceFirst("^www.", "api.") + CATSImport.PATH).openConnection();
+        authenticateClientCert(pk, ce, connection);
+        connection.setDoOutput(true);
+        OutputStream os = connection.getOutputStream();
+        os.write(query.getBytes("UTF-8"));
+        return connection;
+    }
+
+    private String apiLookup(Certificate target) throws IOException, MalformedURLException, NoSuchAlgorithmException, KeyManagementException, UnsupportedEncodingException, GeneralSecurityException {
+        HttpURLConnection connection = (HttpURLConnection) new URL("https://" + getServerName().replaceFirst("^www.", "api.") + CATSResolve.PATH).openConnection();
+        authenticateClientCert(pk, ce, connection);
+        connection.setDoOutput(true);
+        OutputStream os = connection.getOutputStream();
+        os.write(("serial=" + target.cert().getSerialNumber().toString(16).toLowerCase()).getBytes());
+        if (connection.getResponseCode() != 200) {
+            throw new Error(connection.getResponseMessage());
+        }
+        return IOUtils.readURL(connection);
+    }
+}
diff --git a/tests/org/cacert/gigi/api/IssueCert.java b/tests/org/cacert/gigi/api/IssueCert.java
new file mode 100644 (file)
index 0000000..ca4b903
--- /dev/null
@@ -0,0 +1,129 @@
+package org.cacert.gigi.api;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.junit.Test;
+
+import sun.security.x509.X500Name;
+
+public class IssueCert extends ClientTest {
+
+    private final PrivateKey pk;
+
+    private final X509Certificate ce;
+
+    private final Certificate c;
+
+    private final KeyPair kp;
+
+    public IssueCert() {
+        try {
+            kp = generateKeypair();
+            String key1 = generatePEMCSR(kp, "EMAIL=testmail@example.com");
+            c = new Certificate(u, u, Certificate.buildDN("EMAIL", "testmail@example.com"), Digest.SHA256, key1, CSRType.CSR, CertificateProfile.getById(1));
+            pk = kp.getPrivate();
+            c.issue(null, "2y", u).waitFor(60000);
+            ce = c.cert();
+        } catch (Exception e) {
+            throw new Error(e);
+        }
+    }
+
+    @Test
+    public void testIssueCert() throws Exception {
+        String cert = issueCert(generatePEMCSR(kp, "EMAIL=" + email + ",CN=CAcert WoT User"), "profile=client");
+
+        CertificateFactory cf = CertificateFactory.getInstance("X509");
+        java.security.cert.X509Certificate xcert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert.getBytes("UTF-8")));
+        assertEquals("CAcert WoT User", ((X500Name) xcert.getSubjectDN()).getCommonName());
+
+    }
+
+    @Test
+    public void testRevoke() throws Exception {
+        revoke(c.getSerial().toLowerCase());
+        assertEquals(CertificateStatus.REVOKED, c.getStatus());
+    }
+
+    @Test
+    public void testIssueCertAssured() throws Exception {
+        makeAssurer(id);
+
+        Name n = u.getName();
+        String whishName = n.getFname() + " " + n.getLname();
+        String cert = issueCert(generatePEMCSR(kp, "EMAIL=" + email + ",CN=" + whishName), "profile=client-a");
+
+        CertificateFactory cf = CertificateFactory.getInstance("X509");
+        java.security.cert.X509Certificate xcert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert.getBytes("UTF-8")));
+        assertEquals(whishName, ((X500Name) xcert.getSubjectDN()).getCommonName());
+
+    }
+
+    @Test
+    public void testIssueOrgCert() throws Exception {
+        makeAssurer(id);
+        u.grantGroup(u, Group.ORGASSURER);
+
+        Organisation o1 = new Organisation("name", "st", "pr", "st", "test@mail", "", "", u);
+        o1.addAdmin(u, u, false);
+        String testdom = createUniqueName() + "-example.com";
+        Domain d2 = new Domain(u, o1, testdom);
+        verify(d2);
+
+        String whishName = createUniqueName();
+        String cert = issueCert(generatePEMCSR(kp, "EMAIL=test@" + testdom + ",CN=" + whishName), "profile=client-orga&asOrg=" + o1.getId());
+
+        CertificateFactory cf = CertificateFactory.getInstance("X509");
+        java.security.cert.X509Certificate xcert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert.getBytes("UTF-8")));
+        assertEquals(whishName, ((X500Name) xcert.getSubjectDN()).getCommonName());
+
+    }
+
+    private String issueCert(String csr, String options) throws IOException, GeneralSecurityException {
+        HttpURLConnection connection = (HttpURLConnection) new URL("https://" + getServerName().replaceFirst("^www.", "api.") + CreateCertificate.PATH).openConnection();
+        authenticateClientCert(pk, ce, connection);
+        connection.setDoOutput(true);
+        OutputStream os = connection.getOutputStream();
+        os.write((options + "&csr=" + URLEncoder.encode(csr, "UTF-8")).getBytes("UTF-8"));
+        os.flush();
+        assertEquals(connection.getResponseMessage(), 200, connection.getResponseCode());
+        String cert = IOUtils.readURL(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+        return cert;
+    }
+
+    private void revoke(String serial) throws IOException, GeneralSecurityException {
+        HttpURLConnection connection;
+        OutputStream os;
+        connection = (HttpURLConnection) new URL("https://" + getServerName().replaceFirst("^www.", "api.") + "/account/certs/revoke").openConnection();
+        authenticateClientCert(pk, ce, connection);
+        connection.setDoOutput(true);
+        os = connection.getOutputStream();
+        os.write(("serial=" + URLEncoder.encode(serial, "UTF-8")).getBytes("UTF-8"));
+        os.flush();
+        assertEquals(connection.getResponseCode(), 200);
+    }
+}
diff --git a/tests/org/cacert/gigi/crypto/TestSPKAC.java b/tests/org/cacert/gigi/crypto/TestSPKAC.java
new file mode 100644 (file)
index 0000000..3dd39a2
--- /dev/null
@@ -0,0 +1,72 @@
+package org.cacert.gigi.crypto;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.interfaces.RSAKey;
+import java.util.Base64;
+
+import org.cacert.gigi.testUtils.IOUtils;
+import org.junit.Test;
+
+import sun.security.x509.X509Key;
+
+public class TestSPKAC {
+
+    @Test
+    public void testParse() throws GeneralSecurityException, IOException {
+        String spkac = IOUtils.readURL(new InputStreamReader(TestSPKAC.class.getResourceAsStream("sampleSPKAC.txt"), "UTF-8"));
+        SPKAC parsed = new SPKAC(Base64.getDecoder().decode(spkac.replaceAll("[\r\n]", "")));
+        assertEquals("i am in the testcase", parsed.getChallenge());
+        RSAKey k = ((RSAKey) parsed.getPubkey());
+        assertEquals("a4004c2addf204fb26ce98f5963cc76def609ec0c50905e091fb84e986e3cb" + //
+                "0d5e14edb9cb8e10524350bd2351589284a4f631ddf9b87f04ea0e58f7d8d816b58" + //
+                "d052ce08b6576c04a7d45daf25b0ac9306f9cbb1f626e4ac301b7a4a3a062252b9a" + //
+                "472b2cde5ec803407b18879a59ccba7716016b1de4537a005b2bd0fd6071", k.getModulus().toString(16));
+    }
+
+    @Test
+    public void testAddData() throws GeneralSecurityException, IOException {
+        String spkac = IOUtils.readURL(new InputStreamReader(TestSPKAC.class.getResourceAsStream("sampleSPKAC.txt"), "UTF-8"));
+        byte[] data = Base64.getDecoder().decode(spkac.replaceAll("[\r\n]", ""));
+        byte[] tampered = new byte[data.length + 1];
+        System.arraycopy(data, 0, tampered, 0, data.length);
+        try {
+            new SPKAC(tampered);
+            fail("Expected illegal arg exception.");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        // change the last byte of the signature.
+        data[data.length - 1]--;
+        try {
+            new SPKAC(data);
+            fail("Expected SignatureException.");
+        } catch (SignatureException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testGen() throws GeneralSecurityException, IOException {
+        KeyPairGenerator pkg = KeyPairGenerator.getInstance("RSA");
+        pkg.initialize(1024);
+        KeyPair kp = pkg.generateKeyPair();
+
+        SPKAC s = new SPKAC((X509Key) kp.getPublic(), "this is a even bigger challange");
+        Signature sign = Signature.getInstance("SHA512withRSA");
+        sign.initSign(kp.getPrivate());
+
+        byte[] res = s.getEncoded(sign);
+        SPKAC parsed = new SPKAC(res);
+        assertEquals(s.getChallenge(), parsed.getChallenge());
+        assertEquals(s.getPubkey(), parsed.getPubkey());
+
+    }
+}
diff --git a/tests/org/cacert/gigi/crypto/sampleSPKAC.txt b/tests/org/cacert/gigi/crypto/sampleSPKAC.txt
new file mode 100644 (file)
index 0000000..b3f44b6
--- /dev/null
@@ -0,0 +1,8 @@
+MIIBTjCBuDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApABMKt3yBPsmzpj
+1ljzHbe9gnsDFCQXgkfuE6Ybjyw1eFO25y44QUkNQvSNRWJKEpPYx3fm4fwTqDl
+j32NgWtY0FLOCLZXbASn1F2vJbCskwb5y7H2JuSsMBt6SjoGIlK5pHKyzeXsgDQ
+HsYh5pZzLp3FgFrHeRTegBbK9D9YHECAwEAARYUaSBhbSBpbiB0aGUgdGVzdGNh
+c2UwDQYJKoZIhvcNAQEEBQADgYEAamIuLgSIgEfy5VSp+0R2wCtmxVvX9mIEhVg
+kbnNjC1bNJdtM3HPVlYbPRKuSaucgF4A/pPX55l6JECsIu6yGIyAszwGc1+rspg
+zEztsuU38IOl0sdxZBujyv2v1eoMB6TzBlsJ2hb/pC8XlmrCa5g6n1hI6XLgdu6
+3tyT4lBQ6E=
diff --git a/tests/org/cacert/gigi/email/TestEmailProviderClass.java b/tests/org/cacert/gigi/email/TestEmailProviderClass.java
new file mode 100644 (file)
index 0000000..3b38bc2
--- /dev/null
@@ -0,0 +1,74 @@
+package org.cacert.gigi.email;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Properties;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.cacert.gigi.testUtils.ConfiguredTest;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestEmailProviderClass extends ConfiguredTest {
+
+    @Test
+    public void testNonmail() throws IOException {
+        String result = EmailProvider.getInstance().checkEmailServer(0, "nomail");
+        assertNotEquals(EmailProvider.OK, result);
+    }
+
+    @Test
+    public void testFastcheckSucceed() throws IOException {
+        String succmail = getTestProps().getProperty("email.address");
+        assumeNotNull(succmail);
+
+        String result = EmailProvider.getInstance().checkEmailServer(0, succmail);
+        assertEquals(EmailProvider.OK, result);
+    }
+
+    @Test
+    public void testFastcheckFail() throws IOException {
+        String failmail = getTestProps().getProperty("email.non-address");
+        assumeNotNull(failmail);
+
+        String result = EmailProvider.getInstance().checkEmailServer(0, failmail);
+        assertNotEquals(EmailProvider.OK, result);
+    }
+
+    @BeforeClass
+    public static void initMailsystem() throws NoSuchAlgorithmException, KeyManagementException {
+        Properties prop = new Properties();
+        prop.setProperty("emailProvider", "org.cacert.gigi.email.Sendmail");
+        EmailProvider.initSystem(prop, null, null);
+        SSLContext c = SSLContext.getInstance("TLS");
+        c.init(null, new TrustManager[] {
+                new X509TrustManager() {
+
+                    @Override
+                    public X509Certificate[] getAcceptedIssuers() {
+                        return null;
+                    }
+
+                    @Override
+                    public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
+
+                }
+
+                    @Override
+                    public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
+
+                }
+                }
+        }, null);
+        SSLContext.setDefault(c);
+    }
+}
diff --git a/tests/org/cacert/gigi/email/TestSendmail.java b/tests/org/cacert/gigi/email/TestSendmail.java
new file mode 100644 (file)
index 0000000..3535abb
--- /dev/null
@@ -0,0 +1,130 @@
+package org.cacert.gigi.email;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Properties;
+import java.util.Random;
+
+import javax.net.ssl.SSLSocketFactory;
+
+import org.cacert.gigi.testUtils.ConfiguredTest;
+import org.junit.Test;
+
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.CertificateAlgorithmId;
+import sun.security.x509.CertificateSerialNumber;
+import sun.security.x509.CertificateValidity;
+import sun.security.x509.CertificateVersion;
+import sun.security.x509.CertificateX509Key;
+import sun.security.x509.X500Name;
+import sun.security.x509.X509CertImpl;
+import sun.security.x509.X509CertInfo;
+
+public class TestSendmail extends ConfiguredTest {
+
+    private static final Random rng = new Random();
+
+    @Test
+    public void testSendmail() throws IOException, GeneralSecurityException {
+        initSelfsign();
+
+        String succmail = getTestProps().getProperty("email.address");
+        String pass = getTestProps().getProperty("email.password");
+        String imap = getTestProps().getProperty("email.imap");
+        String imapuser = getTestProps().getProperty("email.imap.user");
+        assumeNotNull(succmail, pass, imap, imapuser);
+
+        String subj = "subj-" + createUniqueName();
+        String msg = "msg-" + createUniqueName();
+        EmailProvider.getInstance().sendmail(succmail, subj, msg, "system@cacert.org", "system@cacert.org", "Testtarget", "Testsender", null, false);
+
+        try (Socket s = SSLSocketFactory.getDefault().createSocket(imap, 993);//
+                PrintWriter pw = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), "UTF-8"), true);//
+                BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(), "UTF-8"))) {
+            pw.println("a001 login " + imapuser + " " + pass);
+            imapUntil(br, "a001");
+            pw.println("a002 select inbox");
+            String overview = imapUntil(br, "a002");
+            overview = overview.replaceFirst(".*\\* ([0-9]+) EXISTS.*", "$1");
+            int cont = Integer.parseInt(overview);
+
+            int msgid = -1;
+            for (int i = 1; i <= cont; i++) {
+                pw.println("m003" + i + " fetch " + i + " body[header]");
+                String body = imapUntil(br, "m003" + i);
+                if (body.contains(subj)) {
+                    msgid = i;
+                    break;
+                }
+            }
+            assertNotEquals( -1, msgid);
+            pw.println("a003 fetch " + msgid + " body[]");
+            String body = imapUntil(br, "a003");
+            pw.println("delete store " + msgid + " +flags \\deleted");
+            imapUntil(br, "delete");
+            pw.println("exp expunge");
+            imapUntil(br, "exp");
+            pw.println("log logout");
+            imapUntil(br, "log");
+            assertThat(body, containsString("From: support@cacert.local"));
+            assertThat(body, containsString("To: gigi-testuser@dogcraft.de"));
+            assertThat(body, containsString("Subject: " + subj));
+            assertThat(body, containsString(Base64.getEncoder().encodeToString(msg.getBytes("UTF-8"))));
+
+            // TODO maybe verify signature
+        }
+    }
+
+    private String imapUntil(BufferedReader br, String target) throws IOException {
+        StringBuffer response = new StringBuffer();
+        String line = "";
+        while ( !line.startsWith(target)) {
+            line = br.readLine();
+            if (line == null) {
+                throw new EOFException();
+            }
+            response.append(line);
+        }
+        return response.toString();
+    }
+
+    private void initSelfsign() throws GeneralSecurityException, CertificateException, IOException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
+        assumeNotNull(getTestProps().getProperty("emailProvider.smtpHost"), getTestProps().getProperty("emailProvider.smtpPort"));
+        Properties prop = new Properties();
+        prop.setProperty("emailProvider", "org.cacert.gigi.email.Sendmail");
+        prop.setProperty("emailProvider.smtpHost", getTestProps().getProperty("emailProvider.smtpHost"));
+        prop.setProperty("emailProvider.smtpPort", getTestProps().getProperty("emailProvider.smtpPort"));
+        KeyPair kp = generateKeypair();
+        X509CertInfo info = new X509CertInfo();
+        // Add all mandatory attributes
+        info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
+        info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(rng.nextInt() & 0x7fffffff));
+        AlgorithmId algID = AlgorithmId.get("SHA256WithRSA");
+        info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algID));
+        info.set(X509CertInfo.SUBJECT, new X500Name("EMAIL=system@cacert.org"));
+        info.set(X509CertInfo.KEY, new CertificateX509Key(kp.getPublic()));
+        info.set(X509CertInfo.VALIDITY, new CertificateValidity(new Date(System.currentTimeMillis()), new Date(System.currentTimeMillis() + 60 * 60 * 1000)));
+        info.set(X509CertInfo.ISSUER, new X500Name("CN=test-issue"));
+        X509CertImpl cert = new X509CertImpl(info);
+        cert.sign(kp.getPrivate(), "SHA256WithRSA");
+        EmailProvider.initSystem(prop, cert, kp.getPrivate());
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/account/TestCertificateAdd.java b/tests/org/cacert/gigi/pages/account/TestCertificateAdd.java
new file mode 100644 (file)
index 0000000..57a5e63
--- /dev/null
@@ -0,0 +1,364 @@
+package org.cacert.gigi.pages.account;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.crypto.SPKAC;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.pages.account.certs.CertificateAdd;
+import org.cacert.gigi.pages.account.certs.CertificateRequest;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.util.PEM;
+import org.junit.Test;
+
+import sun.security.pkcs.PKCS7;
+import sun.security.pkcs.PKCS9Attribute;
+import sun.security.pkcs10.PKCS10Attribute;
+import sun.security.pkcs10.PKCS10Attributes;
+import sun.security.util.ObjectIdentifier;
+import sun.security.x509.CertificateExtensions;
+import sun.security.x509.DNSName;
+import sun.security.x509.ExtendedKeyUsageExtension;
+import sun.security.x509.GeneralName;
+import sun.security.x509.GeneralNameInterface;
+import sun.security.x509.GeneralNames;
+import sun.security.x509.RFC822Name;
+import sun.security.x509.SubjectAlternativeNameExtension;
+import sun.security.x509.X509Key;
+
+public class TestCertificateAdd extends ClientTest {
+
+    KeyPair kp = generateKeypair();
+
+    String csrf;
+
+    public TestCertificateAdd() throws GeneralSecurityException, IOException {
+        TestDomain.addDomain(cookie, uniq + ".tld");
+
+    }
+
+    @Test
+    public void testSimpleServer() throws IOException, GeneralSecurityException {
+        PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
+                CertificateRequest.OID_KEY_USAGE_SSL_SERVER
+        }, new DNSName(uniq + ".tld"));
+
+        String pem = generatePEMCSR(kp, "CN=a." + uniq + ".tld", atts);
+
+        String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
+        assertArrayEquals(new String[] {
+                "server", "CAcert WoT User", "dns:a." + uniq + ".tld\ndns:" + uniq + ".tld\n", Digest.SHA256.toString()
+        }, res);
+    }
+
+    @Test
+    public void testSimpleMail() throws IOException, GeneralSecurityException {
+        PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
+                CertificateRequest.OID_KEY_USAGE_EMAIL_PROTECTION
+        }, new DNSName("a." + uniq + ".tld"), new DNSName("b." + uniq + ".tld"), new RFC822Name(email));
+
+        String pem = generatePEMCSR(kp, "CN=a b", atts, "SHA384WithRSA");
+
+        String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
+        assertArrayEquals(new String[] {
+                "mail", "a b", "email:" + email + "\ndns:a." + uniq + ".tld\ndns:b." + uniq + ".tld\n", Digest.SHA384.toString()
+        }, res);
+    }
+
+    @Test
+    public void testSimpleClient() throws IOException, GeneralSecurityException {
+        PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
+                CertificateRequest.OID_KEY_USAGE_SSL_CLIENT
+        }, new RFC822Name(email));
+
+        String pem = generatePEMCSR(kp, "CN=a b,email=" + email, atts, "SHA512WithRSA");
+
+        String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
+        assertArrayEquals(new String[] {
+                "client", "a b", "email:" + email + "\n", Digest.SHA512.toString()
+        }, res);
+    }
+
+    @Test
+    public void testSPKAC() throws GeneralSecurityException, IOException {
+        testSPKAC(false);
+        testSPKAC(true);
+    }
+
+    @Test
+    public void testIssue() throws IOException, GeneralSecurityException {
+        PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
+                CertificateRequest.OID_KEY_USAGE_SSL_CLIENT
+        }, new RFC822Name(email));
+
+        String pem = generatePEMCSR(kp, "CN=a b,email=" + email, atts, "SHA512WithRSA");
+
+        String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
+        assertArrayEquals(new String[] {
+                "client", "a b", "email:" + email + "\n", Digest.SHA512.toString()
+        }, res);
+
+        HttpURLConnection huc = (HttpURLConnection) ncert.openConnection();
+        huc.setRequestProperty("Cookie", cookie);
+        huc.setDoOutput(true);
+        OutputStream out = huc.getOutputStream();
+        out.write(("csrf=" + URLEncoder.encode(csrf, "UTF-8")).getBytes("UTF-8"));
+        out.write(("&CN=CAcert+WoT+User&profile=client&SANs=" + URLEncoder.encode("email:" + email + "\n", "UTF-8")).getBytes("UTF-8"));
+        out.write(("&hash_alg=SHA512&tos_agree=y").getBytes("UTF-8"));
+        URLConnection uc = authenticate(new URL(huc.getHeaderField("Location") + ".crt"));
+        String crt = IOUtils.readURL(new InputStreamReader(uc.getInputStream(), "UTF-8"));
+
+        uc = authenticate(new URL(huc.getHeaderField("Location") + ".cer"));
+        byte[] cer = IOUtils.readURL(uc.getInputStream());
+        assertArrayEquals(cer, PEM.decode("CERTIFICATE", crt));
+
+        uc = authenticate(new URL(huc.getHeaderField("Location") + ".cer?install&chain"));
+        byte[] pkcs7 = IOUtils.readURL(uc.getInputStream());
+        PKCS7 p7 = new PKCS7(pkcs7);
+        byte[] sub = verifyChain(p7.getCertificates());
+        assertArrayEquals(cer, sub);
+        assertEquals("application/x-x509-user-cert", uc.getHeaderField("Content-type"));
+
+        uc = authenticate(new URL(huc.getHeaderField("Location")));
+        String gui = IOUtils.readURL(uc);
+        Pattern p = Pattern.compile("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----");
+        Matcher m = p.matcher(gui);
+        assertTrue(m.find());
+        byte[] cert = PEM.decode("CERTIFICATE", m.group(0));
+        Certificate c = CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(cert));
+        gui = c.toString();
+        assertThat(gui, containsString("clientAuth"));
+        assertThat(gui, containsString("CN=CAcert WoT User"));
+        assertThat(gui, containsString("SHA512withRSA"));
+        assertThat(gui, containsString("RFC822Name: " + email));
+
+    }
+
+    private byte[] verifyChain(X509Certificate[] x509Certificates) throws GeneralSecurityException {
+        X509Certificate current = null;
+        nextCert:
+        while (true) {
+            for (int i = 0; i < x509Certificates.length; i++) {
+                X509Certificate cert = x509Certificates[i];
+                if (current == null) {
+                    if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
+                        current = cert;
+                        continue nextCert;
+                    }
+                } else {
+                    if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
+                        continue;
+                    }
+                    if (current.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
+                        Signature s = Signature.getInstance(cert.getSigAlgName());
+                        s.initVerify(current.getPublicKey());
+                        s.update(cert.getTBSCertificate());
+                        assertTrue(s.verify(cert.getSignature()));
+                        current = cert;
+                        continue nextCert;
+                    }
+                }
+            }
+            assertNotNull(current);
+            return current.getEncoded();
+        }
+    }
+
+    @Test
+    public void testValidityPeriodCalendar() throws IOException, GeneralSecurityException {
+        testCertificateValidityRelative(Calendar.YEAR, 2, "2y", true);
+        testCertificateValidityRelative(Calendar.YEAR, 1, "1y", true);
+        testCertificateValidityRelative(Calendar.MONTH, 3, "3m", true);
+        testCertificateValidityRelative(Calendar.MONTH, 7, "7m", true);
+        testCertificateValidityRelative(Calendar.MONTH, 13, "13m", true);
+
+        testCertificateValidityRelative(Calendar.MONTH, 13, "-1m", false);
+    }
+
+    @Test
+    public void testValidityPeriodWhishStart() throws IOException, GeneralSecurityException {
+        long now = System.currentTimeMillis();
+        final long MS_PER_DAY = 24 * 60 * 60 * 1000;
+        now -= now % MS_PER_DAY;
+        now += MS_PER_DAY;
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+        Date start = new Date(now);
+        Date end = new Date(now + MS_PER_DAY * 10);
+        String validity = "&validFrom=" + sdf.format(start) + "&validity=" + sdf.format(end);
+        X509Certificate res = createCertWithValidity(validity);
+        assertNotNull(validity, res);
+        assertEquals(start, res.getNotBefore());
+        assertEquals(end, res.getNotAfter());
+    }
+
+    private void testCertificateValidityRelative(int field, int amount, String length, boolean shouldsucceed) throws IOException, GeneralSecurityException, UnsupportedEncodingException, MalformedURLException, CertificateException {
+        X509Certificate parsed = createCertWithValidity("&validFrom=now&validity=" + length);
+        if (parsed == null) {
+            assertTrue( !shouldsucceed);
+            return;
+        } else {
+            assertTrue(shouldsucceed);
+        }
+
+        long now = System.currentTimeMillis();
+        Date start = parsed.getNotBefore();
+        Date end = parsed.getNotAfter();
+        Calendar c = Calendar.getInstance();
+        c.setTimeZone(TimeZone.getTimeZone("UTC"));
+        c.setTime(start);
+        c.add(field, amount);
+        assertTrue(Math.abs(start.getTime() - now) < 10000);
+        assertEquals(c.getTime(), end);
+    }
+
+    private X509Certificate createCertWithValidity(String validity) throws IOException, GeneralSecurityException, UnsupportedEncodingException, MalformedURLException, CertificateException {
+        PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
+                CertificateRequest.OID_KEY_USAGE_SSL_CLIENT
+        }, new RFC822Name(email));
+
+        String pem = generatePEMCSR(kp, "CN=a b", atts, "SHA512WithRSA");
+        fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
+
+        HttpURLConnection huc = (HttpURLConnection) ncert.openConnection();
+        huc.setRequestProperty("Cookie", cookie);
+        huc.setDoOutput(true);
+        OutputStream out = huc.getOutputStream();
+        out.write(("csrf=" + URLEncoder.encode(csrf, "UTF-8")).getBytes("UTF-8"));
+        out.write(("&profile=client&CN=" + CertificateRequest.DEFAULT_CN + "&SANs=" + URLEncoder.encode("email:" + email + "\n", "UTF-8")).getBytes("UTF-8"));
+        out.write(("&hash_alg=SHA512&tos_agree=y&").getBytes("UTF-8"));
+        out.write(validity.getBytes("UTF-8"));
+
+        String certurl = huc.getHeaderField("Location");
+        if (certurl == null) {
+            return null;
+        }
+        URLConnection uc = authenticate(new URL(certurl + ".crt"));
+        String crt = IOUtils.readURL(new InputStreamReader(uc.getInputStream(), "UTF-8"));
+
+        CertificateFactory cf = CertificateFactory.getInstance("X.509");
+        X509Certificate parsed = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(crt.getBytes("UTF-8")));
+        return parsed;
+    }
+
+    private URLConnection authenticate(URL url) throws IOException {
+        URLConnection uc = url.openConnection();
+        uc.setRequestProperty("Cookie", cookie);
+        return uc;
+    }
+
+    protected String testSPKAC(boolean correctChallange) throws GeneralSecurityException, IOException {
+        HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
+        uc.setRequestProperty("Cookie", cookie);
+        String s = IOUtils.readURL(uc);
+
+        csrf = extractPattern(s, Pattern.compile("<input [^>]*name='csrf' [^>]*value='([^']*)'>"));
+        String challenge = extractPattern(s, Pattern.compile("<keygen [^>]*name=\"SPKAC\" [^>]*challenge=\"([^\"]*)\"/>"));
+
+        SPKAC spk = new SPKAC((X509Key) kp.getPublic(), challenge + (correctChallange ? "" : "b"));
+        Signature sign = Signature.getInstance("SHA512WithRSA");
+        sign.initSign(kp.getPrivate());
+        try {
+            String[] res = fillOutFormDirect("SPKAC=" + URLEncoder.encode(Base64.getEncoder().encodeToString(spk.getEncoded(sign)), "UTF-8"));
+            if ( !correctChallange) {
+                fail("Should not succeed with wrong challange.");
+            }
+            assertArrayEquals(new String[] {
+                    "client", CertificateRequest.DEFAULT_CN, "", Digest.SHA512.toString()
+            }, res);
+        } catch (Error e) {
+            assertTrue(e.getMessage().startsWith("<div>Challenge mismatch"));
+        }
+        return csrf;
+    }
+
+    private PKCS10Attributes buildAtts(ObjectIdentifier[] ekuOIDs, GeneralNameInterface... SANs) throws IOException {
+        CertificateExtensions attributeValue = new CertificateExtensions();
+        GeneralNames names = new GeneralNames();
+
+        for (GeneralNameInterface name : SANs) {
+            names.add(new GeneralName(name));
+        }
+        attributeValue.set("SANs", new SubjectAlternativeNameExtension(names));
+        PKCS10Attributes atts = new PKCS10Attributes(new PKCS10Attribute[] {
+                new PKCS10Attribute(PKCS9Attribute.EXTENSION_REQUEST_OID, attributeValue)
+        });
+        ExtendedKeyUsageExtension eku = new ExtendedKeyUsageExtension(//
+                new Vector<>(Arrays.<ObjectIdentifier>asList(ekuOIDs)));
+        attributeValue.set("eku", eku);
+        return atts;
+    }
+
+    private final URL ncert = new URL("https://" + getServerName() + CertificateAdd.PATH);
+
+    private String[] fillOutForm(String pem) throws IOException {
+        HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
+        uc.setRequestProperty("Cookie", cookie);
+        csrf = getCSRF(uc);
+        return fillOutFormDirect(pem);
+
+    }
+
+    private String[] fillOutFormDirect(String pem) throws IOException {
+
+        HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
+        uc.setRequestProperty("Cookie", cookie);
+        uc.setDoOutput(true);
+        uc.getOutputStream().write(("csrf=" + URLEncoder.encode(csrf, "UTF-8") + "&" + pem).getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+
+        return extractFormData(uc);
+    }
+
+    private String[] extractFormData(HttpURLConnection uc) throws IOException, Error {
+        String result = IOUtils.readURL(uc);
+        if (result.contains("<div class='formError'>")) {
+            String s = fetchStartErrorMessage(result);
+            throw new Error(s);
+        }
+
+        String profileKey = extractPattern(result, Pattern.compile("<option value=\"([^\"]*)\" selected>"));
+        String resultingCN = extractPattern(result, Pattern.compile("<input [^>]*name='CN' [^>]*value='([^']*)'/>"));
+        String txt = extractPattern(result, Pattern.compile("<textarea [^>]*name='SANs' [^>]*>([^<]*)</textarea>"));
+        String md = extractPattern(result, Pattern.compile("<input type=\"radio\" [^>]*name=\"hash_alg\" value=\"([^\"]*)\" checked='checked'/>"));
+        return new String[] {
+                profileKey, resultingCN, txt, md
+        };
+    }
+
+    private String extractPattern(String result, Pattern p) {
+        Matcher m = p.matcher(result);
+        assertTrue(m.find());
+        String resultingCN = m.group(1);
+        return resultingCN;
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/account/TestCertificateRequest.java b/tests/org/cacert/gigi/pages/account/TestCertificateRequest.java
new file mode 100644 (file)
index 0000000..57d481d
--- /dev/null
@@ -0,0 +1,89 @@
+package org.cacert.gigi.pages.account;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.pages.account.certs.CertificateRequest;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.junit.Test;
+
+public class TestCertificateRequest extends ClientTest {
+
+    KeyPair kp = generateKeypair();
+
+    AuthorizationContext ac;
+
+    public TestCertificateRequest() throws GeneralSecurityException, IOException {
+        ac = new AuthorizationContext(u, u);
+        makeAssurer(u.getId());
+        grant(email, Group.CODESIGNING);
+
+    }
+
+    @Test
+    public void testIssuingOtherName() throws Exception {
+        try {
+            new CertificateRequest(ac, generatePEMCSR(kp, "CN=hansi")).draft();
+            fail();
+        } catch (GigiApiException e) {
+            assertThat(e.getMessage(), containsString("name you entered was invalid"));
+        }
+    }
+
+    @Test
+    public void testIssuingDefault() throws Exception {
+        new CertificateRequest(ac, generatePEMCSR(kp, "CN=" + CertificateRequest.DEFAULT_CN + ",EMAIL=" + email)).draft();
+    }
+
+    @Test
+    public void testIssuingRealName() throws Exception {
+        new CertificateRequest(ac, generatePEMCSR(kp, "CN=a b,EMAIL=" + email)).draft();
+    }
+
+    @Test
+    public void testIssuingModifiedName() throws Exception {
+        try {
+            new CertificateRequest(ac, generatePEMCSR(kp, "CN=a ab")).draft();
+            fail();
+        } catch (GigiApiException e) {
+            assertThat(e.getMessage(), containsString("name you entered was invalid"));
+        }
+
+    }
+
+    // TODO annotate that this depends on default config
+    @Test
+    public void testCodesignModifiedName() throws Exception {
+        try {
+            u.grantGroup(u, Group.CODESIGNING);
+            CertificateRequest cr = new CertificateRequest(ac, generatePEMCSR(kp, "CN=a ab"));
+            cr.update("name", "SHA512", "code-a", null, null, "email:" + email, null, null);
+            cr.draft();
+            fail();
+        } catch (GigiApiException e) {
+            assertThat(e.getMessage(), containsString("does not match the details"));
+        }
+
+    }
+
+    // TODO annotate that this depends on default config
+    @Test
+    public void testCodesignNoPermModifiedName() throws Exception {
+        try {
+            CertificateRequest cr = new CertificateRequest(ac, generatePEMCSR(kp, "CN=a ab"));
+            cr.update("name", "SHA512", "code-a", null, null, "email:" + email, null, null);
+            cr.draft();
+            fail();
+        } catch (GigiApiException e) {
+            assertThat(e.getMessage(), containsString("Certificate Profile is invalid."));
+        }
+
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/account/TestChangePassword.java b/tests/org/cacert/gigi/pages/account/TestChangePassword.java
new file mode 100644 (file)
index 0000000..9601842
--- /dev/null
@@ -0,0 +1,104 @@
+package org.cacert.gigi.pages.account;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.junit.Test;
+
+public class TestChangePassword extends ClientTest {
+
+    String path = ChangePasswordPage.PATH;
+
+    public TestChangePassword() throws IOException {
+        cookie = login(u.getEmail(), TEST_PASSWORD);
+        assertTrue(isLoggedin(cookie));
+    }
+
+    @Test
+    public void testChangePasswordInternal() throws IOException, GigiApiException {
+        try {
+            u.changePassword(TEST_PASSWORD + "wrong", TEST_PASSWORD + "v2");
+            fail("Password change must not succeed if old password is wrong.");
+        } catch (GigiApiException e) {
+            // expected
+        }
+        ;
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+        u.changePassword(TEST_PASSWORD, TEST_PASSWORD + "v2");
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+    }
+
+    @Test
+    public void testChangePasswordWeb() throws IOException {
+        String error = executeBasicWebInteraction(cookie, path,
+                "oldpassword=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") //
+                        + "&pword1=" + URLEncoder.encode(TEST_PASSWORD + "v2", "UTF-8")//
+                        + "&pword2=" + URLEncoder.encode(TEST_PASSWORD + "v2", "UTF-8"));
+        assertNull(error);
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+        assertFalse(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+
+    }
+
+    @Test
+    public void testChangePasswordWebOldWrong() throws IOException {
+        String error = executeBasicWebInteraction(cookie, path,
+                "oldpassword=a" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") //
+                        + "&pword1=" + URLEncoder.encode(TEST_PASSWORD + "v2", "UTF-8")//
+                        + "&pword2=" + URLEncoder.encode(TEST_PASSWORD + "v2", "UTF-8"));
+        assertNotNull(error);
+        assertFalse(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+
+    }
+
+    @Test
+    public void testChangePasswordWebNewWrong() throws IOException {
+        String error = executeBasicWebInteraction(cookie, path,
+                "oldpassword=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") //
+                        + "&pword1=" + URLEncoder.encode(TEST_PASSWORD + "v2", "UTF-8")//
+                        + "&pword2=a" + URLEncoder.encode(TEST_PASSWORD + "v2", "UTF-8"));
+        assertNotNull(error);
+        assertFalse(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+
+    }
+
+    @Test
+    public void testChangePasswordWebNewEasy() throws IOException {
+        String error = executeBasicWebInteraction(cookie, path, "oldpassword=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") //
+                + "&pword1=a&pword2=a");
+        assertNotNull(error);
+        assertFalse(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+
+    }
+
+    @Test
+    public void testChangePasswordWebMissingFields() throws IOException {
+        String np = URLEncoder.encode(TEST_PASSWORD + "v2", "UTF-8");
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+        String error = executeBasicWebInteraction(cookie, path, "oldpassword=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") //
+                + "&pword1=" + np);
+        assertNotNull(error);
+        assertFalse(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+
+        error = executeBasicWebInteraction(cookie, path, "oldpassword=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") //
+                + "&pword2=" + np);
+        assertNotNull(error);
+        assertFalse(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+
+        error = executeBasicWebInteraction(cookie, path, "pword1=" + np + "&pword2=" + np);
+        assertNotNull(error);
+        assertFalse(isLoggedin(login(u.getEmail(), TEST_PASSWORD + "v2")));
+        assertTrue(isLoggedin(login(u.getEmail(), TEST_PASSWORD)));
+
+    }
+
+}
diff --git a/tests/org/cacert/gigi/pages/account/TestDomain.java b/tests/org/cacert/gigi/pages/account/TestDomain.java
new file mode 100644 (file)
index 0000000..1a3a5a0
--- /dev/null
@@ -0,0 +1,25 @@
+package org.cacert.gigi.pages.account;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+
+import org.cacert.gigi.pages.account.domain.DomainOverview;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.junit.Test;
+
+public class TestDomain extends ClientTest {
+
+    public TestDomain() throws IOException {}
+
+    @Test
+    public void testAdd() throws IOException {
+        assertNull(addDomain(cookie, uniq + ".de"));
+        assertNotNull(addDomain(cookie, uniq + ".de"));
+    }
+
+    public static String addDomain(String session, String domain) throws IOException {
+        return executeBasicWebInteraction(session, DomainOverview.PATH, "adddomain&newdomain=" + URLEncoder.encode(domain, "UTF-8"), 1);
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/account/TestMailManagement.java b/tests/org/cacert/gigi/pages/account/TestMailManagement.java
new file mode 100644 (file)
index 0000000..bf73cd3
--- /dev/null
@@ -0,0 +1,161 @@
+package org.cacert.gigi.pages.account;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URLEncoder;
+import java.util.Locale;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.ObjectCache;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.account.mail.MailOverview;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.junit.Test;
+
+public class TestMailManagement extends ClientTest {
+
+    private String path = MailOverview.DEFAULT_PATH;
+
+    public TestMailManagement() throws IOException {
+        clearCaches(); // and reset rate limits
+        cookie = login(u.getEmail(), TEST_PASSWORD);
+        assertTrue(isLoggedin(cookie));
+    }
+
+    @Test
+    public void testMailAddInternal() throws InterruptedException, GigiApiException {
+        createVerifiedEmail(u);
+    }
+
+    @Test
+    public void testMailAddInternalFaulty() throws GigiApiException {
+        try {
+            new EmailAddress(u, "kurti ", Locale.ENGLISH);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Intended.
+        }
+    }
+
+    @Test
+    public void testMailAddWeb() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        String newMail = createUniqueName() + "uni@example.org";
+        assertNull(addMail(newMail));
+        assertTrue(existsEmail(newMail));
+    }
+
+    @Test
+    public void testMailAddWebFaulty() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        String newMail = createUniqueName() + "uniexample.org";
+        assertNotNull(addMail(newMail));
+        assertFalse(existsEmail(newMail));
+    }
+
+    @Test
+    public void testMailAddWebMultiple() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        String u = createUniqueName();
+        String newMail = u + "uni@eXample.org";
+        assertNull(addMail(newMail));
+        assertTrue(existsEmail(newMail.toLowerCase()));
+
+        String newMail2 = u + "uni@eXamPlE.org";
+        assertNotNull(addMail(newMail2));
+        assertTrue(existsEmail(newMail2.toLowerCase()));
+
+        String newMail3 = u + "-buni@eXamPlE.org";
+        assertNull(addMail(newMail3));
+        assertTrue(existsEmail(newMail.toLowerCase()));
+        assertTrue(existsEmail(newMail3.toLowerCase()));
+    }
+
+    private String addMail(String newMail) throws IOException, MalformedURLException, UnsupportedEncodingException {
+        return executeBasicWebInteraction(cookie, path, "addmail&newemail=" + URLEncoder.encode(newMail, "UTF-8"), 1);
+    }
+
+    private boolean existsEmail(String newMail) {
+        EmailAddress[] addrs = u.getEmails();
+        for (int i = 0; i < addrs.length; i++) {
+            if (addrs[i].getAddress().equals(newMail)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Test
+    public void testMailSetDefaultWeb() throws MalformedURLException, UnsupportedEncodingException, IOException, InterruptedException, GigiApiException {
+        EmailAddress addr = createVerifiedEmail(u);
+        assertNull(executeBasicWebInteraction(cookie, path, "default=" + addr.getId()));
+        ObjectCache.clearAllCaches();
+        assertEquals(User.getById(u.getId()).getEmail(), addr.getAddress());
+    }
+
+    @Test
+    public void testMailSetDefaultWebUnverified() throws MalformedURLException, UnsupportedEncodingException, IOException, InterruptedException, GigiApiException {
+        EmailAddress addr = new EmailAddress(u, createUniqueName() + "test@test.tld", Locale.ENGLISH);
+        assertNotNull(executeBasicWebInteraction(cookie, path, "default=" + addr.getId()));
+        assertNotEquals(User.getById(u.getId()).getEmail(), addr.getAddress());
+        getMailReciever().clearMails();
+    }
+
+    @Test
+    public void testMailSetDefaultWebInvalidID() throws MalformedURLException, UnsupportedEncodingException, IOException, InterruptedException, GigiApiException {
+        User u2 = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "uni@example.org", TEST_PASSWORD));
+        int id = -1;
+        EmailAddress[] emails = u2.getEmails();
+        for (int i = 0; i < emails.length; i++) {
+            if (emails[i].getAddress().equals(u2.getEmail())) {
+                id = emails[i].getId();
+            }
+        }
+        assertNotEquals(id, -1);
+        assertNotNull(executeBasicWebInteraction(cookie, path, "default=" + id));
+        assertNotEquals(User.getById(u.getId()).getEmail(), u2.getEmail());
+        getMailReciever().clearMails();
+    }
+
+    @Test
+    public void testMailDeleteWeb() throws InterruptedException, GigiApiException, MalformedURLException, UnsupportedEncodingException, IOException {
+        EmailAddress addr = createVerifiedEmail(u);
+        assertNull(executeBasicWebInteraction(cookie, path, "delete=" + addr.getId(), 0));
+        User u = User.getById(this.u.getId());
+        EmailAddress[] addresses = u.getEmails();
+        for (int i = 0; i < addresses.length; i++) {
+            assertNotEquals(addresses[i].getAddress(), addr.getAddress());
+        }
+    }
+
+    @Test
+    public void testMailDeleteWebMulti() throws InterruptedException, GigiApiException, MalformedURLException, UnsupportedEncodingException, IOException {
+        EmailAddress[] addr = new EmailAddress[] {
+                createVerifiedEmail(u), createVerifiedEmail(u)
+        };
+        assertNull(executeBasicWebInteraction(cookie, path, "delete=" + addr[0].getId(), 0));
+        assertNull(executeBasicWebInteraction(cookie, path, "delete=" + addr[1].getId(), 0));
+        User u = User.getById(this.u.getId());
+        EmailAddress[] addresses = u.getEmails();
+        for (int i = 0; i < addresses.length; i++) {
+            assertNotEquals(addresses[i].getAddress(), addr[0].getAddress());
+            assertNotEquals(addresses[i].getAddress(), addr[1].getAddress());
+        }
+    }
+
+    @Test
+    public void testMailDeleteWebFaulty() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        User u2 = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "uni@test.tld", TEST_PASSWORD));
+        EmailAddress em = u2.getEmails()[0];
+        assertNotNull(executeBasicWebInteraction(cookie, path, "delete=" + em.getId(), 0));
+        u2 = User.getById(u2.getId());
+        assertNotEquals(u2.getEmails().length, 0);
+    }
+
+    @Test
+    public void testMailDeleteWebPrimary() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        assertNotNull(executeBasicWebInteraction(cookie, path, "delete=" + u.getEmails()[0].getId(), 0));
+        assertNotEquals(u.getEmails().length, 0);
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/account/TestMyDetailsEdit.java b/tests/org/cacert/gigi/pages/account/TestMyDetailsEdit.java
new file mode 100644 (file)
index 0000000..e6361eb
--- /dev/null
@@ -0,0 +1,113 @@
+package org.cacert.gigi.pages.account;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.sql.Date;
+import java.util.Calendar;
+import java.util.TimeZone;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+public class TestMyDetailsEdit extends ManagedTest {
+
+    String email = createUniqueName() + "@e.de";
+
+    int id = createVerifiedUser("Kurti", "Hansel", email, TEST_PASSWORD);
+
+    String cookie = login(email, TEST_PASSWORD);
+
+    public TestMyDetailsEdit() throws IOException {}
+
+    @Test
+    public void testChangeFnameValid() throws IOException {
+        String newName = createUniqueName();
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "fname=" + newName + "&lname=Hansel&mname=&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        assertEquals(newName, u.getName().getFname());
+    }
+
+    @Test
+    public void testChangeLnameValid() throws IOException {
+        String newName = createUniqueName();
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "lname=" + newName + "&fname=Kurti&mname=&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        assertEquals(newName, u.getName().getLname());
+    }
+
+    @Test
+    public void testChangeMnameValid() throws IOException {
+        String newName = createUniqueName();
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "mname=" + newName + "&fname=Kurti&lname=Hansel&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        assertEquals(newName, u.getName().getMname());
+    }
+
+    @Test
+    public void testChangeSuffixValid() throws IOException {
+        String newName = createUniqueName();
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "mname=&fname=Kurti&lname=Hansel&suffix=" + newName + "&day=1&month=1&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        assertEquals(newName, u.getName().getSuffix());
+    }
+
+    @Test
+    public void testUnsetSuffix() throws IOException {
+        String newName = createUniqueName();
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "mname=&fname=Kurti&lname=Hansel&suffix=" + newName + "&day=1&month=1&year=2000&processDetails", 0));
+        clearCaches();
+        User u = User.getById(id);
+        assertEquals(newName, u.getName().getSuffix());
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "mname=&fname=Kurti&lname=Hansel&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        clearCaches();
+        u = User.getById(id);
+        assertEquals("", u.getName().getSuffix());
+    }
+
+    @Test
+    public void testUnsetFname() throws IOException {
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "fname=&lname=Hansel&mname=&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        assertEquals("", u.getName().getFname());
+
+    }
+
+    @Test
+    public void testUnsetLname() throws IOException {
+        assertNotNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "lname=&fname=Kurti&mname=&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        assertEquals("Hansel", u.getName().getLname());
+    }
+
+    @Test
+    public void testUnsetMname() throws IOException {
+        String newName = createUniqueName();
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "mname=" + newName + "&fname=Kurti&lname=Hansel&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        assertEquals(newName, u.getName().getMname());
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "lname=Hansel&fname=Kurti&mname=&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        clearCaches();
+        u = User.getById(id);
+        assertEquals("", u.getName().getMname());
+
+    }
+
+    @Test
+    public void testChangeDOBValid() throws IOException {
+        assertNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "lname=Hansel&fname=Kurti&mname=&suffix=&day=1&month=2&year=2000&processDetails", 0));
+        User u = User.getById(id);
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+        cal.set(Calendar.YEAR, 2000);
+        cal.set(Calendar.DAY_OF_MONTH, Calendar.FEBRUARY);
+        cal.set(Calendar.MONTH, 1);
+        Date d = new Date(cal.getTimeInMillis());
+        assertEquals(d.toString(), u.getDoB().toSQLDate().toString());
+    }
+
+    @Test
+    public void testChangeDOBInvalid() throws IOException {
+        assertNotNull(executeBasicWebInteraction(cookie, MyDetails.PATH, "lname=Hansel&fname=Kurti&mname=&suffix=&day=1&month=1&year=test&processDetails", 0));
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/account/TestPasswordResetExternal.java b/tests/org/cacert/gigi/pages/account/TestPasswordResetExternal.java
new file mode 100644 (file)
index 0000000..91456f5
--- /dev/null
@@ -0,0 +1,62 @@
+package org.cacert.gigi.pages.account;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.PasswordResetPage;
+import org.cacert.gigi.pages.wot.TestAssurance;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.TestEmailReceiver.TestMail;
+import org.cacert.gigi.util.RandomToken;
+import org.junit.Test;
+
+public class TestPasswordResetExternal extends ClientTest {
+
+    @Test
+    public void testByAssurance() throws IOException {
+        User u = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@example.com", TEST_PASSWORD));
+        String cookie2 = login(u.getEmail(), TEST_PASSWORD);
+        URLConnection uc = TestAssurance.buildupAssureFormConnection(cookie2, email, true);
+        String avalue = RandomToken.generateToken(32);
+        uc.getOutputStream().write(("date=1910-01-01&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10&passwordReset=1&passwordResetValue=" + URLEncoder.encode(avalue, "UTF-8")).getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+        String error = fetchStartErrorMessage(IOUtils.readURL(uc));
+        assertNull(error);
+
+        TestMail mail = getMailReciever().receive();
+        String link = mail.extractLink();
+        String npw = TEST_PASSWORD + "'";
+        System.out.println(link);
+        assertNotNull(toPasswordReset(avalue, link, npw, npw + "'"));
+        assertNotNull(toPasswordReset(avalue + "'", link, npw, npw));
+        assertNotNull(toPasswordReset(avalue, link, "a", "a"));
+        assertNull(toPasswordReset(avalue, link, npw, npw));
+        assertNotNull(login(email, npw));
+    }
+
+    private String toPasswordReset(String avalue, String link, String npw, String npw2) throws IOException, MalformedURLException, UnsupportedEncodingException {
+        URLConnection uc2 = new URL(link).openConnection();
+        String csrf = getCSRF(uc2);
+        String headerField = uc2.getHeaderField("Set-Cookie");
+        assertNotNull(headerField);
+        String cookie3 = stripCookie(headerField);
+        uc2 = new URL("https://" + getServerName() + PasswordResetPage.PATH).openConnection();
+        cookie(uc2, cookie3);
+        uc2.setDoOutput(true);
+        OutputStream o = uc2.getOutputStream();
+        o.write(("csrf=" + csrf + "&pword1=" + URLEncoder.encode(npw, "UTF-8") + "&pword2=" + URLEncoder.encode(npw2, "UTF-8") + "&private_token=" + URLEncoder.encode(avalue, "UTF-8")).getBytes("UTF-8"));
+        System.out.println(((HttpURLConnection) uc2).getResponseCode());
+        String readURL = IOUtils.readURL(uc2);
+        return fetchStartErrorMessage(readURL);
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/admin/TestSEAdminPageDetails.java b/tests/org/cacert/gigi/pages/admin/TestSEAdminPageDetails.java
new file mode 100644 (file)
index 0000000..d1f0139
--- /dev/null
@@ -0,0 +1,173 @@
+package org.cacert.gigi.pages.admin;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URLConnection;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.ObjectCache;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.account.History;
+import org.cacert.gigi.pages.account.MyDetails;
+import org.cacert.gigi.pages.admin.support.SupportEnterTicketPage;
+import org.cacert.gigi.pages.admin.support.SupportUserDetailsPage;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.junit.Test;
+
+public class TestSEAdminPageDetails extends ClientTest {
+
+    public TestSEAdminPageDetails() throws IOException {
+        grant(email, Group.SUPPORTER);
+        assertEquals(302, post(cookie, SupportEnterTicketPage.PATH, "ticketno=a20140808.8&setTicket=action", 0).getResponseCode());
+    }
+
+    @Test
+    public void testUserDetailsDisplay() throws MalformedURLException, IOException {
+        String email = createUniqueName() + "@example.com";
+        String fname = "Först";
+        String lname = "Secönd";
+        int id = createVerifiedUser(fname, lname, email, TEST_PASSWORD);
+        URLConnection uc = get(SupportUserDetailsPage.PATH + id);
+        uc.setDoOutput(true);
+        String res = IOUtils.readURL(uc);
+        assertThat(res, containsString("type=\"text\" value=\"" + fname + "\" name=\"fname\">"));
+        assertThat(res, containsString("type=\"text\" value=\"" + lname + "\" name=\"lname\">"));
+        assertThat(res, containsString(email));
+    }
+
+    @Test
+    public void testUserDetailsEmail() throws MalformedURLException, IOException, GigiApiException {
+        String email = createUniqueName() + "@example.com";
+        String fname = "Först";
+        String lname = "Secönd";
+        int id = createVerifiedUser(fname, lname, email, TEST_PASSWORD);
+        String email2 = createUniqueName() + "@example.com";
+        EmailAddress ea = new EmailAddress(User.getById(id), email2, Locale.ENGLISH);
+        getMailReciever().receive().verify();
+        // Refresh email Object
+        ObjectCache.clearAllCaches();
+        ea = EmailAddress.getById(ea.getId());
+        assertTrue(ea.isVerified());
+
+        String res = IOUtils.readURL(get(SupportUserDetailsPage.PATH + id));
+        assertEquals(2, countRegex(res, Pattern.quote(email)));
+        assertEquals(1, countRegex(res, Pattern.quote(email2)));
+
+        User.getById(id).updateDefaultEmail(ea);
+        clearCaches();
+        res = IOUtils.readURL(get(SupportUserDetailsPage.PATH + id));
+        assertEquals(1, countRegex(res, Pattern.quote(email)));
+        assertEquals(2, countRegex(res, Pattern.quote(email2)));
+    }
+
+    @Test
+    public void testUserDetailsEdit() throws MalformedURLException, IOException {
+        String email = createUniqueName() + "@example.com";
+        String fname = "Först";
+        String lname = "Secönd";
+        int id = createVerifiedUser(fname, lname, email, TEST_PASSWORD);
+
+        String userCookie = login(email, TEST_PASSWORD);
+        assertEquals("Först", getFname(IOUtils.readURL(get(userCookie, MyDetails.PATH))));
+        // User can change his name
+        assertNull(executeBasicWebInteraction(userCookie, MyDetails.PATH, "fname=Kurti&lname=Hansel&mname=&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        assertEquals("Kurti", getFname(IOUtils.readURL(get(userCookie, MyDetails.PATH))));
+        // But when assurer
+        makeAssurer(id);
+        // User cannot change his name, and the form changed
+        assertNotNull(executeBasicWebInteraction(userCookie, MyDetails.PATH, "fname=Kurti2&lname=Hansel&mname=&suffix=&day=1&month=1&year=2000&processDetails", 0));
+        assertNull(getFname(IOUtils.readURL(get(userCookie, MyDetails.PATH))));
+        assertEquals("Kurti", getFnamePlain(IOUtils.readURL(get(userCookie, MyDetails.PATH))));
+
+        // but support still can
+        assertNull(executeBasicWebInteraction(cookie, SupportUserDetailsPage.PATH + id, "fname=Kurti3&lname=Hansel&mname=&suffix=&dobd=1&dobm=2&doby=2000&detailupdate", 0));
+        assertEquals("Kurti3", getFnamePlain(IOUtils.readURL(get(userCookie, MyDetails.PATH))));
+
+    }
+
+    @Test
+    public void testUserDetailsEditToLog() throws MalformedURLException, IOException {
+        String email = createUniqueName() + "@example.com";
+        String fname = "Först";
+        String lname = "Secönd";
+        int id = createVerifiedUser(fname, lname, email, TEST_PASSWORD);
+        String clientCookie = login(email, TEST_PASSWORD);
+
+        assertEquals(0, logCountAdmin(id));
+        assertEquals(0, logCountUser(clientCookie));
+        // chaniging both leads to 2 entries
+        assertNull(executeBasicWebInteraction(cookie, SupportUserDetailsPage.PATH + id, "fname=Kurti&lname=Hansel&mname=&suffix=&dobd=1&dobm=2&doby=2000&detailupdate", 0));
+        assertEquals(2, logCountAdmin(id));
+        assertEquals(2, logCountUser(clientCookie));
+
+        // Sending same data keeps same
+        assertNull(executeBasicWebInteraction(cookie, SupportUserDetailsPage.PATH + id, "fname=Kurti&lname=Hansel&mname=&suffix=&dobd=1&dobm=2&doby=2000&detailupdate", 0));
+        assertEquals(2, logCountAdmin(id));
+        assertEquals(2, logCountUser(clientCookie));
+
+        // changing one leads to one entry
+        assertNull(executeBasicWebInteraction(cookie, SupportUserDetailsPage.PATH + id, "fname=Kurti2&lname=Hansel&mname=&suffix=&dobd=1&dobm=2&doby=2000&detailupdate", 0));
+        assertEquals(3, logCountAdmin(id));
+        assertEquals(3, logCountUser(clientCookie));
+
+        // changing one leads to one entry
+        assertNull(executeBasicWebInteraction(cookie, SupportUserDetailsPage.PATH + id, "fname=Kurti2&lname=Hansel&mname=&suffix=&dobd=2&dobm=2&doby=2000&detailupdate", 0));
+        assertEquals(4, logCountAdmin(id));
+        assertEquals(4, logCountUser(clientCookie));
+
+        // changing none -> no entry
+        assertNull(executeBasicWebInteraction(cookie, SupportUserDetailsPage.PATH + id, "fname=Kurti2&lname=Hansel&mname=&suffix=&dobd=2&dobm=2&doby=2000&detailupdate", 0));
+        assertEquals(4, logCountAdmin(id));
+        assertEquals(4, logCountUser(clientCookie));
+
+    }
+
+    private int logCountAdmin(int id) throws IOException {
+        return getLogEntryCount(IOUtils.readURL(get(History.SUPPORT_PATH.replace("*", Integer.toString(id)))));
+    }
+
+    private int logCountUser(String cookie) throws IOException {
+        return getLogEntryCount(IOUtils.readURL(get(cookie, History.PATH)));
+    }
+
+    private int getLogEntryCount(String readURL) {
+        String s = "<tr><th>Support actions";
+        int start = readURL.indexOf(s);
+        int end = readURL.indexOf("</table>", start);
+        String logs = readURL.substring(start + s.length(), end);
+        int i = 0;
+        int c = -1;
+        while (i != -1) {
+            i = logs.indexOf("<tr>", i + 1);
+            c++;
+        }
+        return c;
+    }
+
+    private String getFname(String res) {
+        Pattern p = Pattern.compile("type=\"text\" name=\"fname\" value=\"([^\"]*)\">");
+        Matcher m = p.matcher(res);
+        if (m.find()) {
+            return m.group(1);
+        }
+        return null;
+    }
+
+    private String getFnamePlain(String res) {
+        Pattern p = Pattern.compile("\\s*<td[^>]*>First Name: </td>\\s*<td[^>]*>([^<]*)</td>");
+        Matcher m = p.matcher(res);
+        if (m.find()) {
+            return m.group(1);
+        }
+        return null;
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/admin/TestSEAdminPageUserDomainSearch.java b/tests/org/cacert/gigi/pages/admin/TestSEAdminPageUserDomainSearch.java
new file mode 100644 (file)
index 0000000..4337e33
--- /dev/null
@@ -0,0 +1,77 @@
+package org.cacert.gigi.pages.admin;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.admin.support.FindDomainPage;
+import org.cacert.gigi.pages.admin.support.SupportEnterTicketPage;
+import org.cacert.gigi.pages.admin.support.SupportUserDetailsPage;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.util.ServerConstants;
+import org.junit.Test;
+
+public class TestSEAdminPageUserDomainSearch extends ClientTest {
+
+    public TestSEAdminPageUserDomainSearch() throws IOException {
+        grant(email, Group.SUPPORTER);
+        assertEquals(302, post(cookie, SupportEnterTicketPage.PATH, "ticketno=a20140808.8&setTicket=action", 0).getResponseCode());
+    }
+
+    @Test
+    public void testDomainSearch() throws MalformedURLException, UnsupportedEncodingException, IOException, GigiApiException {
+        String mail = createUniqueName() + "@example.com";
+        int id = createVerifiedUser("Först", "Secönd", mail, TEST_PASSWORD);
+        User user = User.getById(id);
+        String domainName = createUniqueName() + ".org";
+        new Domain(user, user, domainName);
+        URLConnection uc = post(FindDomainPage.PATH, "process&domain=" + URLEncoder.encode(domainName, "UTF-8"));
+
+        assertEquals("https://" + ServerConstants.getWwwHostNamePortSecure() + SupportUserDetailsPage.PATH + id, uc.getHeaderField("Location"));
+    }
+
+    @Test
+    public void testDomainSearchById() throws MalformedURLException, UnsupportedEncodingException, IOException, GigiApiException {
+        String mail = createUniqueName() + "@example.com";
+        int id = createVerifiedUser("Först", "Secönd", mail, TEST_PASSWORD);
+        User user = User.getById(id);
+        String domainName = createUniqueName() + ".org";
+        Domain d = new Domain(user, user, domainName);
+        URLConnection uc = post(FindDomainPage.PATH, "process&domain=#" + d.getId());
+        assertEquals("https://" + ServerConstants.getWwwHostNamePortSecure() + SupportUserDetailsPage.PATH + id, uc.getHeaderField("Location"));
+    }
+
+    @Test
+    public void testDomainSearchNonExist() throws MalformedURLException, UnsupportedEncodingException, IOException, GigiApiException {
+        URLConnection uc = post(FindDomainPage.PATH, "process&domain=" + URLEncoder.encode(createUniqueName() + ".de", "UTF-8"));
+        assertNotNull(fetchStartErrorMessage(IOUtils.readURL(uc)));
+    }
+
+    @Test
+    public void testDomainSearchByIdNonExist() throws MalformedURLException, UnsupportedEncodingException, IOException, GigiApiException {
+        int id = (int) (Math.random() * 10000);
+        int count = 0;
+        boolean found = false;
+        try {
+            while (Domain.getById(id) != null && count < 20) {
+                count++;
+                id = (int) (Math.random() * 10000);
+            }
+        } catch (Exception e) {
+            found = true;
+        }
+        assumeTrue(found);
+        URLConnection uc = post(FindDomainPage.PATH, "process&domain=#" + id);
+        assertNotNull(fetchStartErrorMessage(IOUtils.readURL(uc)));
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/admin/TestSEAdminPageUserMailSearch.java b/tests/org/cacert/gigi/pages/admin/TestSEAdminPageUserMailSearch.java
new file mode 100644 (file)
index 0000000..70e5bd4
--- /dev/null
@@ -0,0 +1,85 @@
+package org.cacert.gigi.pages.admin;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.pages.admin.support.FindUserPage;
+import org.cacert.gigi.pages.admin.support.SupportEnterTicketPage;
+import org.cacert.gigi.pages.admin.support.SupportUserDetailsPage;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.util.ServerConstants;
+import org.junit.Test;
+
+public class TestSEAdminPageUserMailSearch extends ClientTest {
+
+    public TestSEAdminPageUserMailSearch() throws IOException {
+        grant(email, Group.SUPPORTER);
+        assertEquals(302, post(cookie, SupportEnterTicketPage.PATH, "ticketno=a20140808.8&setTicket=action", 0).getResponseCode());
+    }
+
+    @Test
+    public void testFulltextMailSearch() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        String mail = createUniqueName() + "@example.com";
+        int id = createVerifiedUser("Först", "Secönd", mail, TEST_PASSWORD);
+
+        URLConnection uc = post(cookie, FindUserPage.PATH, "process&email=" + URLEncoder.encode(mail, "UTF-8"), 0);
+        assertEquals("https://" + ServerConstants.getWwwHostNamePortSecure() + SupportUserDetailsPage.PATH + id, uc.getHeaderField("Location"));
+    }
+
+    @Test
+    public void testWildcardMailSearchSingle() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        String mail = createUniqueName() + "@example.tld";
+        int id = createVerifiedUser("Först", "Secönd", mail, TEST_PASSWORD);
+
+        URLConnection uc = post(cookie, FindUserPage.PATH, "process&email=" + URLEncoder.encode("%@example.tld", "UTF-8"), 0);
+        assertEquals("https://" + ServerConstants.getWwwHostNamePortSecure() + SupportUserDetailsPage.PATH + id, uc.getHeaderField("Location"));
+    }
+
+    @Test
+    public void testWildcardMailSearchMultiple() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        String mail = createUniqueName() + "@example.org";
+        int id = createVerifiedUser("Först", "Secönd", mail, TEST_PASSWORD);
+        String mail2 = createUniqueName() + "@example.org";
+        int id2 = createVerifiedUser("Först", "Secönd", mail2, TEST_PASSWORD);
+        URLConnection uc = post(cookie, FindUserPage.PATH, "process&email=" + URLEncoder.encode("%@example.org", "UTF-8"), 0);
+
+        String res = IOUtils.readURL(uc);
+        assertThat(res, containsString(SupportUserDetailsPage.PATH + id));
+        assertThat(res, containsString(SupportUserDetailsPage.PATH + id2));
+    }
+
+    @Test
+    public void testWildcardMailSearchSingleChar() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        String mail = createUniqueName() + "@example.org";
+        int id = createVerifiedUser("Först", "Secönd", mail, TEST_PASSWORD);
+        String mail2 = createUniqueName() + "@fxample.org";
+        int id2 = createVerifiedUser("Först", "Secönd", mail2, TEST_PASSWORD);
+
+        URLConnection uc = post(cookie, FindUserPage.PATH, "process&email=" + URLEncoder.encode("%@_xample.org", "UTF-8"), 0);
+
+        String res = IOUtils.readURL(uc);
+        assertThat(res, containsString(SupportUserDetailsPage.PATH + id));
+        assertThat(res, containsString(SupportUserDetailsPage.PATH + id2));
+    }
+
+    @Test
+    public void testWildcardMailSearchNoRes() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        URLConnection uc = post(FindUserPage.PATH, "process&email=" + URLEncoder.encode("%@_humpfelkumpf.org", "UTF-8"));
+        assertNotNull(fetchStartErrorMessage(IOUtils.readURL(uc)));
+    }
+
+    @Test
+    public void testFulltextMailSearchNoRes() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        URLConnection uc = post(cookie, FindUserPage.PATH, "process&email=" + URLEncoder.encode(createUniqueName() + "@example.org", "UTF-8"), 0);
+
+        assertNotNull(fetchStartErrorMessage(IOUtils.readURL(uc)));
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/admin/TestSEAdminTicketSetting.java b/tests/org/cacert/gigi/pages/admin/TestSEAdminTicketSetting.java
new file mode 100644 (file)
index 0000000..c4c1a30
--- /dev/null
@@ -0,0 +1,32 @@
+package org.cacert.gigi.pages.admin;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.pages.admin.support.FindDomainPage;
+import org.cacert.gigi.pages.admin.support.FindUserPage;
+import org.cacert.gigi.pages.admin.support.SupportEnterTicketPage;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.junit.Test;
+
+public class TestSEAdminTicketSetting extends ClientTest {
+
+    public TestSEAdminTicketSetting() throws IOException {
+        grant(email, Group.SUPPORTER);
+    }
+
+    @Test
+    public void testFulltextMailSearch() throws MalformedURLException, UnsupportedEncodingException, IOException {
+        assertEquals(403, get(FindUserPage.PATH).getResponseCode());
+        assertEquals(302, post(cookie, SupportEnterTicketPage.PATH, "ticketno=a20140808.8&setTicket=action", 0).getResponseCode());
+        assertEquals(200, get(FindUserPage.PATH).getResponseCode());
+        assertEquals(200, get(FindDomainPage.PATH).getResponseCode());
+        assertEquals(302, post(cookie, SupportEnterTicketPage.PATH, "ticketno=a20140808.8&deleteTicket=action", 0).getResponseCode());
+        assertEquals(403, get(FindUserPage.PATH).getResponseCode());
+    }
+
+}
index 06787248d311a6f3639691a4e7c2726dda397bd7..7286f8a9beba78c4c42fe7b1e0338eac316b5aef 100644 (file)
 package org.cacert.gigi.pages.main;
 
+import static org.hamcrest.CoreMatchers.*;
 import static org.junit.Assert.*;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.net.HttpURLConnection;
+import java.net.URLEncoder;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
 import java.util.regex.Pattern;
 
+import org.cacert.gigi.dbObjects.User;
 import org.cacert.gigi.testUtils.InitTruststore;
 import org.cacert.gigi.testUtils.ManagedTest;
-import org.cacert.gigi.testUtils.TestEmailReciever.TestMail;
 import org.junit.Before;
 import org.junit.Test;
 
 public class RegisterPageTest extends ManagedTest {
-       static {
-               InitTruststore.run();
-               HttpURLConnection.setFollowRedirects(false);
-       }
-
-       @Before
-       public void setUp() throws Exception {
-       }
-       @Test
-       public void testSuccess() throws IOException {
-               long uniq = System.currentTimeMillis();
-               registerUser("ab", "b", "correct" + uniq + "@email.de", "ap12UI.'");
-               TestMail tm = waitForMail();
-               String link = tm.extractLink();
-               assertTrue(link, link.startsWith("http://"));
-       }
-       @Test
-       public void testNoFname() throws IOException {
-               testFailedForm("lname=b&email=e&pword1=ap&pword2=ap&day=1&month=1&year=1910&cca_agree=1");
-       }
-       @Test
-       public void testNoLname() throws IOException {
-               testFailedForm("fname=a&email=e&pword1=ap&pword2=ap&day=1&month=1&year=1910&cca_agree=1");
-       }
-       @Test
-       public void testNoEmail() throws IOException {
-               testFailedForm("fname=a&lname=b&pword1=ap&pword2=ap&day=1&month=1&year=1910&cca_agree=1");
-       }
-
-       @Test
-       public void testNoPword() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword2=ap&day=1&month=1&year=1910&cca_agree=1");
-       }
-
-       @Test
-       public void testDiffPword() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap2&day=1&month=1&year=1910&cca_agree=1");
-       }
-
-       @Test
-       public void testNoDay() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&month=1&year=1910&cca_agree=1");
-       }
-       @Test
-       public void testNoMonth() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&year=1910&cca_agree=1");
-       }
-       @Test
-       public void testNoYear() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=1&cca_agree=1");
-       }
-       @Test
-       public void testInvDay() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=40&month=1&year=1910&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=0&month=1&year=1910&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=a&month=1&year=1910&cca_agree=1");
-       }
-       @Test
-       public void testInvMonth() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=20&year=1910&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=0&year=1910&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=-1&year=1910&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=a&year=1910&cca_agree=1");
-       }
-       @Test
-       public void testInvYear() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=1&year=0&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=1&year=100&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=1&year=a&cca_agree=1");
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=1&year=-1&cca_agree=1");
-       }
-       @Test
-       public void testNoAgree() throws IOException {
-               testFailedForm("fname=a&lname=b&email=e&pword1=ap&pword2=ap&day=1&month=1&year=1910&cca_agree=a");
-       }
-
-       @Test
-       public void testDataStays() throws IOException {
-               long uniq = System.currentTimeMillis();
-               String run = runRegister("fname=fn" + uniq + "&lname=ln" + uniq
-                               + "&email=ma" + uniq + "@cacert.org&pword1=pas" + uniq
-                               + "&pword2=pas2" + uniq + "&day=1&month=1&year=0");
-               assertTrue(run.contains("fn" + uniq));
-               assertTrue(run.contains("ln" + uniq));
-               assertTrue(run.contains("ma" + uniq + "@cacert.org"));
-               assertTrue(!run.contains("pas" + uniq));
-               assertTrue(!run.contains("pas2" + uniq));
-
-       }
-
-       @Test
-       public void testCheckboxesStay() throws IOException {
-               String run2 = runRegister("general=1&country=a&regional=1&radius=0");
-               assertTrue(run2
-                               .contains("name=\"general\" value=\"1\" checked=\"checked\">"));
-               assertTrue(run2.contains("name=\"country\" value=\"1\">"));
-               assertTrue(run2
-                               .contains("name=\"regional\" value=\"1\" checked=\"checked\">"));
-               assertTrue(run2.contains("name=\"radius\" value=\"1\">"));
-               run2 = runRegister("general=0&country=1&radius=1");
-               assertTrue(run2.contains("name=\"general\" value=\"1\">"));
-               assertTrue(run2
-                               .contains("name=\"country\" value=\"1\" checked=\"checked\">"));
-               assertTrue(run2.contains("name=\"regional\" value=\"1\">"));
-               assertTrue(run2
-                               .contains("name=\"radius\" value=\"1\" checked=\"checked\">"));
-       }
-
-       @Test
-       public void testDoubleMail() throws IOException {
-               long uniq = System.currentTimeMillis();
-               registerUser("RegisterTest", "User", "testmail" + uniq + "@cacert.org",
-                               "registerPW'1");
-               try {
-                       registerUser("RegisterTest", "User", "testmail" + uniq
-                                       + "@cacert.org", "registerPW");
-                       throw new Error(
-                                       "Registering a user with the same email needs to fail.");
-               } catch (AssertionError e) {
-
-               }
-       }
-       @Test
-       public void testInvalidMailbox() {
-               getMailReciever().setApproveRegex(Pattern.compile("a"));
-               long uniq = System.currentTimeMillis();
-               try {
-                       registerUser("RegisterTest", "User", "testInvalidMailbox" + uniq
-                                       + "@cacert.org", "registerPW");
-                       throw new Error(
-                                       "Registering a user with invalid mailbox must fail.");
-               } catch (AssertionError e) {
-
-               }
-       }
-       private void testFailedForm(String query) throws IOException {
-               String startError = fetchStartErrorMessage(runRegister(query));
-               assertTrue(startError, !startError.startsWith("</div>"));
-       }
+
+    static {
+        InitTruststore.run();
+        HttpURLConnection.setFollowRedirects(false);
+        try {
+            p = "&pword1=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&pword2=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new Error(e);
+        }
+    }
+
+    public static final String p;
+
+    @Before
+    public void setUp() throws Exception {}
+
+    private static String createBase() {
+        return createUniqueName() + "@email.de";
+    }
+
+    @Test
+    public void testSuccess() throws IOException, InterruptedException {
+        long uniq = System.currentTimeMillis();
+        registerUser("ab", "b", "correct" + uniq + "@email.de", TEST_PASSWORD);
+        assertSuccessfullRegMail();
+
+        String defaultSignup = "fname=" + URLEncoder.encode("ab", "UTF-8") + "&lname=" + URLEncoder.encode("b", "UTF-8") + "&pword1=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&pword2=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&day=1&month=1&year=1910&tos_agree=1&mname=mn&suffix=sf&email=";
+
+        String query = defaultSignup + URLEncoder.encode("correct3_" + uniq + "@email.de", "UTF-8") + "&general=1&country=1&regional=1&radius=1";
+        String data = fetchStartErrorMessage(runRegister(query));
+        assertNull(data);
+        assertSuccessfullRegMail();
+
+        getMailReciever().setEmailCheckError("400 Greylisted");
+        getMailReciever().setApproveRegex(Pattern.compile("a"));
+        query = defaultSignup + URLEncoder.encode("correct4_" + uniq + "@email.de", "UTF-8") + "&general=1&country=1&regional=1&radius=1";
+        data = fetchStartErrorMessage(runRegister(query));
+        assertNotNull(data);
+
+        assertNull(getMailReciever().poll());
+
+    }
+
+    private void assertSuccessfullRegMail() {
+        String link = getMailReciever().receive().extractLink();
+        assertTrue(link, link.startsWith("https://"));
+    }
+
+    @Test
+    public void testNoFname() throws IOException {
+        testFailedForm("lname=b" + createBase() + "&day=1&month=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testNoLname() throws IOException {
+        testFailedForm("fname=a" + createBase() + "&day=1&month=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testNoEmail() throws IOException {
+        testFailedForm("fname=a&lname=b&pword1=ap&pword2=ap&day=1&month=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testNoPword() throws IOException {
+        testFailedForm("fname=a&lname=b&email=e&pword2=ap&day=1&month=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testDiffPword() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "2&day=1&month=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testNoDay() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "&month=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testNoMonth() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testNoYear() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=1&tos_agree=1");
+    }
+
+    @Test
+    public void testInvDay() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=40&month=1&year=1910&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=0&month=1&year=1910&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=-1&month=1&year=1910&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=a&month=1&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testInvMonth() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=20&year=1910&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=0&year=1910&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=-1&year=1910&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=a&year=1910&tos_agree=1");
+    }
+
+    @Test
+    public void testInvYear() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=1&year=0&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=1&year=100&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=1&year=a&tos_agree=1");
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=1&year=-1&tos_agree=1");
+    }
+
+    @Test
+    public void testNoAgree() throws IOException {
+        testFailedForm("fname=a&lname=b" + createBase() + "&day=1&month=1&year=1910&tos_agree=a");
+    }
+
+    @Test
+    public void testTooYoung() throws IOException {
+        Calendar c = GregorianCalendar.getInstance();
+        c.add(Calendar.YEAR, -User.MINIMUM_AGE + 2);
+        testFailedForm("fname=a&lname=b&email=" + createUniqueName() + "@email.de" + p + "&day=1&month=1&year=" + c.get(Calendar.YEAR) + "&tos_agree=1");
+    }
+
+    @Test
+    public void testDataStays() throws IOException {
+        long uniq = System.currentTimeMillis();
+        String run = runRegister("fname=fn" + uniq + "&lname=ln" + uniq + "&email=ma" + uniq + "@cacert.org&pword1=pas" + uniq + "&pword2=pas2" + uniq + "&day=1&month=1&year=0");
+        assertThat(run, containsString("fn" + uniq));
+        assertThat(run, containsString("ln" + uniq));
+        assertThat(run, containsString("ma" + uniq + "@cacert.org"));
+        assertThat(run, not(containsString("pas" + uniq)));
+        assertThat(run, not(containsString("pas2" + uniq)));
+
+    }
+
+    @Test
+    public void testCheckboxesStay() throws IOException {
+        String run2 = runRegister("general=1&country=a&regional=1&radius=0");
+        assertThat(run2, containsString("name=\"general\" value=\"1\" checked=\"checked\">"));
+        assertThat(run2, containsString("name=\"country\" value=\"1\">"));
+        assertThat(run2, containsString("name=\"regional\" value=\"1\" checked=\"checked\">"));
+        assertThat(run2, containsString("name=\"radius\" value=\"1\">"));
+        run2 = runRegister("general=0&country=1&radius=1");
+        assertThat(run2, containsString("name=\"general\" value=\"1\">"));
+        assertThat(run2, containsString("name=\"country\" value=\"1\" checked=\"checked\">"));
+        assertThat(run2, containsString("name=\"regional\" value=\"1\">"));
+        assertThat(run2, containsString("name=\"radius\" value=\"1\" checked=\"checked\">"));
+    }
+
+    @Test
+    public void testDoubleMail() throws IOException {
+        long uniq = System.currentTimeMillis();
+        registerUser("RegisterTest", "User", "testmail" + uniq + "@cacert.org", TEST_PASSWORD);
+        try {
+            registerUser("RegisterTest", "User", "testmail" + uniq + "@cacert.org", TEST_PASSWORD);
+            throw new Error("Registering a user with the same email needs to fail.");
+        } catch (AssertionError e) {
+
+        }
+    }
+
+    @Test
+    public void testInvalidMailbox() {
+        getMailReciever().setApproveRegex(Pattern.compile("a"));
+        long uniq = System.currentTimeMillis();
+        try {
+            registerUser("RegisterTest", "User", "testInvalidMailbox" + uniq + "@cacert.org", TEST_PASSWORD);
+            throw new Error("Registering a user with invalid mailbox must fail.");
+        } catch (AssertionError e) {
+
+        }
+    }
+
+    private void testFailedForm(String query) throws IOException {
+        String startError = fetchStartErrorMessage(runRegister(query));
+        assertTrue(startError, !startError.startsWith("</div>"));
+    }
 
 }
diff --git a/tests/org/cacert/gigi/pages/orga/TestOrgDomain.java b/tests/org/cacert/gigi/pages/orga/TestOrgDomain.java
new file mode 100644 (file)
index 0000000..d1b930d
--- /dev/null
@@ -0,0 +1,88 @@
+package org.cacert.gigi.pages.orga;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.testUtils.OrgTest;
+import org.junit.Test;
+
+public class TestOrgDomain extends OrgTest {
+
+    public TestOrgDomain() throws IOException {
+
+    }
+
+    @Test
+    public void testAdd() throws IOException, GigiApiException {
+        Organisation o1 = createUniqueOrg();
+        String dom = createUniqueName() + ".de";
+        assertNull(executeBasicWebInteraction(cookie, ViewOrgPage.DEFAULT_PATH + "/" + o1.getId(), "addDomain&domain=" + URLEncoder.encode(dom, "UTF-8"), 3));
+        Domain[] d = o1.getDomains();
+        assertEquals(1, d.length);
+        assertEquals(dom, d[0].getSuffix());
+    }
+
+    @Test
+    public void testDel() throws IOException, GigiApiException {
+        Organisation o1 = createUniqueOrg();
+        String dom = createUniqueName() + ".de";
+        Domain d = new Domain(u, o1, dom);
+        assertEquals(1, o1.getDomains().length);
+        assertNull(executeBasicWebInteraction(cookie, ViewOrgPage.DEFAULT_PATH + "/" + o1.getId(), "delete=" + d.getId(), 2));
+        assertEquals(0, o1.getDomains().length);
+    }
+
+    @Test
+    public void testBusinessAddWhileUser() throws IOException, GigiApiException {
+        Organisation o1 = createUniqueOrg();
+        String dom = createUniqueName() + ".de";
+        new Domain(u, u, dom);
+        try {
+            new Domain(u, o1, dom);
+            fail("Was able to add domain twice.");
+        } catch (GigiApiException e) {
+            assertEquals("Domain could not be inserted. Domain is already known to the system.", e.getMessage());
+            // expected
+        }
+        assertEquals(0, o1.getDomains().length);
+        assertEquals(1, u.getDomains().length);
+    }
+
+    @Test
+    public void testBusinessAddWhileOtherOrg() throws IOException, GigiApiException {
+        Organisation o1 = createUniqueOrg();
+        Organisation o2 = createUniqueOrg();
+
+        String dom = createUniqueName() + ".de";
+        new Domain(u, o1, dom);
+        try {
+            new Domain(u, o2, dom);
+            fail("Was able to add domain twice.");
+        } catch (GigiApiException e) {
+            assertEquals("Domain could not be inserted. Domain is already known to the system.", e.getMessage());
+            // expected
+        }
+        assertEquals(1, o1.getDomains().length);
+        assertEquals(0, o2.getDomains().length);
+        assertEquals(0, u.getDomains().length);
+    }
+
+    @Test
+    public void testBusinessAddInvalid() throws IOException, GigiApiException {
+        Organisation o1 = createUniqueOrg();
+        String dom = createUniqueName() + ".invalid-tld";
+        try {
+            new Domain(u, o1, dom);
+            fail("Was able to add invalid domain.");
+        } catch (GigiApiException e) {
+            // expected
+        }
+        assertEquals(0, o1.getDomains().length);
+        assertEquals(0, u.getDomains().length);
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/orga/TestOrgManagement.java b/tests/org/cacert/gigi/pages/orga/TestOrgManagement.java
new file mode 100644 (file)
index 0000000..80db6bd
--- /dev/null
@@ -0,0 +1,163 @@
+package org.cacert.gigi.pages.orga;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Organisation;
+import org.cacert.gigi.dbObjects.Organisation.Affiliation;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.account.MyDetails;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.OrgTest;
+import org.junit.After;
+import org.junit.Test;
+
+public class TestOrgManagement extends OrgTest {
+
+    public TestOrgManagement() throws IOException {
+
+    }
+
+    @After
+    public void purgeDbAfterTest() throws SQLException, IOException {
+        purgeDatabase();
+    }
+
+    @Test
+    public void testAdd() throws IOException {
+        for (Organisation i : Organisation.getOrganisations(0, 30)) {
+            i.delete();
+        }
+        executeBasicWebInteraction(cookie, CreateOrgPage.DEFAULT_PATH, "action=new&O=name&contact=mail&L=K%C3%B6ln&ST=" + URLEncoder.encode(DIFFICULT_CHARS, "UTF-8") + "&C=DE&comments=jkl%C3%B6loiuzfdfgjlh%C3%B6&optionalName=opname&postalAddress=postaladdress", 0);
+        Organisation[] orgs = Organisation.getOrganisations(0, 30);
+        assertEquals(1, orgs.length);
+        assertEquals("mail", orgs[0].getContactEmail());
+        assertEquals("name", orgs[0].getName());
+        assertEquals("Köln", orgs[0].getCity());
+        assertEquals(DIFFICULT_CHARS, orgs[0].getProvince());
+        assertEquals("opname", orgs[0].getOptionalName());
+        assertEquals("postaladdress", orgs[0].getPostalAddress());
+
+        User u2 = User.getById(createAssuranceUser("testworker", "testname", createUniqueName() + "@testdom.com", TEST_PASSWORD));
+        executeBasicWebInteraction(cookie, ViewOrgPage.DEFAULT_PATH + "/" + orgs[0].getId(), "email=" + URLEncoder.encode(u2.getEmail(), "UTF-8") + "&do_affiliate=y&master=y", 1);
+        List<Affiliation> allAdmins = orgs[0].getAllAdmins();
+        assertEquals(1, allAdmins.size());
+        Affiliation affiliation = allAdmins.get(0);
+        assertSame(u2, affiliation.getTarget());
+        assertTrue(affiliation.isMaster());
+
+        executeBasicWebInteraction(cookie, ViewOrgPage.DEFAULT_PATH + "/" + orgs[0].getId(), "email=" + URLEncoder.encode(u.getEmail(), "UTF-8") + "&do_affiliate=y", 1);
+        allAdmins = orgs[0].getAllAdmins();
+        assertEquals(2, allAdmins.size());
+        Affiliation affiliation2 = allAdmins.get(0);
+        if (affiliation2.getTarget().getId() == u2.getId()) {
+            affiliation2 = allAdmins.get(1);
+        }
+        assertEquals(u.getId(), affiliation2.getTarget().getId());
+        assertFalse(affiliation2.isMaster());
+
+        executeBasicWebInteraction(cookie, ViewOrgPage.DEFAULT_PATH + "/" + orgs[0].getId(), "del=" + URLEncoder.encode(u.getEmail(), "UTF-8") + "&email=&do_affiliate=y", 1);
+        assertEquals(1, orgs[0].getAllAdmins().size());
+
+        executeBasicWebInteraction(cookie, ViewOrgPage.DEFAULT_PATH + "/" + orgs[0].getId(), "del=" + URLEncoder.encode(u2.getEmail(), "UTF-8") + "&email=&do_affiliate=y", 1);
+        assertEquals(0, orgs[0].getAllAdmins().size());
+
+        executeBasicWebInteraction(cookie, ViewOrgPage.DEFAULT_PATH + "/" + orgs[0].getId(), "action=updateCertificateData&O=name1&contact=&L=K%C3%B6ln&ST=%C3%9C%C3%96%C3%84%C3%9F&C=DE&comments=jkl%C3%B6loiuzfdfgjlh%C3%B6", 0);
+        clearCaches();
+        orgs = Organisation.getOrganisations(0, 30);
+        assertEquals("name1", orgs[0].getName());
+    }
+
+    @Test
+    public void testNonAssurerSeeOnlyOwn() throws IOException, GigiApiException {
+        User u2 = User.getById(createAssuranceUser("testworker", "testname", createUniqueName() + "@testdom.com", TEST_PASSWORD));
+        Organisation o1 = createUniqueOrg();
+        Organisation o2 = createUniqueOrg();
+        o1.addAdmin(u2, u, false);
+        String session2 = login(u2.getEmail(), TEST_PASSWORD);
+
+        URLConnection uc = get(session2, ViewOrgPage.DEFAULT_PATH);
+        assertEquals(403, ((HttpURLConnection) uc).getResponseCode());
+
+        uc = get(session2, MyDetails.PATH);
+        String content = IOUtils.readURL(uc);
+        assertThat(content, containsString(o1.getName()));
+        assertThat(content, not(containsString(o2.getName())));
+        uc = get(session2, ViewOrgPage.DEFAULT_PATH + "/" + o1.getId());
+        assertEquals(403, ((HttpURLConnection) uc).getResponseCode());
+        uc = get(session2, ViewOrgPage.DEFAULT_PATH + "/" + o2.getId());
+        assertEquals(403, ((HttpURLConnection) uc).getResponseCode());
+
+        uc = get(ViewOrgPage.DEFAULT_PATH);
+        content = IOUtils.readURL(uc);
+        assertThat(content, containsString(o1.getName()));
+        assertThat(content, containsString(o2.getName()));
+        uc = get(ViewOrgPage.DEFAULT_PATH + "/" + o1.getId());
+        assertEquals(200, ((HttpURLConnection) uc).getResponseCode());
+        uc = get(ViewOrgPage.DEFAULT_PATH + "/" + o2.getId());
+        assertEquals(200, ((HttpURLConnection) uc).getResponseCode());
+        o1.delete();
+        o2.delete();
+    }
+
+    @Test
+    public void testAffiliationRights() throws IOException, GigiApiException {
+        User u2 = User.getById(createAssuranceUser("testworker", "testname", createUniqueName() + "@testdom.com", TEST_PASSWORD));
+        User u3 = User.getById(createAssuranceUser("testmaster", "testname", createUniqueName() + "@testdom.com", TEST_PASSWORD));
+        User u4_dummy = User.getById(createVerifiedUser("testmaster", "testname", createUniqueName() + "@testdom.com", TEST_PASSWORD));
+        Organisation o1 = createUniqueOrg();
+        o1.addAdmin(u3, u, true);
+        try {
+            // must fail because u4 is no assurer
+            o1.addAdmin(u4_dummy, u3, false);
+            fail("No exception!");
+        } catch (GigiApiException e) {
+        }
+        o1.addAdmin(u2, u3, false);
+        try {
+            // must fail because u2 may not add admins
+            o1.addAdmin(u3, u2, false);
+            fail("No exception!");
+        } catch (GigiApiException e) {
+        }
+        try {
+            // must fail because u4 is no assurer
+            o1.addAdmin(u4_dummy, u, false);
+            fail("No exception!");
+        } catch (GigiApiException e) {
+        }
+        o1.removeAdmin(u2, u3);
+        o1.removeAdmin(u3, u3);
+        assertEquals(0, o1.getAllAdmins().size());
+        o1.delete();
+    }
+
+    @Test
+    public void testUpdateOrgCertData() throws IOException, GigiApiException {
+        Organisation o1 = createUniqueOrg();
+        o1.updateCertData("name", "DE", DIFFICULT_CHARS, "Köln");
+        assertEquals("name", o1.getName());
+        assertEquals("DE", o1.getState());
+        assertEquals(DIFFICULT_CHARS, o1.getProvince());
+        assertEquals("Köln", o1.getCity());
+        o1.delete();
+    }
+
+    @Test
+    public void testUpdateOrgData() throws IOException, GigiApiException {
+        Organisation o1 = createUniqueOrg();
+        o1.updateOrgData("mail", "opname", "Köln" + DIFFICULT_CHARS);
+        assertEquals("mail", o1.getContactEmail());
+        assertEquals("opname", o1.getOptionalName());
+        assertEquals("Köln" + DIFFICULT_CHARS, o1.getPostalAddress());
+        o1.delete();
+    }
+}
index 54a85d8bd964b34321a20a87601e251ed212fa72..d626cee0a3e55c18a37c0efd39473df368cc07ae 100644 (file)
 package org.cacert.gigi.pages.wot;
 
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
-import java.net.URL;
 import java.net.URLConnection;
 import java.net.URLEncoder;
-import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.text.SimpleDateFormat;
+import java.util.Calendar;
 import java.util.Date;
+import java.util.regex.Pattern;
 
-import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.pages.account.MyDetails;
 import org.cacert.gigi.testUtils.IOUtils;
 import org.cacert.gigi.testUtils.ManagedTest;
 import org.junit.Before;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
-
 public class TestAssurance extends ManagedTest {
-       private String assurerM;
-       private String assureeM;
-       private int assurer;
-       private int assuree;
-       private String cookie;
-       @Before
-       public void setup() throws IOException {
-               assurerM = createUniqueName() + "@cacert-test.org";
-               assureeM = createUniqueName() + "@cacert-test.org";
-               assurer = createAssuranceUser("a", "b", assurerM, "xvXV.1");
-               assuree = createAssuranceUser("a", "c", assureeM, "xvXV.1");
-               cookie = login(assurerM, "xvXV.1");
-
-       }
-       @Test
-       public void testAssureSearch() throws IOException {
-               String loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8")
-                               + "&day=1&month=1&year=1910");
-               assertTrue(loc, loc.endsWith(AssurePage.PATH + "/" + assuree));
-       }
-
-       @Test
-       public void testAssureSearchEmail() throws IOException {
-               String loc = search("email=1" + URLEncoder.encode(assureeM, "UTF-8")
-                               + "&day=1&month=1&year=1910");
-               assertNull(loc);
-       }
-       @Test
-       public void testAssureSearchDob() throws IOException {
-               String loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8")
-                               + "&day=2&month=1&year=1910");
-               assertNull(loc);
-               loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8")
-                               + "&day=1&month=2&year=1910");
-               assertNull(loc);
-               loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8")
-                               + "&day=1&month=1&year=1911");
-               assertNull(loc);
-       }
-       private String search(String query) throws MalformedURLException,
-                       IOException, UnsupportedEncodingException {
-               URL u = new URL("https://" + getServerName() + AssurePage.PATH);
-               URLConnection uc = u.openConnection();
-               uc.setDoOutput(true);
-               uc.addRequestProperty("Cookie", cookie);
-               uc.getOutputStream().write((query).getBytes());
-               uc.getOutputStream().flush();
-
-               String loc = uc.getHeaderField("Location");
-               return loc;
-       }
-       @Test
-       public void testAssureForm() throws IOException {
-               String error = getError("date=2000-01-01&location=testcase&certify=1&rules=1&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, error.startsWith("</div>"));
-       }
-
-       @Test
-       public void testAssureFormRace() throws IOException, SQLException {
-               URLConnection uc = buildupAssureFormConnection();
-               PreparedStatement ps = DatabaseConnection.getInstance().prepare(
-                               "UPDATE `users` SET email='changed' WHERE id=?");
-               ps.setInt(1, assuree);
-               ps.execute();
-               uc.getOutputStream()
-                               .write(("date=2000-01-01&location=testcase&certify=1&rules=1&CCAAgreed=1&assertion=1&points=10")
-                                               .getBytes());
-               uc.getOutputStream().flush();
-               String error = fetchStartErrorMessage(IOUtils.readURL(uc));
-               assertTrue(error, !error.startsWith("</div>"));
-       }
-       @Test
-       public void testAssureFormFuture() throws IOException {
-               SimpleDateFormat sdf = new SimpleDateFormat("YYYY");
-               int year = Integer.parseInt(sdf.format(new Date(System
-                               .currentTimeMillis()))) + 2;
-               String error = getError("date="
-                               + year
-                               + "-01-01&location=testcase&certify=1&rules=1&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-       }
-       @Test
-       public void testAssureFormNoLoc() throws IOException {
-               String error = getError("date=2000-01-01&location=a&certify=1&rules=1&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-               error = getError("date=2000-01-01&location=&certify=1&rules=1&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-       }
-
-       @Test
-       public void testAssureFormInvalDate() throws IOException {
-               String error = getError("date=20000101&location=testcase&certify=1&rules=1&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-               error = getError("date=&location=testcase&certify=1&rules=1&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-       }
-       @Test
-       public void testAssureFormBoxes() throws IOException {
-               String error = getError("date=2000-01-01&location=testcase&certify=0&rules=1&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-               error = getError("date=2000-01-01&location=testcase&certify=1&rules=&CCAAgreed=1&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-               error = getError("date=2000-01-01&location=testcase&certify=1&rules=1&CCAAgreed=a&assertion=1&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-               error = getError("date=2000-01-01&location=testcase&certify=1&rules=1&CCAAgreed=1&assertion=z&points=10");
-               assertTrue(error, !error.startsWith("</div>"));
-       }
-       private String getError(String query) throws MalformedURLException,
-                       IOException {
-               URLConnection uc = buildupAssureFormConnection();
-               uc.getOutputStream().write((query).getBytes());
-               uc.getOutputStream().flush();
-               String error = fetchStartErrorMessage(IOUtils.readURL(uc));
-               return error;
-       }
-       private URLConnection buildupAssureFormConnection()
-                       throws MalformedURLException, IOException {
-               URL u = new URL("https://" + getServerName() + AssurePage.PATH + "/"
-                               + assuree);
-               URLConnection uc = u.openConnection();
-               uc.addRequestProperty("Cookie", cookie);
-               uc.getInputStream();// request form
-               uc = u.openConnection();
-               uc.addRequestProperty("Cookie", cookie);
-               uc.setDoOutput(true);
-               return uc;
-       }
+
+    private String assurerM;
+
+    private String assureeM;
+
+    private String cookie;
+
+    @Before
+    public void setup() throws IOException {
+        clearCaches();
+        assurerM = createUniqueName() + "@cacert-test.org";
+        assureeM = createUniqueName() + "@cacert-test.org";
+
+        createAssuranceUser("a", "b", assurerM, TEST_PASSWORD);
+        createVerifiedUser("a", "c", assureeM, TEST_PASSWORD);
+
+        cookie = login(assurerM, TEST_PASSWORD);
+    }
+
+    @Test
+    public void testAssureSearch() throws IOException {
+        String loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8") + "&day=1&month=1&year=1910");
+        assertTrue(loc, loc.contains("type=\"checkbox\" name=\"tos_agree\""));
+    }
+
+    @Test
+    public void testAssureSearchEmail() throws IOException {
+        String loc = search("email=1" + URLEncoder.encode(assureeM, "UTF-8") + "&day=1&month=1&year=1910");
+        assertTrue(loc, !loc.contains("type=\"checkbox\" name=\"tos_agree\""));
+    }
+
+    @Test
+    public void testAssureSearchDob() throws IOException {
+        String loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8") + "&day=2&month=1&year=1910");
+        assertTrue(loc, !loc.contains("type=\"checkbox\" name=\"tos_agree\""));
+        loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8") + "&day=1&month=2&year=1910");
+        assertTrue(loc, !loc.contains("type=\"checkbox\" name=\"tos_agree\""));
+        loc = search("email=" + URLEncoder.encode(assureeM, "UTF-8") + "&day=1&month=1&year=1911");
+        assertTrue(loc, !loc.contains("type=\"checkbox\" name=\"tos_agree\""));
+    }
+
+    private String search(String query) throws MalformedURLException, IOException, UnsupportedEncodingException {
+        URLConnection uc = get(cookie, AssurePage.PATH);
+        uc.setDoOutput(true);
+        uc.getOutputStream().write(("search&" + query).getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+
+        return IOUtils.readURL(uc);
+    }
+
+    @Test
+    public void testAssureForm() throws IOException {
+        String error = getError("date=2000-01-01&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertNull(error);
+    }
+
+    @Test
+    public void testAssureFormContanisData() throws IOException {
+        URLConnection uc = buildupAssureFormConnection(true);
+        uc.getOutputStream().write(("date=2000-01-01&location=testcase&rules=1&tos_agree=1&assertion=1&points=10").getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+        String data = IOUtils.readURL(uc);
+        assertThat(data, containsString("2000-01-01"));
+        assertThat(data, containsString("testcase"));
+    }
+
+    @Test
+    public void testAssureFormNoCSRF() throws IOException {
+        // override csrf
+        HttpURLConnection uc = (HttpURLConnection) buildupAssureFormConnection(false);
+        uc.getOutputStream().write(("date=2000-01-01&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10").getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+        assertEquals(500, uc.getResponseCode());
+    }
+
+    @Test
+    public void testAssureFormWrongCSRF() throws IOException {
+        // override csrf
+        HttpURLConnection uc = (HttpURLConnection) buildupAssureFormConnection(false);
+        uc.getOutputStream().write(("date=2000-01-01&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10&csrf=aragc").getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+        assertEquals(500, uc.getResponseCode());
+    }
+
+    @Test
+    public void testAssureFormRaceName() throws IOException, SQLException {
+        testAssureFormRace(true, false);
+    }
+
+    @Test
+    public void testAssureFormRaceDoB() throws IOException, SQLException {
+        testAssureFormRace(false, false);
+    }
+
+    @Test
+    public void testAssureFormRaceNameBlind() throws IOException, SQLException {
+        testAssureFormRace(true, true);
+    }
+
+    @Test
+    public void testAssureFormRaceDoBBlind() throws IOException, SQLException {
+        testAssureFormRace(false, true);
+    }
+
+    public void testAssureFormRace(boolean name, boolean succeed) throws IOException, SQLException {
+        URLConnection uc = buildupAssureFormConnection(true);
+
+        String assureeCookie = login(assureeM, TEST_PASSWORD);
+        String newName = "lname=" + (name && !succeed ? "a" : "c") + "&fname=a&mname=&suffix=";
+        String newDob = "day=1&month=1&year=" + ( !name && !succeed ? 1911 : 1910);
+
+        assertNull(executeBasicWebInteraction(assureeCookie, MyDetails.PATH, newName + "&" + newDob + "&processDetails", 0));
+
+        uc.getOutputStream().write(("date=2000-01-01&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10").getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+        String error = fetchStartErrorMessage(IOUtils.readURL(uc));
+        if (succeed) {
+            assertNull(error);
+        } else {
+            assertTrue(error, !error.startsWith("</div>"));
+            assertThat(error, containsString("changed his personal details"));
+        }
+    }
+
+    @Test
+    public void testAssureFormFuture() throws IOException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy");
+        int year = Integer.parseInt(sdf.format(new Date(System.currentTimeMillis()))) + 2;
+        String error = getError("date=" + year + "-01-01&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+    }
+
+    @Test
+    public void testAssureFormFutureOK() throws IOException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        Calendar c = Calendar.getInstance();
+        c.setTimeInMillis(System.currentTimeMillis());
+        c.add(Calendar.HOUR_OF_DAY, 12);
+
+        String error = getError("date=" + sdf.format(new Date(c.getTimeInMillis())) + "&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertNull(error);
+    }
+
+    @Test
+    public void testAssureFormNoLoc() throws IOException {
+        String error = getError("date=2000-01-01&location=a&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+        error = getError("date=2000-01-01&location=&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+    }
+
+    @Test
+    public void testAssureFormInvalDate() throws IOException {
+        String error = getError("date=20000101&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+        error = getError("date=&location=testcase&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+    }
+
+    @Test
+    public void testAssureFormBoxes() throws IOException {
+        String error = getError("date=2000-01-01&location=testcase&certify=0&rules=1&tos_agree=1&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+        error = getError("date=2000-01-01&location=testcase&certify=1&rules=&tos_agree=1&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+        error = getError("date=2000-01-01&location=testcase&certify=1&rules=1&tos_agree=a&assertion=1&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+        error = getError("date=2000-01-01&location=testcase&certify=1&rules=1&tos_agree=1&assertion=z&points=10");
+        assertTrue(error, !error.startsWith("</div>"));
+    }
+
+    @Test
+    public void testAssureListingValid() throws IOException {
+        String uniqueLoc = createUniqueName();
+        String error = getError("date=2000-01-01&location=" + uniqueLoc + "&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertNull(error);
+        String cookie = login(assureeM, TEST_PASSWORD);
+        URLConnection url = get(cookie, MyPoints.PATH);
+        String resp = IOUtils.readURL(url);
+        resp = resp.split(Pattern.quote("</table>"))[0];
+        assertThat(resp, containsString(uniqueLoc));
+    }
+
+    @Test
+    public void testAssurerListingValid() throws IOException {
+        String uniqueLoc = createUniqueName();
+        String error = getError("date=2000-01-01&location=" + uniqueLoc + "&certify=1&rules=1&tos_agree=1&assertion=1&points=10");
+        assertNull(error);
+        String cookie = login(assurerM, TEST_PASSWORD);
+        URLConnection url = get(cookie, MyPoints.PATH);
+        String resp = IOUtils.readURL(url);
+        resp = resp.split(Pattern.quote("</table>"))[1];
+        assertThat(resp, containsString(uniqueLoc));
+    }
+
+    private String getError(String query) throws MalformedURLException, IOException {
+        URLConnection uc = buildupAssureFormConnection(true);
+        uc.getOutputStream().write((query).getBytes("UTF-8"));
+        uc.getOutputStream().flush();
+        String error = fetchStartErrorMessage(IOUtils.readURL(uc));
+        return error;
+    }
+
+    private URLConnection buildupAssureFormConnection(boolean doCSRF) throws MalformedURLException, IOException {
+        return buildupAssureFormConnection(cookie, assureeM, doCSRF);
+    }
+
+    public static URLConnection buildupAssureFormConnection(String cookie, String email, boolean doCSRF) throws MalformedURLException, IOException {
+        URLConnection uc = get(cookie, AssurePage.PATH);
+        uc.setDoOutput(true);
+        uc.getOutputStream().write(("email=" + URLEncoder.encode(email, "UTF-8") + "&day=1&month=1&year=1910&search").getBytes("UTF-8"));
+
+        String csrf = getCSRF(uc);
+        uc = get(cookie, AssurePage.PATH);
+        uc.setDoOutput(true);
+        if (doCSRF) {
+            uc.getOutputStream().write(("csrf=" + csrf + "&").getBytes("UTF-8"));
+        }
+        return uc;
+    }
 
 }
diff --git a/tests/org/cacert/gigi/pages/wot/TestListing.java b/tests/org/cacert/gigi/pages/wot/TestListing.java
new file mode 100644 (file)
index 0000000..2747c50
--- /dev/null
@@ -0,0 +1,47 @@
+package org.cacert.gigi.pages.wot;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.junit.Test;
+
+public class TestListing extends ClientTest {
+
+    @Test
+    public void testListing() throws IOException, GigiApiException {
+        String c = IOUtils.readURL(get(MyListingPage.PATH));
+        // Default settings not listed, empty text
+        assertThat(c, not(containsString("value=\"1\" selected")));
+        assertThat(c, containsString("value=\"0\" selected"));
+        assertThat(c, containsString("></textarea>"));
+
+        assertEquals(302, post(MyListingPage.PATH, "listme=0&contactinfo=a").getResponseCode());
+        c = IOUtils.readURL(get(MyListingPage.PATH));
+        assertThat(c, not(containsString("value=\"1\" selected")));
+        assertThat(c, containsString("value=\"0\" selected"));
+        assertThat(c, containsString("></textarea>"));
+
+        assertEquals(302, post(MyListingPage.PATH, "listme=1&contactinfo=a").getResponseCode());
+        c = IOUtils.readURL(get(MyListingPage.PATH));
+        assertThat(c, containsString("value=\"1\" selected"));
+        assertThat(c, not(containsString("value=\"0\" selected")));
+        assertThat(c, containsString(">a</textarea>"));
+
+        assertEquals(302, post(MyListingPage.PATH, "listme=1&contactinfo=b").getResponseCode());
+        c = IOUtils.readURL(get(MyListingPage.PATH));
+        assertThat(c, containsString("value=\"1\" selected"));
+        assertThat(c, not(containsString("value=\"0\" selected")));
+        assertThat(c, containsString(">b</textarea>"));
+
+        assertEquals(302, post(MyListingPage.PATH, "listme=0&contactinfo=b").getResponseCode());
+        c = IOUtils.readURL(get(MyListingPage.PATH));
+        assertThat(c, containsString("value=\"0\" selected"));
+        assertThat(c, not(containsString("value=\"1\" selected")));
+        assertThat(c, containsString("></textarea>"));
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/wot/TestTTP.java b/tests/org/cacert/gigi/pages/wot/TestTTP.java
new file mode 100644 (file)
index 0000000..34b0ca6
--- /dev/null
@@ -0,0 +1,41 @@
+package org.cacert.gigi.pages.wot;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.ObjectCache;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.junit.Test;
+
+public class TestTTP extends ClientTest {
+
+    public TestTTP() throws IOException {}
+
+    @Test
+    public void testTTPApply() throws IOException {
+        String ttp = IOUtils.readURL(get(RequestTTPPage.PATH));
+        assertThat(ttp, containsString("<form"));
+        executeBasicWebInteraction(cookie, RequestTTPPage.PATH, "country=0");
+
+        ttp = IOUtils.readURL(get(RequestTTPPage.PATH));
+        assertThat(ttp, not(containsString("<form")));
+        ObjectCache.clearAllCaches();
+        u = User.getById(u.getId());
+        assertTrue(u.isInGroup(Group.getByString("ttp-applicant")));
+    }
+
+    @Test
+    public void testTTPEnoughPoints() throws IOException, GigiApiException {
+        User u = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@example.org", TEST_PASSWORD));
+        cookie = login(u.getEmail(), TEST_PASSWORD);
+
+        String ttp = IOUtils.readURL(get(RequestTTPPage.PATH));
+        assertThat(ttp, not(containsString("<form")));
+    }
+}
diff --git a/tests/org/cacert/gigi/pages/wot/TestTTPAdmin.java b/tests/org/cacert/gigi/pages/wot/TestTTPAdmin.java
new file mode 100644 (file)
index 0000000..31b0e51
--- /dev/null
@@ -0,0 +1,49 @@
+package org.cacert.gigi.pages.wot;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.admin.TTPAdminPage;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.junit.Test;
+
+public class TestTTPAdmin extends ClientTest {
+
+    User us2;
+
+    public TestTTPAdmin() throws IOException {
+        us2 = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "@example.com", TEST_PASSWORD));
+    }
+
+    @Test
+    public void testHasRight() throws IOException {
+        testTTPAdmin(true);
+    }
+
+    @Test
+    public void testHasNoRight() throws IOException {
+        testTTPAdmin(false);
+    }
+
+    public void testTTPAdmin(boolean hasRight) throws IOException {
+        if (hasRight) {
+            grant(email, Group.getByString("ttp-assurer"));
+        }
+        grant(u.getEmail(), TTPAdminPage.TTP_APPLICANT);
+        cookie = login(u.getEmail(), TEST_PASSWORD);
+
+        assertEquals( !hasRight ? 403 : 200, fetchStatusCode(TTPAdminPage.PATH));
+        assertEquals( !hasRight ? 403 : 200, fetchStatusCode(TTPAdminPage.PATH + "/"));
+        assertEquals( !hasRight ? 403 : 200, fetchStatusCode(TTPAdminPage.PATH + "/" + u.getId()));
+        assertEquals( !hasRight ? 403 : 404, fetchStatusCode(TTPAdminPage.PATH + "/" + us2.getId()));
+        assertEquals( !hasRight ? 403 : 404, fetchStatusCode(TTPAdminPage.PATH + "/" + 100));
+    }
+
+    private int fetchStatusCode(String path) throws MalformedURLException, IOException {
+        return get(path).getResponseCode();
+    }
+}
diff --git a/tests/org/cacert/gigi/ping/TestDNS.java b/tests/org/cacert/gigi/ping/TestDNS.java
new file mode 100644 (file)
index 0000000..8bb6146
--- /dev/null
@@ -0,0 +1,101 @@
+package org.cacert.gigi.ping;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.sql.SQLException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.naming.NamingException;
+
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.PingTest;
+import org.cacert.gigi.testUtils.TestEmailReceiver.TestMail;
+import org.cacert.gigi.util.DNSUtil;
+import org.cacert.gigi.util.RandomToken;
+import org.junit.Test;
+
+public class TestDNS extends PingTest {
+
+    @Test
+    public void dnsSanity() throws IOException, NamingException {
+
+        String token = RandomToken.generateToken(16);
+        String value = RandomToken.generateToken(16);
+
+        updateService(token, value, "dns");
+        assertEquals(value, readDNS(token));
+
+    }
+
+    @Test
+    public void emailAndDNSSuccess() throws IOException, InterruptedException, SQLException, NamingException {
+        testEmailAndDNS(0, 0, true, true);
+    }
+
+    @Test
+    public void dnsFail() throws IOException, InterruptedException, SQLException, NamingException {
+        testEmailAndDNS(1, 0, false, true);
+    }
+
+    @Test
+    public void dnsContentFail() throws IOException, InterruptedException, SQLException, NamingException {
+        testEmailAndDNS(2, 0, false, true);
+    }
+
+    @Test
+    public void emailFail() throws IOException, InterruptedException, SQLException, NamingException {
+        testEmailAndDNS(0, 1, true, false);
+    }
+
+    @Test
+    public void emailAndDNSFail() throws IOException, InterruptedException, SQLException, NamingException {
+        testEmailAndDNS(2, 1, false, false);
+    }
+
+    public void testEmailAndDNS(int dnsVariant, int emailVariant, boolean successDNS, boolean successMail) throws IOException, InterruptedException, SQLException, NamingException {
+
+        String test = getTestProps().getProperty("domain.dnstest");
+        assumeNotNull(test);
+
+        Matcher m = initailizeDomainForm();
+        updateService(m.group(1) + (dnsVariant == 1 ? "a" : ""), m.group(2) + (dnsVariant == 2 ? "a" : ""), "dns");
+
+        String content = "newdomain=" + URLEncoder.encode(test, "UTF-8") + //
+                "&emailType=y&email=2&DNSType=y" + //
+                "&ssl-type-0=direct&ssl-port-0=" + //
+                "&ssl-type-1=direct&ssl-port-1=" + //
+                "&ssl-type-2=direct&ssl-port-2=" + //
+                "&ssl-type-3=direct&ssl-port-3=" + //
+                "&adddomain&csrf=" + csrf;
+        String p2 = sendDomainForm(content);
+
+        TestMail mail = getMailReciever().receive();
+        if (emailVariant == 0) {
+            mail.verify();
+        }
+
+        waitForPings(2);
+
+        String newcontent = IOUtils.readURL(get(p2));
+        Pattern pat = Pattern.compile("<td>dns</td>\\s*<td>success</td>");
+        assertTrue(newcontent, !successDNS ^ pat.matcher(newcontent).find());
+        pat = Pattern.compile("<td>email</td>\\s*<td>success</td>");
+        assertTrue(newcontent, !successMail ^ pat.matcher(newcontent).find());
+    }
+
+    private String readDNS(String token) throws NamingException {
+        String test = getTestProps().getProperty("domain.dnstest");
+        assumeNotNull(test);
+        String targetDomain = token + "._cacert._auth." + test;
+        String testns = getTestProps().getProperty("domain.testns");
+        assumeNotNull(testns);
+        String[] data = DNSUtil.getTXTEntries(targetDomain, testns);
+        assertEquals(1, data.length);
+        return data[0];
+
+    }
+}
diff --git a/tests/org/cacert/gigi/ping/TestHTTP.java b/tests/org/cacert/gigi/ping/TestHTTP.java
new file mode 100644 (file)
index 0000000..31fecd9
--- /dev/null
@@ -0,0 +1,109 @@
+package org.cacert.gigi.ping;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.sql.SQLException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.naming.NamingException;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.DomainPingConfiguration;
+import org.cacert.gigi.dbObjects.DomainPingType;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.PingTest;
+import org.cacert.gigi.testUtils.TestEmailReceiver.TestMail;
+import org.cacert.gigi.util.RandomToken;
+import org.junit.Test;
+
+public class TestHTTP extends PingTest {
+
+    @Test
+    public void httpSanity() throws IOException, NamingException {
+
+        String token = RandomToken.generateToken(16);
+        String value = RandomToken.generateToken(16);
+
+        TestDNS.updateService(token, value, "http");
+        assertEquals(value, readHTTP(token));
+
+    }
+
+    @Test
+    public void httpAndMailSuccess() throws Exception {
+        testEmailAndHTTP(0, 0, true, true);
+    }
+
+    @Test
+    public void httpFailKeyAndMailSuccess() throws Exception {
+        testEmailAndHTTP(1, 0, false, true);
+    }
+
+    @Test
+    public void httpFailValAndMailFail() throws Exception {
+        testEmailAndHTTP(2, 1, false, false);
+    }
+
+    public void testEmailAndHTTP(int httpVariant, int emailVariant, boolean successHTTP, boolean successMail) throws IOException, InterruptedException, SQLException, GigiApiException {
+
+        String test = getTestProps().getProperty("domain.http");
+        assumeNotNull(test);
+
+        Matcher m = initailizeDomainForm();
+        updateService(m.group(1) + (httpVariant == 1 ? "a" : ""), m.group(2) + (httpVariant == 2 ? "a" : ""), "http");
+
+        String content = "newdomain=" + URLEncoder.encode(test, "UTF-8") + //
+                "&emailType=y&email=2&HTTPType=y" + //
+                "&ssl-type-0=direct&ssl-port-0=" + //
+                "&ssl-type-1=direct&ssl-port-1=" + //
+                "&ssl-type-2=direct&ssl-port-2=" + //
+                "&ssl-type-3=direct&ssl-port-3=" + //
+                "&adddomain&csrf=" + csrf;
+        String p2 = sendDomainForm(content);
+
+        TestMail mail = getMailReciever().receive();
+        if (emailVariant == 0) {
+            mail.verify();
+        }
+        waitForPings(2);
+
+        String newcontent = IOUtils.readURL(get(p2));
+        Pattern pat = Pattern.compile("<td>http</td>\\s*<td>success</td>");
+        assertTrue(newcontent, !successHTTP ^ pat.matcher(newcontent).find());
+        pat = Pattern.compile("<td>email</td>\\s*<td>success</td>");
+        assertTrue(newcontent, !successMail ^ pat.matcher(newcontent).find());
+
+        if (successHTTP) { // give it a second try
+            int id = Integer.parseInt(p2.replaceFirst("^.*/([0-9]+)$", "$1"));
+            Domain d = Domain.getById(id);
+            DomainPingConfiguration dpc = null;
+            for (DomainPingConfiguration conf : d.getConfiguredPings()) {
+                if (conf.getType() == DomainPingType.HTTP) {
+                    dpc = conf;
+                    break;
+                }
+            }
+            if (dpc == null) {
+                fail("Http config not found");
+            }
+            String res = executeBasicWebInteraction(cookie, p2, "configId=" + dpc.getId());
+            assertThat(res, containsString("only allowed after"));
+        }
+    }
+
+    private String readHTTP(String token) throws IOException {
+        String httpDom = getTestProps().getProperty("domain.http");
+        assumeNotNull(httpDom);
+        URL u = new URL("http://" + httpDom + "/cacert-" + token + ".txt");
+        return IOUtils.readURL(new InputStreamReader(u.openStream(), "UTF-8")).trim();
+
+    }
+}
diff --git a/tests/org/cacert/gigi/ping/TestSSL.java b/tests/org/cacert/gigi/ping/TestSSL.java
new file mode 100644 (file)
index 0000000..34c8d29
--- /dev/null
@@ -0,0 +1,332 @@
+package org.cacert.gigi.ping;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.URLEncoder;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import javax.security.auth.x500.X500Principal;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.database.DatabaseConnection.Link;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.IOUtils;
+import org.cacert.gigi.testUtils.PingTest;
+import org.cacert.gigi.testUtils.TestEmailReceiver.TestMail;
+import org.cacert.gigi.util.SimpleSigner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestSSL extends PingTest {
+
+    @Parameters(name = "self-signed = {0}")
+    public static Iterable<Object[]> genParams() throws IOException {
+        return Arrays.asList(new Object[] {
+                true
+        }, new Object[] {
+                false
+        });
+
+    }
+
+    @Parameter
+    public Boolean self = false;
+
+    public abstract static class AsyncTask<T> {
+
+        T res;
+
+        Thread runner;
+
+        Exception ex;
+
+        public T join() throws InterruptedException {
+            runner.join();
+            if (ex != null) {
+                throw new Error(ex);
+            }
+            return res;
+        }
+
+        public void start() {
+            runner = new Thread() {
+
+                @Override
+                public void run() {
+                    try {
+                        res = AsyncTask.this.run();
+                    } catch (Exception e) {
+                        ex = e;
+                    }
+                }
+            };
+            runner.start();
+        }
+
+        public abstract T run() throws Exception;
+
+    }
+
+    private KeyPair kp;
+
+    private X509Certificate c;
+
+    @Test(timeout = 70000)
+    public void sslAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException {
+        testEmailAndSSL(0, 0, true);
+    }
+
+    @Test(timeout = 70000)
+    public void sslWongTypeAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException {
+        testEmailAndSSL(1, 0, true);
+    }
+
+    @Test(timeout = 70000)
+    public void sslOneMissingAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException {
+        testEmailAndSSL(2, 0, true);
+    }
+
+    @Test(timeout = 70000)
+    public void sslBothMissingAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException {
+        testEmailAndSSL(3, 0, true);
+    }
+
+    @Test(timeout = 70000)
+    public void sslWrongTypeAndMailFail() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException {
+        testEmailAndSSL(1, 1, false);
+    }
+
+    private void testEmailAndSSL(int sslVariant, int emailVariant, boolean successMail) throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException {
+        try (Link link = DatabaseConnection.newLink(false)) {
+            testEmailAndSSLWithLink(sslVariant, emailVariant, successMail);
+        }
+    }
+
+    /**
+     * @param sslVariant
+     *            <ul>
+     *            <li>0= all valid</li>
+     *            <li>1= wrong type</li>
+     *            <li>2= one server missing</li>
+     *            <li>3= both servers missing</li>
+     *            </ul>
+     * @param emailVariant
+     * @param successSSL
+     * @param successMail
+     * @throws IOException
+     * @throws InterruptedException
+     * @throws SQLException
+     * @throws GeneralSecurityException
+     * @throws GigiApiException
+     */
+
+    private void testEmailAndSSLWithLink(int sslVariant, int emailVariant, boolean successMail) throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException {
+        String test = getTestProps().getProperty("domain.local");
+        assumeNotNull(test);
+        Matcher m = initailizeDomainForm();
+        String value = m.group(2);
+
+        if (self) {
+            createCertificateSelf(test, sslVariant == 1 ? "clientAuth" : "serverAuth", value);
+        } else {
+            createCertificate(test, CertificateProfile.getByName(sslVariant == 1 ? "client" : "server"));
+        }
+
+        final SSLServerSocket sss = createSSLServer(kp.getPrivate(), c);
+        int port = sss.getLocalPort();
+        final SSLServerSocket sss2 = createSSLServer(kp.getPrivate(), c);
+        int port2 = sss2.getLocalPort();
+        if (sslVariant == 3 || sslVariant == 2) {
+            sss2.close();
+            if (sslVariant == 3) {
+                sss.close();
+            }
+        }
+        String content = "adddomain&newdomain=" + URLEncoder.encode(test, "UTF-8") + //
+                "&emailType=y&email=2&SSLType=y" + //
+                "&ssl-type-0=direct&ssl-port-0=" + port + //
+                "&ssl-type-1=direct&ssl-port-1=" + port2 + //
+                "&ssl-type-2=direct&ssl-port-2=" + //
+                "&ssl-type-3=direct&ssl-port-3=" + //
+                "&adddomain&csrf=" + csrf;
+        String p2 = sendDomainForm(content);
+        boolean firstSucceeds = sslVariant != 0 && sslVariant != 2;
+        AsyncTask<Boolean> ass = new AsyncTask<Boolean>() {
+
+            @Override
+            public Boolean run() throws Exception {
+                return acceptSSLServer(sss);
+            }
+        };
+        ass.start();
+        System.out.println(port + " and " + port2 + " ready");
+        System.err.println(port + " and " + port2 + " ready");
+        boolean accept2 = acceptSSLServer(sss2);
+        boolean accept1 = ass.join();
+        // assertTrue(firstSucceeds ^ accept1);
+        boolean secondsSucceeds = sslVariant != 0;
+        // assertTrue(secondsSucceeds ^ accept2);
+
+        TestMail mail = getMailReciever().receive();
+        if (emailVariant == 0) {
+            mail.verify();
+        }
+        waitForPings(3);
+
+        String newcontent = IOUtils.readURL(get(p2));
+        Pattern pat = Pattern.compile("<td>ssl</td>\\s*<td>success</td>");
+        Matcher matcher = pat.matcher(newcontent);
+        assertTrue(newcontent, firstSucceeds ^ matcher.find());
+        assertTrue(newcontent, secondsSucceeds ^ matcher.find());
+        assertFalse(newcontent, matcher.find());
+        pat = Pattern.compile("<td>email</td>\\s*<td>success</td>");
+        assertTrue(newcontent, !successMail ^ pat.matcher(newcontent).find());
+    }
+
+    private void createCertificate(String test, CertificateProfile profile) throws GeneralSecurityException, IOException, SQLException, InterruptedException, GigiApiException {
+        kp = generateKeypair();
+        String csr = generatePEMCSR(kp, "CN=" + test);
+        User u = User.getById(id);
+        Certificate c = new Certificate(u, u, Certificate.buildDN("CN", test), Digest.SHA256, csr, CSRType.CSR, profile);
+        c.issue(null, "2y", u).waitFor(60000);
+        this.c = c.cert();
+    }
+
+    private void createCertificateSelf(String test, String eku, String tok) throws GeneralSecurityException, IOException, SQLException, InterruptedException, GigiApiException {
+        kp = generateKeypair();
+        HashMap<String, String> name = new HashMap<>();
+        name.put("CN", "");
+        name.put("OU", tok);
+
+        Date from = new Date();
+        Date to = new Date(from.getTime() + 1000 * 60 * 60 * 2);
+        List<Certificate.SubjectAlternateName> l = new LinkedList<>();
+
+        byte[] cert = SimpleSigner.generateCert(kp.getPublic(), kp.getPrivate(), name, new X500Principal(SimpleSigner.genX500Name(name).getEncoded()), l, from, to, Digest.SHA256, eku);
+        c = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(cert));
+    }
+
+    private boolean acceptSSLServer(SSLServerSocket sss) throws IOException {
+        try (Socket s = sss.accept()) {
+            s.getOutputStream().write('b');
+            s.getOutputStream().close();
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    private SSLServerSocket createSSLServer(final PrivateKey priv, final X509Certificate cert) throws Error, IOException {
+        SSLContext sc;
+        try {
+            sc = SSLContext.getInstance("SSL");
+            sc.init(new KeyManager[] {
+                    new X509KeyManager() {
+
+                        @Override
+                        public String[] getServerAliases(String keyType, Principal[] issuers) {
+                            return new String[] {
+                                    "server"
+                            };
+                        }
+
+                        @Override
+                        public PrivateKey getPrivateKey(String alias) {
+                            return priv;
+                        }
+
+                        @Override
+                        public String[] getClientAliases(String keyType, Principal[] issuers) {
+                            throw new Error();
+                        }
+
+                        @Override
+                        public X509Certificate[] getCertificateChain(String alias) {
+                            return new X509Certificate[] {
+                                    cert
+                            };
+                        }
+
+                        @Override
+                        public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+                            throw new Error();
+                        }
+
+                        @Override
+                        public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+                            return "server";
+                        }
+
+                    }
+            }, new TrustManager[] {
+                    new X509TrustManager() {
+
+                        @Override
+                        public X509Certificate[] getAcceptedIssuers() {
+                            return null;
+                        }
+
+                        @Override
+                        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
+
+                        @Override
+                        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
+                    }
+            }, new SecureRandom());
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+            throw new Error(e);
+        } catch (KeyManagementException e) {
+            e.printStackTrace();
+            throw new Error(e);
+        }
+
+        SSLServerSocketFactory sssf = sc.getServerSocketFactory();
+        return (SSLServerSocket) sssf.createServerSocket(0);
+    }
+
+    public static void main(String[] args) throws Exception {
+        initEnvironment();
+        TestSSL t1 = new TestSSL();
+        t1.sslAndMailSuccess();
+        tearDownServer();
+    }
+
+}
diff --git a/tests/org/cacert/gigi/template/TestTemplate.java b/tests/org/cacert/gigi/template/TestTemplate.java
new file mode 100644 (file)
index 0000000..7dca58e
--- /dev/null
@@ -0,0 +1,140 @@
+package org.cacert.gigi.template;
+
+import static org.junit.Assert.*;
+
+import java.io.CharArrayWriter;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.HashAlgorithms;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Outputable;
+import org.cacert.gigi.output.template.OutputableArrayIterable;
+import org.cacert.gigi.output.template.Template;
+import org.junit.Test;
+
+public class TestTemplate {
+
+    private String testExecute(Language l, HashMap<String, Object> vars, String input) {
+        Template t = new Template(new StringReader(input));
+        CharArrayWriter caw = new CharArrayWriter();
+        PrintWriter pw = new PrintWriter(caw);
+        t.output(pw, l, vars);
+        pw.flush();
+        return new String(caw.toCharArray());
+    }
+
+    HashMap<String, Object> vars = new HashMap<>();
+
+    @Test
+    public void testVarEscape() {
+        vars.put("var", "val");
+        assertEquals("vall", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=$var?>l"));
+        vars.put("var", "val<");
+        assertEquals("val&lt;l", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=$var?>l"));
+        assertEquals("val<l", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=$!var?>l"));
+        vars.put("var", "val\">");
+        assertEquals("val&quot;&gt;l", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=$var?>l"));
+        assertEquals("val\">l", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=$!var?>l"));
+
+    }
+
+    @Test
+    public void testVarSprintf() {
+        vars.put("var", "val\">");
+        vars.put("var2", "val3<\"");
+        vars.put("var3", "val4>");
+        assertEquals("This val&quot;&gt; textl", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=_This ${var} text?>l"));
+        assertEquals("This val\"> textl", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=_This $!{var} text?>l"));
+
+        assertEquals("This val&quot;&gt; val3&lt;&quot; the val4&gt; textl", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=_This ${var} ${var2} the ${var3} text?>l"));
+        assertEquals("This val\"> val3<\" the val4> textl", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=_This $!{var} $!{var2} the $!{var3} text?>l"));
+
+        assertEquals("This blargh&lt;&gt;!, <>! textl", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=_This !'blargh&lt;&gt;!', !'<>! 'text?>l"));
+        assertEquals("This blargh&lt;&gt;!, <>!l", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=_This !'blargh&lt;&gt;!', !'<>!'?>l"));
+    }
+
+    @Test
+    public void testIf() {
+        vars.put("existent", "val");
+        assertEquals(">existent<", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? if($existent) { ?>>existent<? } ?><<? if($nonexistent) { ?>nonexistent<? } ?>"));
+    }
+
+    @Test
+    public void testForeach() {
+        vars.put("it", new IterableDataset() {
+
+            int i = 0;
+
+            @Override
+            public boolean next(Language l, Map<String, Object> vars) {
+                vars.put("e", Integer.toString(i++));
+                return i < 10;
+            }
+        });
+        assertEquals("012345678<", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? foreach($it) { ?><?=$e?><? } ?><<? foreach($nonexistent) { ?>nonexistent<? } ?>"));
+    }
+
+    @Test
+    public void testNullContent() {
+        assertEquals("<null>", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<<?=$undef?>>"));
+
+    }
+
+    @Test
+    public void testTranslate() {
+        assertEquals("&lt;null&gt;", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<?=_<null>?>"));
+
+    }
+
+    @Test
+    public void testOutputable() {
+        Outputable o = new Outputable() {
+
+            @Override
+            public void output(PrintWriter out, Language l, Map<String, Object> vars) {
+                out.print("bl");
+            }
+        };
+        vars.put("v", new OutputableArrayIterable(new Object[] {
+                o, o, o, o, o
+        }, "e"));
+        assertEquals("[0]bl[1]bl[2]bl[3]bl[4]bl", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? foreach($v) { ?>[<?=$i?>]<?=$e?><? } ?>"));
+
+    }
+
+    @Test
+    public void testHashalgs() {
+        vars.put("v", new HashAlgorithms(Digest.SHA256));
+        assertEquals("SHA256[ checked='checked']SHA384[]SHA512[]", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? foreach($v) { ?><?=$algorithm?>[<?=$!checked?>]<? } ?>"));
+
+    }
+
+    @Test
+    public void testInvalidBracketContent() {
+        try {
+            assertEquals("", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? } ?>"));
+            fail("should throw an error");
+        } catch (Error e) {
+
+        }
+    }
+
+    @Test
+    public void testIfElse() {
+        vars.put("b", Boolean.TRUE);
+        assertEquals("true", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? if($b){ ?>true<? } else{?>false<?}?>"));
+        vars.put("b", Boolean.FALSE);
+        assertEquals("false", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? if($b){ ?>true<? } else{?>false<?}?>"));
+
+        vars.put("b", new Object());
+        assertEquals("true", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? if($b){ ?>true<? } else{?>false<?}?>"));
+        vars.put("b", null);
+        assertEquals("false", testExecute(Language.getInstance(Locale.ENGLISH), vars, "<? if($b){ ?>true<? } else{?>false<?}?>"));
+    }
+}
diff --git a/tests/org/cacert/gigi/testUtils/ClientTest.java b/tests/org/cacert/gigi/testUtils/ClientTest.java
new file mode 100644 (file)
index 0000000..923fd97
--- /dev/null
@@ -0,0 +1,54 @@
+package org.cacert.gigi.testUtils;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import org.cacert.gigi.dbObjects.User;
+
+/**
+ * Superclass for testsuites in a scenario where there is an registered member,
+ * who is already logged on.
+ */
+public abstract class ClientTest extends ManagedTest {
+
+    /**
+     * Email of the member.
+     */
+    protected String email = createUniqueName() + "@example.org";
+
+    /**
+     * Id of the member
+     */
+    protected int id = createVerifiedUser("a", "b", email, TEST_PASSWORD);
+
+    /**
+     * {@link User} object of the member
+     */
+    protected User u = User.getById(id);
+
+    /**
+     * Session cookie of the member.
+     */
+    protected String cookie;
+
+    public ClientTest() {
+        try {
+            cookie = login(email, TEST_PASSWORD);
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
+    public HttpURLConnection post(String path, String query) throws IOException {
+        return post(path, query, 0);
+    }
+
+    public HttpURLConnection post(String path, String query, int formIndex) throws IOException {
+        return post(cookie, path, query, formIndex);
+    }
+
+    public HttpURLConnection get(String path) throws IOException {
+        return get(cookie, path);
+    }
+
+}
diff --git a/tests/org/cacert/gigi/testUtils/ConfiguredTest.java b/tests/org/cacert/gigi/testUtils/ConfiguredTest.java
new file mode 100644 (file)
index 0000000..90b3468
--- /dev/null
@@ -0,0 +1,119 @@
+package org.cacert.gigi.testUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Signature;
+import java.util.Properties;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.database.DatabaseConnection.Link;
+import org.cacert.gigi.util.PEM;
+import org.junit.BeforeClass;
+
+import sun.security.pkcs10.PKCS10;
+import sun.security.pkcs10.PKCS10Attributes;
+import sun.security.x509.X500Name;
+
+/**
+ * Base class for a Testsuite that makes use of the config variables that define
+ * the environment.
+ */
+public abstract class ConfiguredTest {
+
+    static Properties testProps = new Properties();
+
+    public static Properties getTestProps() {
+        return testProps;
+    }
+
+    private static boolean envInited = false;
+
+    @BeforeClass
+    public static void initEnvironment() throws IOException {
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+        if (envInited) {
+            return;
+        }
+        envInited = true;
+        try (FileInputStream inStream = new FileInputStream("config/test.properties")) {
+            testProps.load(inStream);
+        }
+        if ( !DatabaseConnection.isInited()) {
+            DatabaseConnection.init(testProps);
+            try {
+                l = DatabaseConnection.newLink(false);
+            } catch (InterruptedException e) {
+                throw new Error(e);
+            }
+        }
+    }
+
+    public static KeyPair generateKeypair() throws GeneralSecurityException {
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+        kpg.initialize(4096);
+        KeyPair keyPair = null;
+        File f = new File("testKeypair");
+        if (f.exists()) {
+            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f))) {
+                keyPair = (KeyPair) ois.readObject();
+            } catch (ClassNotFoundException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        } else {
+            keyPair = kpg.generateKeyPair();
+            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f))) {
+                oos.writeObject(keyPair);
+                oos.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return keyPair;
+    }
+
+    public static String generatePEMCSR(KeyPair kp, String dn) throws GeneralSecurityException, IOException {
+        return generatePEMCSR(kp, dn, new PKCS10Attributes());
+    }
+
+    public static String generatePEMCSR(KeyPair kp, String dn, PKCS10Attributes atts) throws GeneralSecurityException, IOException {
+        return generatePEMCSR(kp, dn, atts, "SHA256WithRSA");
+    }
+
+    public static String generatePEMCSR(KeyPair kp, String dn, PKCS10Attributes atts, String signature) throws GeneralSecurityException, IOException {
+        PKCS10 p10 = new PKCS10(kp.getPublic(), atts);
+        Signature s = Signature.getInstance(signature);
+        s.initSign(kp.getPrivate());
+        p10.encodeAndSign(new X500Name(dn), s);
+        return PEM.encode("CERTIFICATE REQUEST", p10.getEncoded());
+    }
+
+    static int count = 0;
+
+    private static Link l;
+
+    public static String createUniqueName() {
+        return "test" + System.currentTimeMillis() + "a" + (count++) + "u";
+    }
+
+    public static int countRegex(String text, String pattern) {
+        Pattern p = Pattern.compile(pattern);
+        Matcher m = p.matcher(text);
+        int i = 0;
+        while (m.find()) {
+            i++;
+        }
+        return i;
+    }
+}
index ab18ee2ed6ba38068bc0900da6bddeb5ad92b180..23db401f8221d0c50769c08acb893437e53ba7a3 100644 (file)
@@ -1,39 +1,63 @@
 package org.cacert.gigi.testUtils;
 
+import java.io.ByteArrayOutputStream;
 import java.io.CharArrayWriter;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.net.HttpURLConnection;
 import java.net.URLConnection;
 
 public class IOUtils {
-       private IOUtils() {
-
-       }
-       public static String readURL(URLConnection in) {
-               try {
-                       if (!in.getContentType().equals("text/html; charset=UTF-8")) {
-                               throw new Error("Unrecognized content-type: "
-                                               + in.getContentType());
-                       }
-                       return readURL(new InputStreamReader(in.getInputStream(), "UTF-8"));
-               } catch (IOException e) {
-                       throw new Error(e);
-               }
-
-       }
-       public static String readURL(Reader in) {
-               CharArrayWriter caw = new CharArrayWriter();
-               char[] buffer = new char[1024];
-               int len = 0;
-               try {
-                       while ((len = in.read(buffer)) > 0) {
-                               caw.write(buffer, 0, len);
-                       }
-                       return new String(caw.toCharArray());
-               } catch (IOException e) {
-                       throw new Error(e);
-               }
-
-       }
+
+    private IOUtils() {
+
+    }
+
+    public static String readURL(URLConnection in) {
+        try {
+            if ( !in.getContentType().equals("text/html; charset=UTF-8") && !in.getContentType().equals("text/plain; charset=UTF-8")) {
+                if (in instanceof HttpURLConnection && ((HttpURLConnection) in).getResponseCode() != 200) {
+                    System.err.println(readURL(new InputStreamReader(((HttpURLConnection) in).getErrorStream(), "UTF-8")));
+                }
+                throw new Error("Unrecognized content-type: " + in.getContentType());
+            }
+            return readURL(new InputStreamReader(in.getInputStream(), "UTF-8"));
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+
+    }
+
+    public static String readURL(Reader in) {
+        CharArrayWriter caw = new CharArrayWriter();
+        char[] buffer = new char[1024];
+        int len = 0;
+        try {
+            while ((len = in.read(buffer)) > 0) {
+                caw.write(buffer, 0, len);
+            }
+            in.close();
+            return new String(caw.toCharArray());
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+
+    }
+
+    public static byte[] readURL(InputStream in) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int len = 0;
+        try {
+            while ((len = in.read(buffer)) > 0) {
+                baos.write(buffer, 0, len);
+            }
+            in.close();
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
 }
index 39c1d370ea7bc70aad956c9943956679f127e161..1207df930b3cd6b70e0d353f40dab0d45807ca9e 100644 (file)
@@ -1,13 +1,15 @@
 package org.cacert.gigi.testUtils;
 
 public class InitTruststore {
-       private InitTruststore() {
-       }
-       static {
-               System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
-               System.setProperty("javax.net.ssl.trustStore", "config/cacerts.jks");
-       }
-       public static void run() {
-
-       }
+
+    private InitTruststore() {}
+
+    static {
+        System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
+        System.setProperty("javax.net.ssl.trustStore", "config/cacerts.jks");
+    }
+
+    public static void run() {
+
+    }
 }
index ce60a295832584c5c6b282af91bee8ae1de52838..9f22edf1940493f53af1c3900122fc0a5426beeb 100644 (file)
 package org.cacert.gigi.testUtils;
 
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 
 import java.io.BufferedReader;
 import java.io.DataOutputStream;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.HttpURLConnection;
 import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Socket;
 import java.net.URL;
+import java.net.URLConnection;
 import java.net.URLEncoder;
 import java.nio.file.Files;
 import java.nio.file.Paths;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
 import java.sql.SQLException;
+import java.util.Locale;
 import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509KeyManager;
 
 import org.cacert.gigi.DevelLauncher;
-import org.cacert.gigi.database.DatabaseConnection;
-import org.cacert.gigi.testUtils.TestEmailReciever.TestMail;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.database.SQLFileManager.ImportType;
+import org.cacert.gigi.dbObjects.CATS;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.DomainPingType;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.ObjectCache;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.pages.Manager;
+import org.cacert.gigi.pages.account.MyDetails;
+import org.cacert.gigi.pages.main.RegisterPage;
+import org.cacert.gigi.testUtils.TestEmailReceiver.TestMail;
 import org.cacert.gigi.util.DatabaseManager;
+import org.cacert.gigi.util.ServerConstants;
+import org.cacert.gigi.util.SimpleSigner;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
-public class ManagedTest {
-       private final String registerService = "/register";
-
-       private static TestEmailReciever ter;
-       private static Process gigi;
-       private static String url = "localhost:4443";
-
-       public static String getServerName() {
-               return url;
-       }
-       static Properties testProps = new Properties();
-       static {
-               InitTruststore.run();
-               HttpURLConnection.setFollowRedirects(false);
-       }
-
-       @BeforeClass
-       public static void connectToServer() {
-               try {
-                       testProps.load(new FileInputStream("config/test.properties"));
-                       if (!DatabaseConnection.isInited()) {
-                               DatabaseConnection.init(testProps);
-                       }
-                       System.out.println("... purging Database");
-                       DatabaseManager.run(new String[]{
-                                       testProps.getProperty("sql.driver"),
-                                       testProps.getProperty("sql.url"),
-                                       testProps.getProperty("sql.user"),
-                                       testProps.getProperty("sql.password")});
-
-                       String type = testProps.getProperty("type");
-                       if (type.equals("local")) {
-                               url = testProps.getProperty("server");
-                               String[] parts = testProps.getProperty("mail").split(":", 2);
-                               ter = new TestEmailReciever(new InetSocketAddress(parts[0],
-                                               Integer.parseInt(parts[1])));
-                               return;
-                       }
-                       url = "localhost:" + testProps.getProperty("serverPort");
-                       gigi = Runtime.getRuntime().exec(testProps.getProperty("java"));
-                       DataOutputStream toGigi = new DataOutputStream(
-                                       gigi.getOutputStream());
-                       System.out.println("... starting server");
-                       Properties mainProps = new Properties();
-                       mainProps.setProperty("host", "127.0.0.1");
-                       mainProps.setProperty("port", testProps.getProperty("serverPort"));
-                       mainProps.setProperty("emailProvider",
-                                       "org.cacert.gigi.email.TestEmailProvider");
-                       mainProps.setProperty("emailProvider.port", "8473");
-                       mainProps.setProperty("sql.driver",
-                                       testProps.getProperty("sql.driver"));
-                       mainProps.setProperty("sql.url", testProps.getProperty("sql.url"));
-                       mainProps
-                                       .setProperty("sql.user", testProps.getProperty("sql.user"));
-                       mainProps.setProperty("sql.password",
-                                       testProps.getProperty("sql.password"));
-
-                       byte[] cacerts = Files
-                                       .readAllBytes(Paths.get("config/cacerts.jks"));
-                       byte[] keystore = Files.readAllBytes(Paths
-                                       .get("config/keystore.pkcs12"));
-
-                       DevelLauncher.writeGigiConfig(toGigi, new byte[]{},
-                                       "changeit".getBytes(), mainProps, cacerts, keystore);
-                       toGigi.flush();
-                       // TODO wait for ready
-                       try {
-                               Thread.sleep(3000);
-                       } catch (InterruptedException e) {
-                               e.printStackTrace();
-                       }
-                       final BufferedReader br = new BufferedReader(new InputStreamReader(
-                                       gigi.getErrorStream()));
-                       String line;
-                       while ((line = br.readLine()) != null
-                                       && !line.contains("Server:main: Started")) {
-                               System.err.println(line);
-                       }
-                       new Thread() {
-                               @Override
-                               public void run() {
-                                       String line;
-                                       try {
-                                               while ((line = br.readLine()) != null) {
-                                                       System.err.println(line);
-                                               }
-                                       } catch (IOException e) {
-                                               e.printStackTrace();
-                                       }
-                               }
-                       }.start();
-                       System.err.println(line);
-                       if (line == null) {
-                               throw new Error("Server startup failed");
-                       }
-                       ter = new TestEmailReciever(
-                                       new InetSocketAddress("localhost", 8473));
-               } catch (IOException e) {
-                       throw new Error(e);
-               } catch (ClassNotFoundException e1) {
-                       e1.printStackTrace();
-               } catch (SQLException e1) {
-                       e1.printStackTrace();
-               }
-
-       }
-       @AfterClass
-       public static void tearDownServer() {
-               String type = testProps.getProperty("type");
-               if (type.equals("local")) {
-                       return;
-               }
-               gigi.destroy();
-       }
-
-       @After
-       public void removeMails() {
-               ter.reset();
-       }
-
-       public TestMail waitForMail() {
-               try {
-                       return ter.recieve();
-               } catch (InterruptedException e) {
-                       throw new Error(e);
-               }
-       }
-       public static TestEmailReciever getMailReciever() {
-               return ter;
-       }
-       public String runRegister(String param) throws IOException {
-               HttpURLConnection uc = (HttpURLConnection) new URL("https://"
-                               + getServerName() + registerService).openConnection();
-               uc.setDoOutput(true);
-               uc.getOutputStream().write(param.getBytes());
-               String d = IOUtils.readURL(uc);
-               return d;
-       }
-       public String fetchStartErrorMessage(String d) throws IOException {
-               String formFail = "<div class='formError'>";
-               int idx = d.indexOf(formFail);
-               assertNotEquals(-1, idx);
-               String startError = d.substring(idx + formFail.length(), idx + 100)
-                               .trim();
-               return startError;
-       }
-
-       public void registerUser(String firstName, String lastName, String email,
-                       String password) {
-               try {
-                       String query = "fname=" + URLEncoder.encode(firstName, "UTF-8")
-                                       + "&lname=" + URLEncoder.encode(lastName, "UTF-8")
-                                       + "&email=" + URLEncoder.encode(email, "UTF-8")
-                                       + "&pword1=" + URLEncoder.encode(password, "UTF-8")
-                                       + "&pword2=" + URLEncoder.encode(password, "UTF-8")
-                                       + "&day=1&month=1&year=1910&cca_agree=1";
-                       String data = fetchStartErrorMessage(runRegister(query));
-                       assertTrue(data, data.startsWith("</div>"));
-               } catch (UnsupportedEncodingException e) {
-                       throw new Error(e);
-               } catch (IOException e) {
-                       throw new Error(e);
-               }
-       }
-       public int createVerifiedUser(String firstName, String lastName,
-                       String email, String password) {
-               registerUser(firstName, lastName, email, password);
-               try {
-                       TestMail tm = ter.recieve();
-                       String verifyLink = tm.extractLink();
-                       String[] parts = verifyLink.split("\\?");
-                       URL u = new URL("https://" + getServerName() + "/verify?"
-                                       + parts[1]);
-                       u.openStream().close();;
-                       PreparedStatement ps = DatabaseConnection.getInstance().prepare(
-                                       "SELECT id FROM users where email=?");
-                       ps.setString(1, email);
-                       ResultSet rs = ps.executeQuery();
-                       if (rs.next()) {
-                               return rs.getInt(1);
-                       }
-                       throw new Error();
-               } catch (InterruptedException e) {
-                       throw new Error(e);
-               } catch (IOException e) {
-                       throw new Error(e);
-               } catch (SQLException e) {
-                       throw new Error(e);
-               }
-       }
-       public int createAssuranceUser(String firstName, String lastName,
-                       String email, String password) {
-               int uid = createVerifiedUser(firstName, lastName, email, password);
-               try {
-                       PreparedStatement ps = DatabaseConnection
-                                       .getInstance()
-                                       .prepare(
-                                                       "INSERT INTO `cats_passed` SET `user_id`=?, `variant_id`=?");
-                       ps.setInt(1, uid);
-                       ps.setInt(2, 0);
-                       ps.execute();
-                       ps = DatabaseConnection.getInstance().prepare(
-                                       "INSERT INTO `notary` SET `from`=?, `to`=?, points='100'");
-                       ps.setInt(1, uid);
-                       ps.setInt(2, uid);
-                       ps.execute();
-
-               } catch (SQLException e) {
-                       throw new Error(e);
-               }
-               return uid;
-       }
-       static int count = 0;
-       public String createUniqueName() {
-               return "test" + System.currentTimeMillis() + "a" + (count++);
-       }
-       public String login(String email, String pw) throws IOException {
-               URL u = new URL("https://" + getServerName() + "/login");
-               HttpURLConnection huc = (HttpURLConnection) u.openConnection();
-               huc.setDoOutput(true);
-               OutputStream os = huc.getOutputStream();
-               String data = "username=" + URLEncoder.encode(email, "UTF-8")
-                               + "&password=" + URLEncoder.encode(pw, "UTF-8");
-               os.write(data.getBytes());
-               os.flush();
-               String headerField = huc.getHeaderField("Set-Cookie");
-               headerField = headerField.substring(0, headerField.indexOf(';'));
-               return headerField;
-       }
+/**
+ * Base class for test suites who require a launched Gigi instance. The instance
+ * is cleared once per test suite.
+ */
+public class ManagedTest extends ConfiguredTest {
+
+    static {
+        System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
+    }
+
+    /**
+     * Some password that fulfills the password criteria.
+     */
+    public static final String TEST_PASSWORD = "xvXV12°§";
+
+    public static final String DIFFICULT_CHARS = "ÜÖÄß𐀀";
+
+    private static TestEmailReceiver ter;
+
+    private static Process gigi;
+
+    private static String url = "localhost:4443";
+
+    private static String acceptLanguage = null;
+
+    public static void setAcceptLanguage(String acceptLanguage) {
+        ManagedTest.acceptLanguage = acceptLanguage;
+    }
+
+    public static String getServerName() {
+        return url;
+    }
+
+    static {
+        InitTruststore.run();
+        HttpURLConnection.setFollowRedirects(false);
+    }
+
+    @BeforeClass
+    public static void initEnvironment() {
+        try {
+            ConfiguredTest.initEnvironment();
+
+            purgeDatabase();
+            String type = testProps.getProperty("type");
+            Properties mainProps = generateMainProps();
+            ServerConstants.init(mainProps);
+            if (type.equals("local")) {
+                url = testProps.getProperty("name.www") + ":" + testProps.getProperty("serverPort.https");
+                String[] parts = testProps.getProperty("mail").split(":", 2);
+                ter = new TestEmailReceiver(new InetSocketAddress(parts[0], Integer.parseInt(parts[1])));
+                ter.start();
+                return;
+            }
+            url = testProps.getProperty("name.www") + ":" + testProps.getProperty("serverPort.https");
+            gigi = Runtime.getRuntime().exec(testProps.getProperty("java"));
+            DataOutputStream toGigi = new DataOutputStream(gigi.getOutputStream());
+            System.out.println("... starting server");
+
+            byte[] cacerts = Files.readAllBytes(Paths.get("config/cacerts.jks"));
+            byte[] keystore = Files.readAllBytes(Paths.get("config/keystore.pkcs12"));
+
+            DevelLauncher.writeGigiConfig(toGigi, "changeit".getBytes("UTF-8"), "changeit".getBytes("UTF-8"), mainProps, cacerts, keystore);
+            toGigi.flush();
+
+            final BufferedReader br = new BufferedReader(new InputStreamReader(gigi.getErrorStream(), "UTF-8"));
+            String line;
+            while ((line = br.readLine()) != null && !line.contains("System successfully started.")) {
+                System.err.println(line);
+            }
+            new Thread() {
+
+                @Override
+                public void run() {
+                    String line;
+                    try {
+                        while ((line = br.readLine()) != null) {
+                            System.err.println(line);
+                        }
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }.start();
+            if (line == null) {
+                throw new Error("Server startup failed");
+            }
+            ter = new TestEmailReceiver(new InetSocketAddress("localhost", 8473));
+            ter.start();
+            SimpleSigner.runSigner();
+        } catch (IOException e) {
+            throw new Error(e);
+        } catch (SQLException e1) {
+            e1.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    public static void purgeDatabase() throws SQLException, IOException {
+        System.out.print("... resetting Database");
+        long ms = System.currentTimeMillis();
+        try {
+            DatabaseManager.run(new String[] {
+                    testProps.getProperty("sql.driver"), testProps.getProperty("sql.url"), testProps.getProperty("sql.user"), testProps.getProperty("sql.password")
+            }, ImportType.TRUNCATE);
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        }
+        System.out.println(" in " + (System.currentTimeMillis() - ms) + " ms");
+        clearCaches();
+    }
+
+    public static void clearCaches() throws IOException {
+        ObjectCache.clearAllCaches();
+        // String type = testProps.getProperty("type");
+        URL u = new URL("https://" + getServerName() + "/manage");
+        u.openConnection().getHeaderField("Location");
+    }
+
+    private static Properties generateMainProps() {
+        Properties mainProps = new Properties();
+        mainProps.setProperty("testrunner", "true");
+        mainProps.setProperty("host", "127.0.0.1");
+        mainProps.setProperty("name.secure", testProps.getProperty("name.secure"));
+        mainProps.setProperty("name.www", testProps.getProperty("name.www"));
+        mainProps.setProperty("name.static", testProps.getProperty("name.static"));
+        mainProps.setProperty("name.api", testProps.getProperty("name.api"));
+
+        mainProps.setProperty("https.port", testProps.getProperty("serverPort.https"));
+        mainProps.setProperty("http.port", testProps.getProperty("serverPort.http"));
+        mainProps.setProperty("emailProvider", "org.cacert.gigi.email.TestEmailProvider");
+        mainProps.setProperty("emailProvider.port", "8473");
+        mainProps.setProperty("sql.driver", testProps.getProperty("sql.driver"));
+        mainProps.setProperty("sql.url", testProps.getProperty("sql.url"));
+        mainProps.setProperty("sql.user", testProps.getProperty("sql.user"));
+        mainProps.setProperty("sql.password", testProps.getProperty("sql.password"));
+        mainProps.setProperty("testing", "true");
+        return mainProps;
+    }
+
+    @AfterClass
+    public static void tearDownServer() {
+        String type = testProps.getProperty("type");
+        ter.destroy();
+        if (type.equals("local")) {
+            return;
+        }
+        gigi.destroy();
+        try {
+            SimpleSigner.stopSigner();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public final String uniq = createUniqueName();
+
+    @After
+    public void removeMails() {
+        ter.reset();
+    }
+
+    @After
+    public void clearAcceptLanguage() {
+        ManagedTest.setAcceptLanguage(null);
+    }
+
+    public static TestEmailReceiver getMailReciever() {
+        return ter;
+    }
+
+    public static String runRegister(String param) throws IOException {
+        URL regist = new URL("https://" + getServerName() + RegisterPage.PATH);
+        HttpURLConnection uc = (HttpURLConnection) regist.openConnection();
+        HttpURLConnection csrfConn = (HttpURLConnection) regist.openConnection();
+        if (acceptLanguage != null) {
+            csrfConn.setRequestProperty("Accept-Language", acceptLanguage);
+            uc.setRequestProperty("Accept-Language", acceptLanguage);
+        }
+
+        String headerField = csrfConn.getHeaderField("Set-Cookie");
+        headerField = stripCookie(headerField);
+
+        String csrf = getCSRF(csrfConn);
+        uc.addRequestProperty("Cookie", headerField);
+        uc.setDoOutput(true);
+        uc.getOutputStream().write((param + "&csrf=" + csrf).getBytes("UTF-8"));
+        String d = IOUtils.readURL(uc);
+        return d;
+    }
+
+    public static String fetchStartErrorMessage(String d) throws IOException {
+        String formFail = "<div class='formError'>";
+        int idx = d.indexOf(formFail);
+        if (idx == -1) {
+            return null;
+        }
+        String startError = d.substring(idx + formFail.length(), idx + 100).trim();
+        return startError;
+    }
+
+    public static void registerUser(String firstName, String lastName, String email, String password) {
+        try {
+            String query = "fname=" + URLEncoder.encode(firstName, "UTF-8") + "&lname=" + URLEncoder.encode(lastName, "UTF-8") + "&email=" + URLEncoder.encode(email, "UTF-8") + "&pword1=" + URLEncoder.encode(password, "UTF-8") + "&pword2=" + URLEncoder.encode(password, "UTF-8") + "&day=1&month=1&year=1910&tos_agree=1";
+            String data = fetchStartErrorMessage(runRegister(query));
+            assertNull(data);
+        } catch (UnsupportedEncodingException e) {
+            throw new Error(e);
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
+    public static int createVerifiedUser(String firstName, String lastName, String email, String password) {
+        registerUser(firstName, lastName, email, password);
+        try {
+            ter.receive().verify();
+
+            try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id` FROM `users` WHERE `email`=?")) {
+                ps.setString(1, email);
+
+                GigiResultSet rs = ps.executeQuery();
+                if (rs.next()) {
+                    return rs.getInt(1);
+                }
+            }
+
+            throw new Error();
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
+    public static void grant(String email, Group g) throws IOException {
+        HttpURLConnection huc = (HttpURLConnection) new URL("https://" + getServerName() + Manager.PATH).openConnection();
+        huc.setDoOutput(true);
+        huc.getOutputStream().write(("addpriv=y&priv=" + URLEncoder.encode(g.getDatabaseName(), "UTF-8") + "&email=" + URLEncoder.encode(email, "UTF-8")).getBytes("UTF-8"));
+        assertEquals(200, huc.getResponseCode());
+    }
+
+    /**
+     * Creates a new user with 100 Assurance points given by an (invalid)
+     * assurance.
+     * 
+     * @param firstName
+     *            the first name
+     * @param lastName
+     *            the last name
+     * @param email
+     *            the email
+     * @param password
+     *            the password
+     * @return a new userid.
+     */
+    public static int createAssuranceUser(String firstName, String lastName, String email, String password) {
+        int uid = createVerifiedUser(firstName, lastName, email, password);
+
+        makeAssurer(uid);
+
+        return uid;
+    }
+
+    public static void makeAssurer(int uid) {
+        try (GigiPreparedStatement ps1 = new GigiPreparedStatement("INSERT INTO `cats_passed` SET `user_id`=?, `variant_id`=?")) {
+            ps1.setInt(1, uid);
+            ps1.setInt(2, CATS.ASSURER_CHALLENGE_ID);
+            ps1.execute();
+        }
+
+        try (GigiPreparedStatement ps2 = new GigiPreparedStatement("INSERT INTO `notary` SET `from`=?, `to`=?, points='100'")) {
+            ps2.setInt(1, uid);
+            ps2.setInt(2, uid);
+            ps2.execute();
+        }
+    }
+
+    protected static String stripCookie(String headerField) {
+        return headerField.substring(0, headerField.indexOf(';'));
+    }
+
+    public static final String SECURE_REFERENCE = MyDetails.PATH;
+
+    public static boolean isLoggedin(String cookie) throws IOException {
+        URL u = new URL("https://" + getServerName() + SECURE_REFERENCE);
+        HttpURLConnection huc = (HttpURLConnection) u.openConnection();
+        huc.addRequestProperty("Cookie", cookie);
+        return huc.getResponseCode() == 200;
+    }
+
+    public static String login(String email, String pw) throws IOException {
+        URL u = new URL("https://" + getServerName() + "/login");
+        HttpURLConnection huc = (HttpURLConnection) u.openConnection();
+
+        String csrf = getCSRF(huc);
+        String headerField = stripCookie(huc.getHeaderField("Set-Cookie"));
+
+        huc = (HttpURLConnection) u.openConnection();
+        cookie(huc, headerField);
+        huc.setDoOutput(true);
+        OutputStream os = huc.getOutputStream();
+        String data = "username=" + URLEncoder.encode(email, "UTF-8") + "&password=" + URLEncoder.encode(pw, "UTF-8") + "&csrf=" + URLEncoder.encode(csrf, "UTF-8");
+        os.write(data.getBytes("UTF-8"));
+        os.flush();
+        headerField = huc.getHeaderField("Set-Cookie");
+        if (headerField == null) {
+            return "";
+        }
+        return stripCookie(headerField);
+    }
+
+    public static String login(final PrivateKey pk, final X509Certificate ce) throws NoSuchAlgorithmException, KeyManagementException, IOException, MalformedURLException {
+
+        HttpURLConnection connection = (HttpURLConnection) new URL("https://" + getServerName().replaceFirst("^www.", "secure.") + "/login").openConnection();
+        authenticateClientCert(pk, ce, connection);
+        if (connection.getResponseCode() == 302) {
+            assertEquals("https://" + getServerName().replaceFirst("^www.", "secure.").replaceFirst(":443$", "") + "/", connection.getHeaderField("Location").replaceFirst(":443$", ""));
+            return stripCookie(connection.getHeaderField("Set-Cookie"));
+        } else {
+            return null;
+        }
+    }
+
+    public static void authenticateClientCert(final PrivateKey pk, final X509Certificate ce, HttpURLConnection connection) throws NoSuchAlgorithmException, KeyManagementException {
+        KeyManager km = new X509KeyManager() {
+
+            @Override
+            public String chooseClientAlias(String[] arg0, Principal[] arg1, Socket arg2) {
+                return "client";
+            }
+
+            @Override
+            public String chooseServerAlias(String arg0, Principal[] arg1, Socket arg2) {
+                return null;
+            }
+
+            @Override
+            public X509Certificate[] getCertificateChain(String arg0) {
+                return new X509Certificate[] {
+                        ce
+                };
+            }
+
+            @Override
+            public String[] getClientAliases(String arg0, Principal[] arg1) {
+                return new String[] {
+                        "client"
+                };
+            }
+
+            @Override
+            public PrivateKey getPrivateKey(String arg0) {
+                if (arg0.equals("client")) {
+                    return pk;
+                }
+                return null;
+            }
+
+            @Override
+            public String[] getServerAliases(String arg0, Principal[] arg1) {
+                return new String[] {
+                        "client"
+                };
+            }
+        };
+        SSLContext sc = SSLContext.getInstance("TLS");
+        sc.init(new KeyManager[] {
+                km
+        }, null, null);
+        if (connection instanceof HttpsURLConnection) {
+            ((HttpsURLConnection) connection).setSSLSocketFactory(sc.getSocketFactory());
+        }
+    }
+
+    public static String getCSRF(URLConnection u) throws IOException {
+        return getCSRF(u, 0);
+    }
+
+    public static String getCSRF(URLConnection u, int formIndex) throws IOException {
+        String content = IOUtils.readURL(u);
+        return getCSRF(formIndex, content);
+    }
+
+    public static String getCSRF(int formIndex, String content) throws Error {
+        Pattern p = Pattern.compile("<input type='hidden' name='csrf' value='([^']+)'>");
+        Matcher m = p.matcher(content);
+        for (int i = 0; i < formIndex + 1; i++) {
+            if ( !m.find()) {
+                throw new Error("No CSRF Token");
+            }
+        }
+        return m.group(1);
+    }
+
+    public static String executeBasicWebInteraction(String cookie, String path, String query) throws MalformedURLException, UnsupportedEncodingException, IOException {
+        return executeBasicWebInteraction(cookie, path, query, 0);
+    }
+
+    public static String executeBasicWebInteraction(String cookie, String path, String query, int formIndex) throws IOException, MalformedURLException, UnsupportedEncodingException {
+        URLConnection uc = post(cookie, path, query, formIndex);
+        String error = fetchStartErrorMessage(IOUtils.readURL(uc));
+        return error;
+    }
+
+    public static HttpURLConnection post(String cookie, String path, String query, int formIndex) throws IOException, MalformedURLException, UnsupportedEncodingException {
+        URLConnection uc = new URL("https://" + getServerName() + path).openConnection();
+        uc.addRequestProperty("Cookie", cookie);
+        String csrf = getCSRF(uc, formIndex);
+
+        uc = new URL("https://" + getServerName() + path).openConnection();
+        uc.addRequestProperty("Cookie", cookie);
+        uc.setDoOutput(true);
+        OutputStream os = uc.getOutputStream();
+        os.write(("csrf=" + URLEncoder.encode(csrf, "UTF-8") + "&" //
+                + query//
+        ).getBytes("UTF-8"));
+        os.flush();
+        return (HttpURLConnection) uc;
+    }
+
+    public static HttpURLConnection get(String cookie, String path) throws IOException {
+        URLConnection uc = new URL("https://" + getServerName() + path).openConnection();
+        uc.addRequestProperty("Cookie", cookie);
+        return (HttpURLConnection) uc;
+    }
+
+    public static EmailAddress createVerifiedEmail(User u) throws InterruptedException, GigiApiException {
+        EmailAddress adrr = new EmailAddress(u, createUniqueName() + "test@test.tld", Locale.ENGLISH);
+        TestMail testMail = getMailReciever().receive();
+        assertEquals(adrr.getAddress(), testMail.getTo());
+        String hash = testMail.extractLink().substring(testMail.extractLink().lastIndexOf('=') + 1);
+        adrr.verify(hash);
+        getMailReciever().clearMails();
+        return adrr;
+    }
+
+    public static URLConnection cookie(URLConnection openConnection, String cookie) {
+        openConnection.setRequestProperty("Cookie", cookie);
+        return openConnection;
+    }
+
+    public static void verify(Domain d) {
+        try {
+            System.out.println(d.getId());
+            d.addPing(DomainPingType.EMAIL, "admin");
+            TestMail testMail = ter.receive();
+            testMail.verify();
+            assertTrue(d.isVerified());
+        } catch (GigiApiException e) {
+            throw new Error(e);
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
 }
diff --git a/tests/org/cacert/gigi/testUtils/OrgTest.java b/tests/org/cacert/gigi/testUtils/OrgTest.java
new file mode 100644 (file)
index 0000000..8ae7e2c
--- /dev/null
@@ -0,0 +1,22 @@
+package org.cacert.gigi.testUtils;
+
+import java.io.IOException;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Organisation;
+
+public class OrgTest extends ClientTest {
+
+    public OrgTest() throws IOException {
+        makeAssurer(u.getId());
+        u.grantGroup(u, Group.ORGASSURER);
+        clearCaches();
+        cookie = login(email, TEST_PASSWORD);
+    }
+
+    public Organisation createUniqueOrg() throws GigiApiException {
+        Organisation o1 = new Organisation(createUniqueName(), "st", "pr", "city", "test@example.com", "", "", u);
+        return o1;
+    }
+}
diff --git a/tests/org/cacert/gigi/testUtils/PingTest.java b/tests/org/cacert/gigi/testUtils/PingTest.java
new file mode 100644 (file)
index 0000000..0d15f2e
--- /dev/null
@@ -0,0 +1,82 @@
+package org.cacert.gigi.testUtils;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.sql.SQLException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.pages.account.domain.DomainOverview;
+import org.junit.After;
+
+/**
+ * Base class for test suites that check extensively if the domain-ping
+ * functionality wroks as expected.
+ */
+public abstract class PingTest extends ClientTest {
+
+    protected String csrf;
+
+    protected static void updateService(String token, String value, String action) throws IOException, MalformedURLException {
+        String manage = getTestProps().getProperty("domain.manage");
+        assumeNotNull(manage);
+        String url = manage + "t1=" + token + "&t2=" + value + "&action=" + action;
+        assertEquals(200, ((HttpURLConnection) new URL(url).openConnection()).getResponseCode());
+    }
+
+    protected void waitForPings(int count) throws SQLException, InterruptedException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT COUNT(*) FROM `domainPinglog`")) {
+            long start = System.currentTimeMillis();
+            while (System.currentTimeMillis() - start < 10000) {
+                GigiResultSet rs = ps.executeQuery();
+                rs.next();
+                if (rs.getInt(1) >= count) {
+                    break;
+                }
+                Thread.sleep(200);
+            }
+        }
+    }
+
+    protected String sendDomainForm(String content) throws IOException, MalformedURLException {
+        URLConnection openConnection = get(DomainOverview.PATH);
+        openConnection.setDoOutput(true);
+        openConnection.getOutputStream().write(content.getBytes("UTF-8"));
+        openConnection.getHeaderField("Location");
+        if (((HttpURLConnection) openConnection).getResponseCode() != 302) {
+            throw new Error(IOUtils.readURL(openConnection));
+        }
+
+        String newcontent = IOUtils.readURL(get(DomainOverview.PATH));
+        Pattern dlink = Pattern.compile(DomainOverview.PATH + "([0-9]+)'>");
+        Matcher m1 = dlink.matcher(newcontent);
+        if ( !m1.find()) {
+            throw new Error(newcontent);
+        }
+        return DomainOverview.PATH + m1.group(1);
+    }
+
+    protected Matcher initailizeDomainForm() throws IOException, Error {
+        String content1 = IOUtils.readURL(get(DomainOverview.PATH));
+        csrf = getCSRF(1, content1);
+
+        Pattern p = Pattern.compile("([A-Za-z0-9]+)._cacert._auth IN TXT ([A-Za-z0-9]+)");
+        Matcher m = p.matcher(content1);
+        m.find();
+        return m;
+    }
+
+    @After
+    public void purgeDbAfterTest() throws SQLException, IOException {
+        purgeDatabase();
+    }
+
+}
diff --git a/tests/org/cacert/gigi/testUtils/RegisteredUser.java b/tests/org/cacert/gigi/testUtils/RegisteredUser.java
new file mode 100644 (file)
index 0000000..a5b5172
--- /dev/null
@@ -0,0 +1,32 @@
+package org.cacert.gigi.testUtils;
+
+import org.cacert.gigi.dbObjects.User;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class RegisteredUser implements TestRule {
+
+    User u;
+
+    @Override
+    public Statement apply(final Statement base, Description description) {
+        return new Statement() {
+
+            @Override
+            public void evaluate() throws Throwable {
+                u = User.getById(ManagedTest.createVerifiedUser("fn", "ln", ManagedTest.createUniqueName() + "@example.org", ManagedTest.TEST_PASSWORD));
+                try {
+                    base.evaluate();
+                } finally {
+
+                }
+            }
+        };
+    }
+
+    public User getUser() {
+        return u;
+    }
+
+}
diff --git a/tests/org/cacert/gigi/testUtils/TestEmailReceiver.java b/tests/org/cacert/gigi/testUtils/TestEmailReceiver.java
new file mode 100644 (file)
index 0000000..77c3e2d
--- /dev/null
@@ -0,0 +1,278 @@
+package org.cacert.gigi.testUtils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.cacert.gigi.email.EmailProvider;
+import org.cacert.gigi.email.TestEmailProvider;
+
+/**
+ * This class reveives emails from the current system under test. It is the
+ * counterpart to the {@link TestEmailProvider} who is loaded into the system to
+ * intercept the emails. This class resides in the VM that executes the
+ * testcases and supplies the intercepted emails to the current test case.
+ */
+public final class TestEmailReceiver extends EmailProvider implements Runnable {
+
+    /**
+     * An email that has been intercepted.
+     */
+    public static class TestMail {
+
+        String to;
+
+        String subject;
+
+        String message;
+
+        String from;
+
+        String replyto;
+
+        public TestMail(String to, String subject, String message, String from, String replyto) {
+            this.to = to;
+            this.subject = subject;
+            this.message = message;
+            this.from = from;
+            this.replyto = replyto;
+        }
+
+        public String getTo() {
+            return to;
+        }
+
+        public String getSubject() {
+            return subject;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public String getFrom() {
+            return from;
+        }
+
+        public String getReplyto() {
+            return replyto;
+        }
+
+        public String extractLink() {
+            Pattern link = Pattern.compile("https?://[^\\s]+(?=\\s)");
+            Matcher m = link.matcher(getMessage());
+            m.find();
+            return m.group(0);
+        }
+
+        public void verify() throws IOException {
+            String link = extractLink();
+            String[] parts = link.split("\\?");
+            URL u = new URL("https://" + ManagedTest.getServerName() + "/verify?" + parts[1]);
+
+            URLConnection csrfConn = u.openConnection();
+            String csrf = ManagedTest.getCSRF(csrfConn, 0);
+
+            u = new URL("https://" + ManagedTest.getServerName() + "/verify");
+            URLConnection uc = u.openConnection();
+            ManagedTest.cookie(uc, ManagedTest.stripCookie(csrfConn.getHeaderField("Set-Cookie")));
+            uc.setDoOutput(true);
+            uc.getOutputStream().write((parts[1] + "&csrf=" + csrf).getBytes("UTF-8"));
+            uc.connect();
+            uc.getInputStream().close();
+        }
+
+    }
+
+    private Socket s;
+
+    private DataInputStream dis;
+
+    private DataOutputStream dos;
+
+    /**
+     * Creates a new TestEmailReceiver based on the address where the
+     * {@link TestEmailProvider} is listening. This class is only ready after
+     * {@link #start()} has been called.
+     * 
+     * @param target
+     *            the address where the {@link TestEmailProvider} is listening.
+     * @throws IOException
+     *             if the connection cannot be opened
+     */
+    public TestEmailReceiver(SocketAddress target) throws IOException {
+        s = new Socket();
+        s.connect(target);
+        s.setKeepAlive(true);
+        s.setSoTimeout(1000 * 60 * 60);
+        dis = new DataInputStream(s.getInputStream());
+        dos = new DataOutputStream(s.getOutputStream());
+        setInstance(this);
+    }
+
+    /**
+     * Spawns a new {@link Thread} that reads incoming {@link TestMail}s.
+     * 
+     * @see #destroy()
+     */
+    public void start() {
+        new Thread(this, "Mail reciever").start();
+    }
+
+    private LinkedBlockingQueue<TestMail> mails = new LinkedBlockingQueue<TestEmailReceiver.TestMail>();
+
+    /**
+     * Retrieves an outgoing mail from the system. The method will return a
+     * {@link TestMail} or fail.
+     * 
+     * @return The intercepted {@link TestMail}
+     * @see #poll()
+     */
+    public TestMail receive() {
+        TestMail poll;
+
+        try {
+            poll = mails.poll(60, TimeUnit.SECONDS);
+
+        } catch (InterruptedException e) {
+            throw new AssertionError("Interrupted while recieving mails");
+        }
+        if (poll == null) {
+            throw new AssertionError("Mail recieving timed out");
+        }
+
+        return poll;
+    }
+
+    /**
+     * Retrieves an outgoing mail from the system or returns <code>null</code>
+     * if there was no mail sent in 30 seconds.
+     * 
+     * @return The intercepted {@link TestMail} or <code>null</code> if no mail
+     *         has been sent.
+     * @see #receive()
+     */
+    public TestMail poll() {
+        return mails.poll();
+    }
+
+    @Override
+    public void run() {
+        try {
+            while (true) {
+                String type = dis.readUTF();
+                if (type.equals("mail")) {
+                    String to = dis.readUTF();
+                    String subject = dis.readUTF();
+                    String message = dis.readUTF();
+                    String from = dis.readUTF();
+                    String replyto = dis.readUTF();
+                    mails.add(new TestMail(to, subject, message, from, replyto));
+                } else if (type.equals("challengeAddrBox")) {
+                    String email = dis.readUTF();
+                    dos.writeUTF(quickEmailCheck(email));
+                    dos.flush();
+                } else if (type.equals("ping")) {
+                } else {
+                    System.err.println("Unknown type: " + type);
+                }
+            }
+        } catch (IOException e) {
+            if ( !closed) {
+                e.printStackTrace();
+            }
+        }
+
+    }
+
+    private String quickEmailCheck(String email) throws IOException {
+        if (approveRegex.matcher(email).matches()) {
+            return "OK";
+        } else {
+            return error;
+        }
+    }
+
+    String error = "FAIL";
+
+    /**
+     * Sets the error that will be sent back to incoming "fast mail checks" that
+     * only check for the availability of a mailbox.
+     * 
+     * @param error
+     *            the error Massage to return in
+     *            {@link EmailProvider#checkEmailServer(int, String)}
+     */
+    public void setEmailCheckError(String error) {
+        this.error = error;
+    }
+
+    private Pattern approveRegex = Pattern.compile(".*");
+
+    /**
+     * Specifies a pattern that will be used for incoming
+     * {@link EmailProvider#checkEmailServer(int, String)} calls to determine
+     * whether the mailbox should exist.
+     * 
+     * @param approveRegex
+     *            the regex that will perform the check
+     */
+    public void setApproveRegex(Pattern approveRegex) {
+        this.approveRegex = approveRegex;
+    }
+
+    /**
+     * Removes all queued mails.
+     */
+    public void clearMails() {
+        mails.clear();
+    }
+
+    /**
+     * Resets this class to its initial state
+     * 
+     * @see #clearMails()
+     * @see #setApproveRegex(Pattern)
+     * @see #setEmailCheckError(String)
+     */
+    public void reset() {
+        clearMails();
+        error = "FAIL";
+        approveRegex = Pattern.compile(".*");
+    }
+
+    private boolean closed = false;
+
+    /**
+     * stops reading for incoming messages
+     * 
+     * @see #start()
+     */
+    public void destroy() {
+        try {
+            closed = true;
+            s.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public String checkEmailServer(int forUid, String address) throws IOException {
+        return quickEmailCheck(address);
+    }
+
+    @Override
+    public void sendmail(String to, String subject, String message, String from, String replyto, String toname, String fromname, String errorsto, boolean extra) throws IOException {
+        mails.add(new TestMail(to, subject, message, from, replyto));
+    }
+
+}
diff --git a/tests/org/cacert/gigi/testUtils/TestEmailReciever.java b/tests/org/cacert/gigi/testUtils/TestEmailReciever.java
deleted file mode 100644 (file)
index e0131a3..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-package org.cacert.gigi.testUtils;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.net.SocketAddress;
-import java.net.Socket;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class TestEmailReciever implements Runnable {
-       public class TestMail {
-               String to;
-               String subject;
-               String message;
-               String from;
-               String replyto;
-               public TestMail(String to, String subject, String message, String from,
-                               String replyto) {
-                       this.to = to;
-                       this.subject = subject;
-                       this.message = message;
-                       this.from = from;
-                       this.replyto = replyto;
-               }
-               public String getTo() {
-                       return to;
-               }
-               public String getSubject() {
-                       return subject;
-               }
-               public String getMessage() {
-                       return message;
-               }
-               public String getFrom() {
-                       return from;
-               }
-               public String getReplyto() {
-                       return replyto;
-               }
-               public String extractLink() {
-                       Pattern link = Pattern.compile("http://[^\\s]+(?=\\s)");
-                       Matcher m = link.matcher(getMessage());
-                       m.find();
-                       return m.group(0);
-               }
-
-       }
-       private Socket s;
-       private DataInputStream dis;
-       private DataOutputStream dos;
-
-       public TestEmailReciever(SocketAddress target) throws IOException {
-               s = new Socket();
-               s.connect(target);
-               s.setKeepAlive(true);
-               s.setSoTimeout(1000 * 60 * 60);
-               dis = new DataInputStream(s.getInputStream());
-               dos = new DataOutputStream(s.getOutputStream());
-               new Thread(this).start();
-       }
-       LinkedBlockingQueue<TestMail> mails = new LinkedBlockingQueue<TestEmailReciever.TestMail>();
-
-       public TestMail recieve() throws InterruptedException {
-               return mails.poll(5, TimeUnit.SECONDS);
-       }
-       @Override
-       public void run() {
-               try {
-                       while (true) {
-                               String type = dis.readUTF();
-                               if (type.equals("mail")) {
-                                       String to = dis.readUTF();
-                                       String subject = dis.readUTF();
-                                       String message = dis.readUTF();
-                                       String from = dis.readUTF();
-                                       String replyto = dis.readUTF();
-                                       mails.add(new TestMail(to, subject, message, from, replyto));
-                               } else if (type.equals("challengeAddrBox")) {
-                                       String email = dis.readUTF();
-                                       if (approveRegex.matcher(email).matches()) {
-                                               System.out.println("approving mbox: " + email);
-                                               dos.writeUTF("OK");
-                                       } else {
-                                               System.out.println("rejecting mbox: " + email);
-                                               dos.writeUTF("FAIL");
-                                       }
-                                       dos.flush();
-                               } else if (type.equals("ping")) {
-                               } else {
-                                       System.err.println("Unknown type: " + type);
-                               }
-                       }
-               } catch (IOException e) {
-                       e.printStackTrace();
-               }
-
-       }
-       Pattern approveRegex = Pattern.compile(".*");
-       public void setApproveRegex(Pattern approveRegex) {
-               this.approveRegex = approveRegex;
-       }
-
-       public void clearMails() {
-               mails.clear();
-       }
-       public void reset() {
-               clearMails();
-               approveRegex = Pattern.compile(".*");
-       }
-
-}
index 7875df935896a6d50c8990585df4d33dfa4e018c..5d3a2aed493d1731dff21a468992d44b4845729d 100644 (file)
@@ -6,21 +6,23 @@ import org.junit.Test;
 
 public class TestHTMLEncoder {
 
-       @Test
-       public void testEncodeSimpleString() {
-               assertEquals("1234_ä", HTMLEncoder.encodeHTML("1234_ä"));
-       }
-       @Test
-       public void testEncodeQuotes() {
-               assertEquals("\\&quot;_ä.", HTMLEncoder.encodeHTML("\\\"_ä."));
-       }
-       @Test
-       public void testEncodeTagString() {
-               assertEquals("&lt;td class=&quot;&amp;amp;&quot;&gt;",
-                               HTMLEncoder.encodeHTML("<td class=\"&amp;\">"));
-       }
-       @Test
-       public void testEncodeSingleQuoteString() {
-               assertEquals("&#39;&amp;#39;", HTMLEncoder.encodeHTML("'&#39;"));
-       }
+    @Test
+    public void testEncodeSimpleString() {
+        assertEquals("1234_ä", HTMLEncoder.encodeHTML("1234_ä"));
+    }
+
+    @Test
+    public void testEncodeQuotes() {
+        assertEquals("\\&quot;_ä.", HTMLEncoder.encodeHTML("\\\"_ä."));
+    }
+
+    @Test
+    public void testEncodeTagString() {
+        assertEquals("&lt;td class=&quot;&amp;amp;&quot;&gt;", HTMLEncoder.encodeHTML("<td class=\"&amp;\">"));
+    }
+
+    @Test
+    public void testEncodeSingleQuoteString() {
+        assertEquals("&#39;&amp;#39;", HTMLEncoder.encodeHTML("'&#39;"));
+    }
 }
diff --git a/tests/org/cacert/gigi/util/TestNotary.java b/tests/org/cacert/gigi/util/TestNotary.java
new file mode 100644 (file)
index 0000000..22f75b2
--- /dev/null
@@ -0,0 +1,131 @@
+package org.cacert.gigi.util;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Date;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.dbObjects.Assurance.AssuranceType;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.output.DateSelector;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.junit.Test;
+
+public class TestNotary extends ManagedTest {
+
+    // These tests create a lot of users and therefore require resetting of the
+    // registering-rate-limit.
+    @Test
+    public void testNormalAssurance() throws SQLException, GigiApiException {
+        try {
+            clearCaches();
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+        User[] users = new User[30];
+        for (int i = 0; i < users.length; i++) {
+            int id = createVerifiedUser("fn" + i, "ln" + i, createUniqueName() + "@email.org", TEST_PASSWORD);
+            users[i] = User.getById(id);
+        }
+        User assurer = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@email.org", TEST_PASSWORD));
+        int[] result = new int[] {
+                10, 10, 10, 10, 15, 15, 15, 15, 15, 20, 20, 20, 20, 20, 25, 25, 25, 25, 25, 30, 30, 30, 30, 30, 35, 35, 35, 35, 35, 35
+        };
+
+        try {
+            Notary.assure(assurer, users[0], users[0].getName(), users[0].getDoB(), -1, "test-notary", "2014-01-01", AssuranceType.FACE_TO_FACE);
+            fail("This shouldn't have passed");
+        } catch (GigiApiException e) {
+            // expected
+        }
+        for (int i = 0; i < result.length; i++) {
+            assertEquals(result[i], assurer.getMaxAssurePoints());
+
+            assuranceFail(assurer, users[i], result[i] + 1, "test-notary", "2014-01-01");
+            Notary.assure(assurer, users[i], users[i].getName(), users[i].getDoB(), result[i], "test-notary", "2014-01-01", AssuranceType.FACE_TO_FACE);
+            assuranceFail(assurer, users[i], result[i], "test-notary", "2014-01-01");
+        }
+
+        assertEquals(35, assurer.getMaxAssurePoints());
+
+        assertEquals(2 + 60, assurer.getExperiencePoints());
+
+    }
+
+    private void assuranceFail(User assurer, User user, int i, String location, String date) throws SQLException {
+        try {
+            Notary.assure(assurer, user, user.getName(), user.getDoB(), i, location, date, AssuranceType.FACE_TO_FACE);
+            fail("This shouldn't have passed");
+        } catch (GigiApiException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testPoJam() throws SQLException, GigiApiException {
+        try {
+            clearCaches();
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+        User[] users = new User[30];
+        for (int i = 0; i < users.length; i++) {
+            int id = createVerifiedUser("fn" + i, "ln" + i, createUniqueName() + "@email.org", TEST_PASSWORD);
+            users[i] = User.getById(id);
+        }
+        int id = createAssuranceUser("fn", "ln", createUniqueName() + "@email.org", TEST_PASSWORD);
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `users` SET dob=NOW() - interval '15 years' WHERE id=?")) {
+            ps.setInt(1, id);
+            ps.execute();
+        }
+        User assurer = User.getById(id);
+        for (int i = 0; i < users.length; i++) {
+            assuranceFail(assurer, users[i], -1, "test-notary", "2014-01-01");
+            assuranceFail(assurer, users[i], 11, "test-notary", "2014-01-01");
+            if (User.POJAM_ENABLED) {
+                Notary.assure(assurer, users[i], users[i].getName(), users[i].getDoB(), 10, "test-notary", "2014-01-01", AssuranceType.FACE_TO_FACE);
+            }
+            assuranceFail(assurer, users[i], 10, "test-notary", "2014-01-01");
+        }
+    }
+
+    @Test
+    public void testFail() throws SQLException, GigiApiException {
+        User assuranceUser = User.getById(createAssuranceUser("fn", "ln", createUniqueName() + "@example.org", TEST_PASSWORD));
+        User assuree = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "@example.org", TEST_PASSWORD));
+
+        // invalid date format
+        assuranceFail(assuranceUser, assuree, 10, "notary-junit-test", "2014-01-blah");
+        // empty date
+        assuranceFail(assuranceUser, assuree, 10, "notary-junit-test", "");
+        // null date
+        assuranceFail(assuranceUser, assuree, 10, "notary-junit-test", null);
+        // null location
+        assuranceFail(assuranceUser, assuree, 10, null, "2014-01-01");
+        // empty location
+        assuranceFail(assuranceUser, assuree, 10, "", "2014-01-01");
+        // date in the future
+        assuranceFail(assuranceUser, assuree, 10, "notary-junit-test", DateSelector.getDateFormat().format(new Date(System.currentTimeMillis() + 2 * 24 * 60 * 60 * 1000)));
+        // location too short
+        assuranceFail(assuranceUser, assuree, 10, "n", "2014-01-01");
+        // points too low
+        assuranceFail(assuranceUser, assuree, -1, "notary-junit-test", "2014-01-01");
+        // points too high
+        assuranceFail(assuranceUser, assuree, 11, "notary-junit-test", "2014-01-01");
+
+        // assure oneself
+        assuranceFail(assuranceUser, assuranceUser, 10, "notary-junit-test", "2014-01-01");
+        // not an assurer
+        assuranceFail(assuree, assuranceUser, 10, "notary-junit-test", "2014-01-01");
+
+        // valid
+        Notary.assure(assuranceUser, assuree, assuree.getName(), assuree.getDoB(), 10, "notary-junit-test", "2014-01-01", AssuranceType.FACE_TO_FACE);
+
+        // assure double
+        assuranceFail(assuranceUser, assuree, 10, "notary-junit-test", "2014-01-01");
+
+    }
+}
index 86205ff5209ad65da38276bf365f2673fa6d57da..6e38a2bd82e78ed870235b7d9a9dd7b3b8990058 100644 (file)
@@ -1,15 +1,21 @@
 package org.cacert.gigi.util;
 
-import org.junit.Test;
 import static org.junit.Assert.*;
 
+import org.junit.Test;
+
 public class TestPasswordHash {
-       @Test
-       public void testVerify() {
-               assertTrue(PasswordHash.verifyHash("a", PasswordHash.hash("a")));
-               assertTrue(PasswordHash.verifyHash("", PasswordHash.hash("")));
-               assertTrue(PasswordHash.verifyHash("a1234", PasswordHash.hash("a1234")));
-               assertTrue(PasswordHash.verifyHash("auhlcb4 9x,IUQẞ&lvrvä",
-                               PasswordHash.hash("auhlcb4 9x,IUQẞ&lvrvä")));
-       }
+
+    @Test
+    public void testVerify() {
+        assertTrue(PasswordHash.verifyHash("a", PasswordHash.hash("a")) != null);
+        assertTrue(PasswordHash.verifyHash("a1234", PasswordHash.hash("a1234")) != null);
+        assertTrue(PasswordHash.verifyHash("auhlcb4 9x,IUQẞ&lvrvä", PasswordHash.hash("auhlcb4 9x,IUQẞ&lvrvä")) != null);
+    }
+
+    @Test
+    public void testVerifyNegative() {
+        assertFalse(PasswordHash.verifyHash("b", PasswordHash.hash("a")) != null);
+        assertFalse(PasswordHash.verifyHash("ae", PasswordHash.hash("auhlcb4 9x,IUQẞ&lvrvä")) != null);
+    }
 }
diff --git a/tests/org/cacert/gigi/util/TestPasswordMigration.java b/tests/org/cacert/gigi/util/TestPasswordMigration.java
new file mode 100644 (file)
index 0000000..7ff4ea6
--- /dev/null
@@ -0,0 +1,38 @@
+package org.cacert.gigi.util;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.testUtils.ManagedTest;
+import org.cacert.gigi.testUtils.RegisteredUser;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class TestPasswordMigration extends ManagedTest {
+
+    @Rule
+    public RegisteredUser ru = new RegisteredUser();
+
+    @Test
+    public void testPasswordMigration() throws IOException {
+        try (GigiPreparedStatement stmt = new GigiPreparedStatement("UPDATE users SET `password`=? WHERE id=?")) {
+            stmt.setString(1, PasswordHash.sha1("a"));
+            stmt.setInt(2, ru.getUser().getId());
+            stmt.execute();
+        }
+        String cookie = login(ru.getUser().getEmail(), "a");
+        assertTrue(isLoggedin(cookie));
+
+        try (GigiPreparedStatement stmt = new GigiPreparedStatement("SELECT `password` FROM users WHERE id=?")) {
+            stmt.setInt(1, ru.getUser().getId());
+            GigiResultSet res = stmt.executeQuery();
+            assertTrue(res.next());
+            String newHash = res.getString(1);
+            assertThat(newHash, containsString("$"));
+        }
+    }
+}
index 91a918c465c19d5f25734562f098430f579803ca..254df05a70d5d612e6b755faf15aa26c2e7dea61 100644 (file)
@@ -1,64 +1,74 @@
 package org.cacert.gigi.util;
 
-import org.cacert.gigi.User;
-import org.junit.Test;
 import static org.junit.Assert.*;
 
+import org.cacert.gigi.dbObjects.Name;
+import org.junit.Test;
+
 public class TestPasswordStrengthChecker {
-       User u;
-       public TestPasswordStrengthChecker() {
-               u = new User();
-               u.setFname("fname");
-               u.setLname("lname");
-               u.setMname("mname");
-               u.setEmail("email");
-               u.setSuffix("suffix");
-       }
-       @Test
-       public void testPasswordLength() {
-               assertEquals(1, PasswordStrengthChecker.checkpw("01234", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw("0123456789012345", u));
-               assertEquals(3,
-                               PasswordStrengthChecker.checkpw("012345678901234567890", u));
-               assertEquals(4, PasswordStrengthChecker.checkpw(
-                               "01234567890123456789012345", u));
-               assertEquals(5, PasswordStrengthChecker.checkpw(
-                               "0123456789012345678901234567890", u));
-       }
-       @Test
-       public void testPasswordCharTypes() {
-               assertEquals(1, PasswordStrengthChecker.checkpw("0", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw("0a", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw("0az", u));
-               assertEquals(3, PasswordStrengthChecker.checkpw("0azZ", u));
-               assertEquals(4, PasswordStrengthChecker.checkpw("0a zZ", u));
-               assertEquals(5, PasswordStrengthChecker.checkpw("0a. zZ", u));
-
-               assertEquals(1, PasswordStrengthChecker.checkpw(".", u));
-               assertEquals(1, PasswordStrengthChecker.checkpw(" ", u));
-               assertEquals(1, PasswordStrengthChecker.checkpw("b", u));
-               assertEquals(1, PasswordStrengthChecker.checkpw("Z", u));
-
-               assertEquals(2, PasswordStrengthChecker.checkpw("0.", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw("0 ", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw("0a", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw("0Z", u));
-
-               assertEquals(2, PasswordStrengthChecker.checkpw(" .", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw(" a", u));
-               assertEquals(2, PasswordStrengthChecker.checkpw(" Z", u));
-
-       }
-       @Test
-       public void testPasswordContains() {
-               assertEquals(-1, PasswordStrengthChecker.checkpw("fnamea", u));
-               assertEquals(-5, PasswordStrengthChecker.checkpw("na", u));
-               assertEquals(0, PasswordStrengthChecker.checkpw("1lname", u));
-               assertEquals(0, PasswordStrengthChecker.checkpw("1email", u));
-               assertEquals(-1, PasswordStrengthChecker.checkpw("mai", u));
-               assertEquals(-1, PasswordStrengthChecker.checkpw("suff", u));
-               assertEquals(0, PasswordStrengthChecker.checkpw("1suffix", u));
-
-       }
+
+    Name n = new Name("fname", "lname", "mname", "suffix");
+
+    String e = "email";
+
+    public TestPasswordStrengthChecker() {}
+
+    private int check(String pw) {
+        return PasswordStrengthChecker.checkpw(pw, n, e);
+    }
+
+    @Test
+    public void testPasswordLength() {
+        assertEquals(1, check("01234"));
+        assertEquals(2, check("0123456789012345"));
+        assertEquals(3, check("012345678901234567890"));
+        assertEquals(4, check("01234567890123456789012345"));
+        assertEquals(5, check("0123456789012345678901234567890"));
+    }
+
+    @Test
+    public void testPasswordNonASCII() {
+        assertEquals(2, check("0ä"));
+        assertEquals(3, check("0aä"));
+        assertEquals(3, check("0azä"));
+        assertEquals(3, check("0az.ä"));
+    }
+
+    @Test
+    public void testPasswordCharTypes() {
+        assertEquals(1, check("0"));
+        assertEquals(2, check("0a"));
+        assertEquals(2, check("0az"));
+        assertEquals(3, check("0azZ"));
+        assertEquals(4, check("0a zZ"));
+        assertEquals(5, check("0a. zZ"));
+
+        assertEquals(1, check("."));
+        assertEquals(1, check(" "));
+        assertEquals(1, check("b"));
+        assertEquals(1, check("Z"));
+
+        assertEquals(2, check("0."));
+        assertEquals(2, check("0 "));
+        assertEquals(2, check("0a"));
+        assertEquals(2, check("0Z"));
+
+        assertEquals(2, check(" ."));
+        assertEquals(2, check(" a"));
+        assertEquals(2, check(" Z"));
+
+    }
+
+    @Test
+    public void testPasswordContains() {
+        assertEquals( -1, check("fnamea"));
+        assertEquals( -5, check("na"));
+        assertEquals(0, check("1lname"));
+        assertEquals(0, check("1email"));
+        assertEquals( -1, check("mai"));
+        assertEquals( -1, check("suff"));
+        assertEquals(0, check("1suffix"));
+
+    }
 
 }
diff --git a/tests/org/cacert/gigi/util/TestPublicSuffixes.java b/tests/org/cacert/gigi/util/TestPublicSuffixes.java
new file mode 100644 (file)
index 0000000..9502241
--- /dev/null
@@ -0,0 +1,82 @@
+package org.cacert.gigi.util;
+
+import static org.junit.Assert.*;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.IDN;
+import java.util.ArrayList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestPublicSuffixes {
+
+    /**
+     * Taken from
+     * http://mxr.mozilla.org/mozilla-central/source/netwerk/test/unit
+     * /data/test_psl.txt?raw=1
+     */
+    @Parameters(name = "publicSuffix({0}) = {1}")
+    public static Iterable<String[]> genParams() throws IOException {
+        BufferedReader br = null;
+        try {
+            br = new BufferedReader(new InputStreamReader(TestPublicSuffixes.class.getResourceAsStream("TestPublicSuffixes.txt"), "UTF-8"));
+            ArrayList<String[]> result = new ArrayList<>();
+            String line;
+            while ((line = br.readLine()) != null) {
+                if (line.startsWith("//") || line.isEmpty()) {
+                    continue;
+                }
+                String parseSuffix = "checkPublicSuffix(";
+                if (line.startsWith(parseSuffix)) {
+                    String data = line.substring(parseSuffix.length(), line.length() - 2);
+                    String[] parts = data.split(", ");
+                    if (parts.length != 2) {
+                        throw new Error("Syntax error in public suffix test data file: " + line);
+                    }
+                    result.add(new String[] {
+                            parse(parts[0]), parse(parts[1])
+                    });
+                } else {
+                    throw new Error("Unparsable line: " + line);
+                }
+            }
+            return result;
+        } finally {
+            if (br != null) {
+                br.close();
+            }
+        }
+    }
+
+    private static String parse(String data) {
+        if (data.equals("null")) {
+            return null;
+        }
+        if (data.startsWith("'") && data.endsWith("'")) {
+            return data.substring(1, data.length() - 1);
+        }
+        throw new Error("Syntax error with literal: " + data);
+    }
+
+    @Parameter(0)
+    public String domain;
+
+    @Parameter(1)
+    public String suffix;
+
+    @Test
+    public void testPublicSuffix() {
+        if (domain != null) {
+            domain = domain.toLowerCase();
+        }
+        String publicSuffix = PublicSuffixes.getInstance().getRegistrablePart(domain == null ? null : IDN.toASCII(domain));
+        assertEquals(suffix == null ? null : IDN.toASCII(suffix), publicSuffix);
+    }
+}
diff --git a/tests/org/cacert/gigi/util/TestPublicSuffixes.txt b/tests/org/cacert/gigi/util/TestPublicSuffixes.txt
new file mode 100644 (file)
index 0000000..d4624a9
--- /dev/null
@@ -0,0 +1,98 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+// null input.
+checkPublicSuffix(null, null);
+// Mixed case.
+checkPublicSuffix('COM', null);
+checkPublicSuffix('example.COM', 'example.com');
+checkPublicSuffix('WwW.example.COM', 'example.com');
+// Leading dot.
+//checkPublicSuffix('.com', null);
+//checkPublicSuffix('.example', null);
+//checkPublicSuffix('.example.com', null);
+//checkPublicSuffix('.example.example', null);
+// Unlisted TLD.
+//checkPublicSuffix('example', null);
+//checkPublicSuffix('example.example', 'example.example');
+//checkPublicSuffix('b.example.example', 'example.example');
+//checkPublicSuffix('a.b.example.example', 'example.example');
+// Listed, but non-Internet, TLD.
+checkPublicSuffix('local', null);
+checkPublicSuffix('example.local', null);
+checkPublicSuffix('b.example.local', null);
+checkPublicSuffix('a.b.example.local', null);
+// TLD with only 1 rule.
+checkPublicSuffix('biz', null);
+checkPublicSuffix('domain.biz', 'domain.biz');
+checkPublicSuffix('b.domain.biz', 'domain.biz');
+checkPublicSuffix('a.b.domain.biz', 'domain.biz');
+// TLD with some 2-level rules.
+checkPublicSuffix('com', null);
+checkPublicSuffix('example.com', 'example.com');
+checkPublicSuffix('b.example.com', 'example.com');
+checkPublicSuffix('a.b.example.com', 'example.com');
+checkPublicSuffix('uk.com', null);
+checkPublicSuffix('example.uk.com', 'example.uk.com');
+checkPublicSuffix('b.example.uk.com', 'example.uk.com');
+checkPublicSuffix('a.b.example.uk.com', 'example.uk.com');
+checkPublicSuffix('test.ac', 'test.ac');
+// TLD with only 1 (wildcard) rule.
+checkPublicSuffix('mm', null);
+checkPublicSuffix('c.mm', null);
+checkPublicSuffix('b.c.mm', 'b.c.mm');
+checkPublicSuffix('a.b.c.mm', 'b.c.mm');
+// More complex TLD.
+checkPublicSuffix('jp', null);
+checkPublicSuffix('test.jp', 'test.jp');
+checkPublicSuffix('www.test.jp', 'test.jp');
+checkPublicSuffix('ac.jp', null);
+checkPublicSuffix('test.ac.jp', 'test.ac.jp');
+checkPublicSuffix('www.test.ac.jp', 'test.ac.jp');
+checkPublicSuffix('kyoto.jp', null);
+checkPublicSuffix('test.kyoto.jp', 'test.kyoto.jp');
+checkPublicSuffix('ide.kyoto.jp', null);
+checkPublicSuffix('b.ide.kyoto.jp', 'b.ide.kyoto.jp');
+checkPublicSuffix('a.b.ide.kyoto.jp', 'b.ide.kyoto.jp');
+checkPublicSuffix('c.kobe.jp', null);
+checkPublicSuffix('b.c.kobe.jp', 'b.c.kobe.jp');
+checkPublicSuffix('a.b.c.kobe.jp', 'b.c.kobe.jp');
+checkPublicSuffix('city.kobe.jp', 'city.kobe.jp');
+checkPublicSuffix('www.city.kobe.jp', 'city.kobe.jp');
+// TLD with a wildcard rule and exceptions.
+checkPublicSuffix('ck', null);
+checkPublicSuffix('test.ck', null);
+checkPublicSuffix('b.test.ck', 'b.test.ck');
+checkPublicSuffix('a.b.test.ck', 'b.test.ck');
+checkPublicSuffix('www.ck', 'www.ck');
+checkPublicSuffix('www.www.ck', 'www.ck');
+// US K12.
+checkPublicSuffix('us', null);
+checkPublicSuffix('test.us', 'test.us');
+checkPublicSuffix('www.test.us', 'test.us');
+checkPublicSuffix('ak.us', null);
+checkPublicSuffix('test.ak.us', 'test.ak.us');
+checkPublicSuffix('www.test.ak.us', 'test.ak.us');
+checkPublicSuffix('k12.ak.us', null);
+checkPublicSuffix('test.k12.ak.us', 'test.k12.ak.us');
+checkPublicSuffix('www.test.k12.ak.us', 'test.k12.ak.us');
+// IDN labels.
+checkPublicSuffix('食狮.com.cn', '食狮.com.cn');
+checkPublicSuffix('食狮.公司.cn', '食狮.公司.cn');
+checkPublicSuffix('www.食狮.公司.cn', '食狮.公司.cn');
+checkPublicSuffix('shishi.公司.cn', 'shishi.公司.cn');
+checkPublicSuffix('公司.cn', null);
+checkPublicSuffix('食狮.中国', '食狮.中国');
+checkPublicSuffix('www.食狮.中国', '食狮.中国');
+checkPublicSuffix('shishi.中国', 'shishi.中国');
+checkPublicSuffix('中国', null);
+// Same as above, but punycoded.
+checkPublicSuffix('xn--85x722f.com.cn', 'xn--85x722f.com.cn');
+checkPublicSuffix('xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn');
+checkPublicSuffix('www.xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn');
+checkPublicSuffix('shishi.xn--55qx5d.cn', 'shishi.xn--55qx5d.cn');
+checkPublicSuffix('xn--55qx5d.cn', null);
+checkPublicSuffix('xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s');
+checkPublicSuffix('www.xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s');
+checkPublicSuffix('shishi.xn--fiqs8s', 'shishi.xn--fiqs8s');
+checkPublicSuffix('xn--fiqs8s', null);
diff --git a/util-testing/org/cacert/gigi/DevelLauncher.java b/util-testing/org/cacert/gigi/DevelLauncher.java
new file mode 100644 (file)
index 0000000..f84d728
--- /dev/null
@@ -0,0 +1,256 @@
+package org.cacert.gigi;
+
+import static org.cacert.gigi.Gigi.*;
+
+import java.awt.Desktop;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.cacert.gigi.dbObjects.ObjectCache;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.output.template.TranslateCommand;
+import org.cacert.gigi.pages.LoginPage;
+import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.pages.account.certs.CertificateRequest;
+import org.cacert.gigi.pages.main.RegisterPage;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.ServerConstants;
+import org.kamranzafar.jtar.TarEntry;
+import org.kamranzafar.jtar.TarHeader;
+import org.kamranzafar.jtar.TarOutputStream;
+
+public class DevelLauncher {
+
+    public static void main(String[] args) throws Exception {
+        Properties mainProps = new Properties();
+        try (FileInputStream inStream = new FileInputStream("config/gigi.properties")) {
+            mainProps.load(inStream);
+        }
+        for (int i = 0; i < args.length; i++) {
+            if (args[i].equals("--port")) {
+                mainProps.setProperty("port", args[i + 1]);
+            }
+            i++;
+        }
+        killPreviousInstance(mainProps);
+
+        ByteArrayOutputStream chunkConfig = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(chunkConfig);
+        byte[] cacerts = Files.readAllBytes(Paths.get("config/cacerts.jks"));
+        byte[] keystore = null;
+        Path p = Paths.get("config/keystore.pkcs12");
+        if (p.toFile().exists()) {
+            keystore = Files.readAllBytes(p);
+        } else {
+            mainProps.setProperty("proxy", "true");
+        }
+
+        DevelLauncher.writeGigiConfig(dos, "changeit".getBytes("UTF-8"), "changeit".getBytes("UTF-8"), mainProps, cacerts, keystore);
+        dos.flush();
+        InputStream oldin = System.in;
+        System.setIn(new ByteArrayInputStream(chunkConfig.toByteArray()));
+        new Launcher().boot();
+        addDevelPage(true);
+        new Thread("ticket awaiter") {
+
+            @Override
+            public void run() {
+                try {
+                    Thread.sleep(8000);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                try {
+                    if ( !ticketUsed) {
+                        Desktop.getDesktop().browse(new URL("http://" + ServerConstants.getWwwHostNamePort() + "/ticketWait").toURI());
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                } catch (URISyntaxException e) {
+                    e.printStackTrace();
+                }
+            }
+        }.start();
+        System.setIn(oldin);
+        BufferedReader br = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
+        System.out.println("Cacert-gigi system sucessfully started.");
+        System.out.println("Press enter to shutdown.");
+        br.readLine();
+        System.exit(0);
+    }
+
+    private static void killPreviousInstance(Properties mainProps) {
+        try {
+            String targetPort = mainProps.getProperty("http.port");
+            String targetHost = mainProps.getProperty("name.www");
+            URL u = new URL("http://" + targetHost + ":" + targetPort + "/kill");
+            u.openStream();
+        } catch (IOException e) {
+        }
+    }
+
+    public static void addDevelPage(boolean withToken) {
+        try {
+            Field instF = Gigi.class.getDeclaredField("instance");
+            Field pageF = Gigi.class.getDeclaredField("pages");
+            instF.setAccessible(true);
+            pageF.setAccessible(true);
+            Object gigi = instF.get(null);
+
+            // Check if we got a proper map (as much as we can tell)
+            Object pagesObj = pageF.get(gigi);
+            @SuppressWarnings("unchecked")
+            HashMap<String, Page> pages = pagesObj instanceof Map ? new HashMap<>((Map<String, Page>) pagesObj) : null;
+
+            pages.put("/manage", new Page("Page-manager") {
+
+                @Override
+                public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                    ObjectCache.clearAllCaches();
+                    RegisterPage.RATE_LIMIT.bypass();
+                    LoginPage.RATE_LIMIT.bypass();
+                    CertificateRequest.RATE_LIMIT.bypass();
+                    resp.getWriter().println("All caches cleared.");
+                    System.out.println("Caches cleared.");
+
+                }
+
+                @Override
+                public boolean needsLogin() {
+                    return false;
+                }
+
+            });
+
+            pages.put("/kill", new Page("Kill") {
+
+                /**
+                 * The contained call to {@link System#exit(int)} is mainly
+                 * needed to kill this instance immediately if another
+                 * {@link DevelLauncher} is booting up to free all ports This is
+                 * required for fast development cycles.
+                 * 
+                 * @see #killPreviousInstance(Properties)
+                 */
+                @Override
+                public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                    System.exit(0);
+                }
+
+                @Override
+                public boolean needsLogin() {
+                    return false;
+                }
+            });
+
+            if (withToken) {
+                addTicketPage(pages);
+            }
+
+            pageF.set(gigi, Collections.unmodifiableMap(pages));
+        } catch (ReflectiveOperationException e) {
+            e.printStackTrace();
+        }
+    }
+
+    static boolean ticketUsed = false;
+
+    private static void addTicketPage(HashMap<String, Page> pages) {
+        pages.put("/ticketWait", new Page("ticket") {
+
+            Template t = new Template(DevelLauncher.class.getResource("DevelTicketWait.templ"));
+
+            @Override
+            public boolean needsLogin() {
+                return false;
+            }
+
+            @Override
+            public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                resp.setHeader("content-security-policy", "");
+                t.output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+            }
+
+        });
+        pages.put("/ticket", new Page("ticket") {
+
+            @Override
+            public boolean beforeTemplate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                // TODO Auto-generated method stub
+                if ( !ticketUsed) {
+                    HttpSession sess = req.getSession();
+                    User user = User.getById(1);
+                    if (user == null) {
+                        resp.getWriter().println("ticket consumed but no user available for that action");
+                        ticketUsed = true;
+                        return true;
+                    }
+                    sess.setAttribute(LOGGEDIN, true);
+                    sess.setAttribute(Language.SESSION_ATTRIB_NAME, user.getPreferredLocale());
+                    sess.setAttribute(AUTH_CONTEXT, new AuthorizationContext(user, user));
+                    req.getSession().setAttribute(LOGIN_METHOD, new TranslateCommand("Ticket"));
+                    resp.getWriter().println("ticket consumed");
+                    ticketUsed = true;
+                }
+                return true;
+            }
+
+            @Override
+            public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {}
+
+            @Override
+            public boolean needsLogin() {
+                return false;
+            }
+        });
+    }
+
+    public static void writeGigiConfig(OutputStream target, byte[] keystorepw, byte[] truststorepw, Properties mainprop, byte[] cacerts, byte[] keystore) throws IOException {
+        TarOutputStream tos = new TarOutputStream(target);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mainprop.store(baos, "");
+
+        putTarEntry(baos.toByteArray(), tos, "gigi.properties");
+        putTarEntry(keystorepw, tos, "keystorepw");
+        putTarEntry(truststorepw, tos, "truststorepw");
+        putTarEntry(keystore, tos, "keystore.pkcs12");
+        putTarEntry(cacerts, tos, "cacerts.jks");
+        tos.close();
+
+    }
+
+    private static void putTarEntry(byte[] data, TarOutputStream tos, String name) throws IOException {
+        if (data == null) {
+            return;
+        }
+        TarHeader th = new TarHeader();
+        th.name = new StringBuffer(name);
+        th.size = data.length;
+        tos.putNextEntry(new TarEntry(th));
+        tos.write(data);
+    }
+
+}
diff --git a/util-testing/org/cacert/gigi/DevelTicketWait.templ b/util-testing/org/cacert/gigi/DevelTicketWait.templ
new file mode 100644 (file)
index 0000000..4b91f35
--- /dev/null
@@ -0,0 +1,8 @@
+Your ticket:
+<iframe width="100" height="100" id='devel-reload-ifr'></iframe>
+
+<script type='text/javascript'>
+setInterval(function(){
+       document.getElementById("devel-reload-ifr").src="/ticket?"+new Date().getTime();
+}, 5000);
+</script>
diff --git a/util-testing/org/cacert/gigi/GenerateProfileOverview.java b/util-testing/org/cacert/gigi/GenerateProfileOverview.java
new file mode 100644 (file)
index 0000000..99d45e2
--- /dev/null
@@ -0,0 +1,83 @@
+package org.cacert.gigi;
+
+import java.awt.Desktop;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.util.LinkedList;
+import java.util.Properties;
+import java.util.TreeSet;
+
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.CertificateProfile.PropertyTemplate;
+
+public class GenerateProfileOverview {
+
+    public static void main(String[] args) throws IOException {
+        Properties pr = new Properties();
+        try (Reader reader = new InputStreamReader(new FileInputStream("config/gigi.properties"), "UTF-8")) {
+            pr.load(reader);
+        }
+        DatabaseConnection.init(pr);
+
+        TreeSet<String> pt = new TreeSet<>();
+        TreeSet<String> req = new TreeSet<>();
+        LinkedList<CertificateProfile> cps = new LinkedList<>();
+        for (CertificateProfile cp : CertificateProfile.getAll()) {
+            cps.add(cp);
+            for (PropertyTemplate p : cp.getTemplates().values()) {
+                pt.add(p.getBase());
+            }
+            req.addAll(cp.getReqireds());
+        }
+        try (PrintWriter pw = new PrintWriter("profiles.html", "UTF-8")) {
+            pw.println("<!DOCTYPE html><html><head><title>Profiles</title>");
+            pw.println("<style>.split{background-color:#000;margin:0;cell-spacing:0}td{text-align:center}</style>");
+            pw.println("</head>");
+            pw.println("<body><table border='1'>");
+            pw.println("<tr><td>id</td><td> </td>");
+            for (String p : pt) {
+                pw.println("<th>" + p + "</th>");
+            }
+            pw.println("<th class='split'></th>");
+            for (String p : req) {
+                pw.println("<th class='req'>" + p + "</th>");
+            }
+            pw.println("</tr>");
+            for (CertificateProfile certificateProfile : cps) {
+                pw.println("<tr>");
+                pw.println("<td>" + certificateProfile.getId() + "</td>");
+                pw.println("<td>" + certificateProfile.getKeyName() + "</td>");
+                outer:
+                for (String p : pt) {
+                    for (PropertyTemplate t : certificateProfile.getTemplates().values()) {
+                        if (t.getBase().equals(p)) {
+                            pw.println("<td>" + (t.isRequired() ? (t.isMultiple() ? "+" : "y") : (t.isMultiple() ? "*" : "?")) + "</td>");
+                            continue outer;
+                        }
+                    }
+                    pw.println("<td></td>");
+                }
+                pw.println("<td class='split'></td>");
+                outer:
+                for (String p : req) {
+                    for (String t : certificateProfile.getReqireds()) {
+                        if (t.equals(p)) {
+                            pw.println("<td class='req'>y</td>");
+                            continue outer;
+                        }
+                    }
+                    pw.println("<td></td>");
+                }
+                pw.println("</tr>");
+            }
+            pw.println("</table></body></html>");
+            Desktop.getDesktop().browse(new File("profiles.html").toURI());
+        }
+    }
+
+}
diff --git a/util-testing/org/cacert/gigi/TestLauncher.java b/util-testing/org/cacert/gigi/TestLauncher.java
new file mode 100644 (file)
index 0000000..89fc9e2
--- /dev/null
@@ -0,0 +1,12 @@
+package org.cacert.gigi;
+
+public class TestLauncher {
+
+    public static void main(String[] args) throws Exception {
+        // As clean as possible
+        Launcher.main(args);
+        DevelLauncher.addDevelPage(false);
+        System.err.println("System successfully started.");
+
+    }
+}
diff --git a/util-testing/org/cacert/gigi/email/CommandlineEmailProvider.java b/util-testing/org/cacert/gigi/email/CommandlineEmailProvider.java
new file mode 100644 (file)
index 0000000..108dd64
--- /dev/null
@@ -0,0 +1,30 @@
+package org.cacert.gigi.email;
+
+import java.io.IOException;
+import java.util.Properties;
+
+public class CommandlineEmailProvider extends EmailProvider {
+
+    public CommandlineEmailProvider(Properties p) {}
+
+    @Override
+    public void sendmail(String to, String subject, String message, String from, String replyto, String toname, String fromname, String errorsto, boolean extra) throws IOException {
+        synchronized (System.out) {
+            System.out.println("== MAIL ==");
+            System.out.println("To: " + to);
+            System.out.println("Subject: " + subject);
+            System.out.println("From: " + from);
+            System.out.println("Errors-To: " + errorsto);
+            System.out.println("Extra: " + extra);
+            System.out.println(message);
+        }
+
+    }
+
+    @Override
+    public String checkEmailServer(int forUid, String address) throws IOException {
+        System.out.println("checkMailBox: " + address);
+        return OK;
+    }
+
+}
diff --git a/util-testing/org/cacert/gigi/email/TestEmailProvider.java b/util-testing/org/cacert/gigi/email/TestEmailProvider.java
new file mode 100644 (file)
index 0000000..bee64f4
--- /dev/null
@@ -0,0 +1,124 @@
+package org.cacert.gigi.email;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.security.Key;
+import java.security.cert.Certificate;
+import java.util.Properties;
+
+/**
+ * This class intercepts emails so that the test cases can evaluate them
+ * automatically.
+ */
+public class TestEmailProvider extends EmailProvider {
+
+    private ServerSocket servs;
+
+    private Socket client;
+
+    private DataOutputStream out;
+
+    private DataInputStream in;
+
+    private EmailProvider target;
+
+    protected TestEmailProvider(Properties props) {
+        try {
+            String name = props.getProperty("emailProvider.test.target");
+            if (name != null) {
+                Class<?> c = Class.forName(name);
+                target = (EmailProvider) c.getDeclaredConstructor(Properties.class).newInstance(props);
+            }
+        } catch (ReflectiveOperationException e) {
+            e.printStackTrace();
+        }
+        try {
+            servs = new ServerSocket(Integer.parseInt(props.getProperty("emailProvider.port")), 10, InetAddress.getByName("127.0.0.1"));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public synchronized void sendmail(String to, String subject, String message, String from, String replyto, String toname, String fromname, String errorsto, boolean extra) throws IOException {
+        while (true) {
+            if ( !assureLocalConnection() && target != null) {
+                target.sendmail(to, subject, message, from, replyto, toname, fromname, errorsto, extra);
+                return;
+            }
+            try {
+                if (out == null) {
+                    continue;
+                }
+                out.writeUTF("mail");
+                write(to);
+                write(subject);
+                write(message);
+                write(from);
+                write(replyto);
+                out.flush();
+                return;
+            } catch (IOException e) {
+                client = null;
+            }
+        }
+    }
+
+    private boolean assureLocalConnection() throws IOException {
+        if (out != null) {
+            try {
+                out.writeUTF("ping");
+            } catch (IOException e) {
+                client = null;
+            }
+        }
+        if (client == null || client.isClosed()) {
+            servs.setSoTimeout(2000);
+            try {
+                client = servs.accept();
+            } catch (SocketTimeoutException e) {
+                return false;
+            }
+            out = new DataOutputStream(client.getOutputStream());
+            in = new DataInputStream(client.getInputStream());
+        }
+        return true;
+    }
+
+    @Override
+    public synchronized String checkEmailServer(int forUid, String address) throws IOException {
+        while (true) {
+            if ( !assureLocalConnection() && target != null) {
+                return checkEmailServer(forUid, address);
+            }
+            try {
+                out.writeUTF("challengeAddrBox");
+                out.writeUTF(address);
+                return in.readUTF();
+            } catch (IOException e) {
+                client = null;
+            }
+        }
+    }
+
+    private void write(String to) throws IOException {
+        if (to == null) {
+            out.writeUTF("<null>");
+        } else {
+            out.writeUTF(to);
+        }
+    }
+
+    @Override
+    protected void init(Certificate c, Key k) {
+        super.init(c, k);
+        if (target != null) {
+            target.init(c, k);
+        }
+    }
+}
diff --git a/util-testing/org/cacert/gigi/localisation/FileIterable.java b/util-testing/org/cacert/gigi/localisation/FileIterable.java
new file mode 100644 (file)
index 0000000..3c23c7f
--- /dev/null
@@ -0,0 +1,50 @@
+package org.cacert.gigi.localisation;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+
+public class FileIterable implements Iterable<String> {
+       File f;
+
+       public FileIterable(File f) {
+               this.f = f;
+       }
+
+       @Override
+       public Iterator<String> iterator() {
+               try {
+                       return new Iterator<String>() {
+                               BufferedReader br = new BufferedReader(new InputStreamReader(
+                                               new FileInputStream(f), "UTF-8"));
+                               String s = null;
+                               private void getNext() {
+                                       if (s == null) {
+                                               try {
+                                                       s = br.readLine();
+                                               } catch (IOException e) {
+                                                       throw new IOError(e);
+                                               }
+                                       }
+                               }
+                               @Override
+                               public boolean hasNext() {
+                                       getNext();
+                                       return s != null;
+                               }
+
+                               @Override
+                               public String next() {
+                                       String out = s;
+                                       s = null;
+                                       return out;
+                               }
+                       };
+               } catch (IOException e) {
+                       throw new IOError(e);
+               }
+       }
+}
diff --git a/util-testing/org/cacert/gigi/localisation/TaintSource.java b/util-testing/org/cacert/gigi/localisation/TaintSource.java
new file mode 100644 (file)
index 0000000..84380e9
--- /dev/null
@@ -0,0 +1,119 @@
+package org.cacert.gigi.localisation;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
+
+public class TaintSource {
+       private String pack, cls, meth;
+       private int tgt;
+       private TaintSource maskOnly;
+
+       public TaintSource(String pack, String cls, String meth, int tgt) {
+               this(pack, cls, meth, tgt, null);
+       }
+       public TaintSource(String pack, String cls, String meth, int tgt,
+                       TaintSource maskOnly) {
+               this.pack = pack;
+               this.cls = cls;
+               this.meth = meth;
+               this.tgt = tgt;
+               this.maskOnly = maskOnly;
+
+       }
+       public TaintSource(MethodBinding mb) {
+               pack = new String(mb.declaringClass.qualifiedPackageName());
+               cls = new String(mb.declaringClass.qualifiedSourceName());
+               meth = new String(mb.readableName());
+       }
+       @Override
+       public int hashCode() {
+               final int prime = 31;
+               int result = 1;
+               result = prime * result + ((cls == null) ? 0 : cls.hashCode());
+               result = prime * result + ((meth == null) ? 0 : meth.hashCode());
+               result = prime * result + ((pack == null) ? 0 : pack.hashCode());
+               return result;
+       }
+       @Override
+       public String toString() {
+               StringBuffer res = new StringBuffer();
+               res.append("new TaintSource(");
+               res.append("\"" + pack + "\",");
+               res.append("\"" + cls + "\",");
+               res.append("\"" + meth + "\",0);");
+               return res.toString();
+       }
+       public String toConfLine() {
+               return pack + " " + cls + "." + meth + "," + tgt
+                               + (maskOnly == null ? "" : "=>" + maskOnly.toConfLine());
+       }
+       @Override
+       public boolean equals(Object obj) {
+               if (this == obj) {
+                       return true;
+               }
+               if (obj == null) {
+                       return false;
+               }
+               if (getClass() != obj.getClass()) {
+                       return false;
+               }
+               TaintSource other = (TaintSource) obj;
+               if (cls == null) {
+                       if (other.cls != null) {
+                               return false;
+                       }
+               } else if (!cls.equals(other.cls)) {
+                       return false;
+               }
+               if (pack == null) {
+                       if (other.pack != null) {
+                               return false;
+                       }
+               } else if (!pack.equals(other.pack)) {
+                       return false;
+               }
+               if (meth == null) {
+                       if (other.meth != null) {
+                               return false;
+                       }
+               } else if (!meth.equals(other.meth)) {
+                       return false;
+               }
+               return true;
+       }
+       public static TaintSource parseTaint(String confline) {
+               // Pattern matches "Taint-lines"
+               // first part is package name up to space (may not include space or equals sign)
+               // second part is Class name [with inner class name] (may not include "=" but may include ".")
+               // third part is method name including params (may not include "=" or ".")
+               // fourth is index of tainted argument (seperated by "," from the rest)
+               Pattern p = Pattern
+                               .compile("^([^= ]*) ([^=]*)\\.([^=.]*\\([^)]*\\)),([0-9]+)");
+               Matcher m = p.matcher(confline);
+               if (!m.find()) {
+                       throw new Error(confline);
+               }
+               String pack = m.group(1);
+               String cls = m.group(2);
+               String meth = m.group(3);
+               int tgt = Integer.parseInt(m.group(4));
+               TaintSource mask = null;
+               if (m.end() != confline.length()) {
+                       String s = confline.substring(m.end(), m.end() + 2);
+                       if (!s.equals("=>")) {
+                               throw new Error("malformed");
+                       }
+                       mask = parseTaint(confline.substring(m.end() + 2));
+               }
+               return new TaintSource(pack, cls, meth, tgt, mask);
+       }
+       public TaintSource getMaskOnly() {
+               return maskOnly;
+       }
+       public int getTgt() {
+               return tgt;
+       }
+
+}
diff --git a/util-testing/org/cacert/gigi/localisation/TranslationCollectingVisitor.java b/util-testing/org/cacert/gigi/localisation/TranslationCollectingVisitor.java
new file mode 100644 (file)
index 0000000..16f7c41
--- /dev/null
@@ -0,0 +1,213 @@
+package org.cacert.gigi.localisation;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+
+import org.eclipse.jdt.internal.compiler.ASTVisitor;
+import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
+import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
+import org.eclipse.jdt.internal.compiler.ast.ConditionalExpression;
+import org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration;
+import org.eclipse.jdt.internal.compiler.ast.ExplicitConstructorCall;
+import org.eclipse.jdt.internal.compiler.ast.Expression;
+import org.eclipse.jdt.internal.compiler.ast.MessageSend;
+import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
+import org.eclipse.jdt.internal.compiler.ast.NullLiteral;
+import org.eclipse.jdt.internal.compiler.ast.StringLiteral;
+import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
+import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
+import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
+import org.eclipse.jdt.internal.compiler.util.Util;
+
+public final class TranslationCollectingVisitor extends ASTVisitor {
+       MethodBinding cm;
+       private CompilationUnitDeclaration unit;
+       TaintSource[] ts;
+       private TranslationCollector translationCollector;
+
+       public TranslationCollectingVisitor(CompilationUnitDeclaration unit,
+                       TaintSource[] target, TranslationCollector c) {
+               this.unit = unit;
+               ts = target;
+               this.translationCollector = c;
+       }
+       @Override
+       public boolean visit(MethodDeclaration methodDeclaration,
+                       org.eclipse.jdt.internal.compiler.lookup.ClassScope scope) {
+               cm = methodDeclaration.binding;
+               return true;
+       }
+       @Override
+       public void endVisit(MethodDeclaration methodDeclaration,
+                       org.eclipse.jdt.internal.compiler.lookup.ClassScope scope) {
+               cm = null;
+       }
+       @Override
+       public boolean visit(ConstructorDeclaration constructorDeclaration,
+                       ClassScope scope) {
+               cm = constructorDeclaration.binding;
+               return super.visit(constructorDeclaration, scope);
+       }
+       @Override
+       public void endVisit(ConstructorDeclaration constructorDeclaration,
+                       ClassScope scope) {
+               cm = null;
+       }
+       @Override
+       public boolean visit(AllocationExpression allocationExpression,
+                       BlockScope scope) {
+               TaintSource test = new TaintSource(allocationExpression.binding);
+               for (TaintSource taintSource : ts) {
+                       if (taintSource.equals(test)) {
+                               check(null, scope,
+                                               allocationExpression.arguments[taintSource.getTgt()],
+                                               allocationExpression.toString());
+                               return true;
+                       }
+               }
+               return super.visit(allocationExpression, scope);
+       }
+       @Override
+       public boolean visit(ExplicitConstructorCall explicitConstructor,
+                       BlockScope scope) {
+
+               TaintSource t = new TaintSource(explicitConstructor.binding);
+
+               for (TaintSource t0 : ts) {
+                       if (t0.equals(t)) {
+                               Expression[] ags = explicitConstructor.arguments;
+                               if (ags == null) {
+                                       System.out.println(explicitConstructor);
+                                       return true;
+                               }
+                               Expression e = ags[t0.getTgt()];
+                               check(null, scope, e, explicitConstructor.toString());
+                               break;
+                       }
+               }
+               return super.visit(explicitConstructor, scope);
+       }
+
+       @Override
+       public boolean visit(
+                       org.eclipse.jdt.internal.compiler.ast.MessageSend call,
+                       org.eclipse.jdt.internal.compiler.lookup.BlockScope scope) {
+               if (call.binding == null) {
+                       System.out.println("Unbound:" + call + " in " + call.sourceStart());
+                       return true;
+               }
+               //System.out.println("Message");
+               TaintSource t = new TaintSource(call.binding);
+
+               for (TaintSource t0 : ts) {
+                       if (t0.equals(t)) {
+                               Expression[] ags = call.arguments;
+                               if (ags == null) {
+                                       System.out.println(call);
+                                       return true;
+                               }
+                               Expression e = ags[t0.getTgt()];
+                               check(call, scope, e, call.toString());
+                               break;
+                       }
+               }
+               return true;
+       }
+       private void check(org.eclipse.jdt.internal.compiler.ast.MessageSend call,
+                       org.eclipse.jdt.internal.compiler.lookup.BlockScope scope,
+                       Expression e, String caller) {
+               if (e instanceof StringLiteral) {
+                       int[] lineEnds = null;
+                       int lineNumber = Util.getLineNumber(
+                                       e.sourceStart,
+                                       lineEnds = unit.compilationResult
+                                                       .getLineSeparatorPositions(), 0,
+                                       lineEnds.length - 1);
+
+                       String content = new String(((StringLiteral) e).source());
+                       File f0 = new File(new String(unit.compilationResult.fileName))
+                                       .getAbsoluteFile();
+                       File f2 = translationCollector.base.getAbsoluteFile();
+                       try {
+                               translationCollector.add(content, f0.getCanonicalPath()
+                                               .substring(f2.getCanonicalPath().length() + 1)
+                                               + ":"
+                                               + lineNumber);
+                       } catch (IOException e1) {
+                               e1.printStackTrace();
+                       }
+                       return;
+               }
+
+               if (e instanceof NullLiteral) {
+                       return;
+               }
+
+               if (e instanceof MessageSend) {
+                       MessageSend m2 = (MessageSend) e;
+                       TaintSource ts = new TaintSource(m2.binding);
+                       if (ts.equals(new TaintSource("org.cacert.gigi.pages", "Page",
+                                       "getTitle()", 0))) {
+                               return;
+                       }
+                       if (m2.receiver.resolvedType.isCompatibleWith(scope
+                                       .getJavaLangEnum())) {
+                               testEnum(m2.receiver, m2.binding);
+                               System.out.println("ENUM-SRC: !" + m2.receiver);
+                       }
+               }
+               if (e.resolvedType.isCompatibleWith(scope.getJavaLangEnum())) {
+                       // TODO ?
+                       System.out.println("ENUM-Not-Hanled");
+               }
+
+               TaintSource b = cm == null ? null : new TaintSource(cm);
+               for (TaintSource taintSource : ts) {
+                       if (taintSource.equals(b)
+                                       || (taintSource.getMaskOnly() != null && taintSource
+                                                       .getMaskOnly().equals(b))) {
+                               return;
+                       }
+               }
+               if (e instanceof ConditionalExpression) {
+                       check(call, scope, ((ConditionalExpression) e).valueIfFalse, caller);
+                       check(call, scope, ((ConditionalExpression) e).valueIfTrue, caller);
+                       return;
+               }
+
+               System.out.println();
+
+               System.out.println(new String(scope.enclosingClassScope()
+                               .referenceType().compilationResult.fileName));
+               System.out.println("Cannot Handle: " + e + " in "
+                               + (call == null ? "constructor" : call.sourceStart) + " => "
+                               + caller);
+               System.out.println(e.getClass());
+               System.out.println("To ignore: " + b.toConfLine());
+       }
+       private void testEnum(Expression e, MethodBinding binding) {
+               if (binding.parameters.length != 0) {
+                       System.out.println("ERROR: meth");
+                       return;
+               }
+               System.out.println(e.resolvedType.getClass());
+               String s2 = new String(e.resolvedType.qualifiedPackageName())
+                               + "."
+                               + (new String(e.resolvedType.qualifiedSourceName()).replace(
+                                               '.', '$'));
+               try {
+                       Class<?> c = Class.forName(s2);
+                       Enum<?>[] e1 = (Enum[]) c.getMethod("values").invoke(null);
+                       Method m = c.getMethod(new String(binding.selector));
+                       for (int j = 0; j < e1.length; j++) {
+                               System.out.println(m.invoke(e1[j]));
+                       }
+               } catch (ClassNotFoundException e1) {
+                       e1.printStackTrace();
+               } catch (ReflectiveOperationException e1) {
+                       e1.printStackTrace();
+               }
+               System.out.println("ENUM-done: " + e + "!");
+               return;
+       }
+}
diff --git a/util-testing/org/cacert/gigi/localisation/TranslationCollector.java b/util-testing/org/cacert/gigi/localisation/TranslationCollector.java
new file mode 100644 (file)
index 0000000..25a0e8d
--- /dev/null
@@ -0,0 +1,270 @@
+package org.cacert.gigi.localisation;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TreeSet;
+
+import org.cacert.gigi.output.template.Template;
+import org.eclipse.jdt.core.compiler.CategorizedProblem;
+import org.eclipse.jdt.internal.compiler.CompilationResult;
+import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
+import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
+import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
+import org.eclipse.jdt.internal.compiler.batch.FileSystem;
+import org.eclipse.jdt.internal.compiler.batch.Main;
+import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
+import org.eclipse.jdt.internal.compiler.env.AccessRestriction;
+import org.eclipse.jdt.internal.compiler.env.IBinaryType;
+import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
+import org.eclipse.jdt.internal.compiler.env.ISourceType;
+import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
+import org.eclipse.jdt.internal.compiler.impl.ITypeRequestor;
+import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
+import org.eclipse.jdt.internal.compiler.lookup.PackageBinding;
+import org.eclipse.jdt.internal.compiler.parser.Parser;
+import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
+
+public class TranslationCollector {
+       static class TranslationEntry implements Comparable<TranslationEntry> {
+               String text;
+               String occur1;
+               List<String> occur;
+               public TranslationEntry(String text, String occur) {
+                       this.text = text;
+                       occur1 = occur;
+               }
+               public List<String> getOccur() {
+                       if (occur == null) {
+                               return Arrays.asList(occur1);
+                       }
+                       return occur;
+               }
+               public void add(String t) {
+                       if (occur == null) {
+                               occur = new ArrayList<>(Arrays.asList(occur1));
+                       }
+                       occur.add(t);
+               }
+               @Override
+               public int compareTo(TranslationEntry o) {
+                       int i = occur1.compareTo(o.occur1);
+                       if (i != 0) {
+                               return i;
+                       }
+
+                       return text.compareTo(o.text);
+               }
+       }
+
+       private HashMap<String, TranslationEntry> translations = new HashMap<>();
+
+       public final File base;
+
+       public TranslationCollector(File base, File conf) {
+               this.base = base;
+               taint = new LinkedList<>();
+               for (String s : new FileIterable(conf)) {
+                       taint.add(TaintSource.parseTaint(s));
+               }
+       }
+       public void run(File out) throws IOException {
+               scanTemplates();
+               scanCode(taint);
+
+               System.out
+                               .println("Total Translatable Strings: " + translations.size());
+               TreeSet<TranslationEntry> trs = new TreeSet<>(translations.values());
+               writePOFile(out, trs);
+
+       }
+
+       public void add(String text, String line) {
+           if(text.contains("\r") || text.contains("\n")){
+               throw new Error("Malformed translation in " + line);
+           }
+               TranslationEntry i = translations.get(text);
+               if (i == null) {
+                       translations.put(text, new TranslationEntry(text, line));
+                       return;
+               }
+               i.add(line);
+       }
+
+       private void scanCode(LinkedList<TaintSource> taint) throws Error {
+               PrintWriter out = new PrintWriter(System.out);
+               Main m = new Main(out, out, false, null, null);
+               File[] fs = recurse(
+                               new File(new File(new File(base, "src"), "org"), "cacert"),
+                               new LinkedList<File>(), ".java").toArray(new File[0]);
+               String[] t = new String[fs.length + 3];
+               t[0] = "-cp";
+               t[1] = new File(base, "bin").getAbsolutePath();
+               t[2] = "-7";
+               for (int i = 0; i < fs.length; i++) {
+                       t[i + 3] = fs[i].getAbsolutePath();
+               }
+               m.configure(t);
+               FileSystem environment = m.getLibraryAccess();
+               CompilerOptions compilerOptions = new CompilerOptions(m.options);//new HashMap<>());//m.options);
+               compilerOptions.performMethodsFullRecovery = false;
+               compilerOptions.performStatementsRecovery = false;
+               //check
+               compilerOptions.sourceLevel = ClassFileConstants.JDK1_7;
+               compilerOptions.complianceLevel = ClassFileConstants.JDK1_7;
+               compilerOptions.originalComplianceLevel = ClassFileConstants.JDK1_7;
+
+               ProblemReporter pr = new ProblemReporter(m.getHandlingPolicy(),
+                               compilerOptions, m.getProblemFactory());
+               ITypeRequestor tr = new ITypeRequestor() {
+
+                       @Override
+                       public void accept(ISourceType[] sourceType,
+                                       PackageBinding packageBinding,
+                                       AccessRestriction accessRestriction) {
+                               throw new IllegalStateException("source type not implemented");
+                       }
+
+                       @Override
+                       public void accept(IBinaryType binaryType,
+                                       PackageBinding packageBinding,
+                                       AccessRestriction accessRestriction) {
+                               le.createBinaryTypeFrom(binaryType, packageBinding,
+                                               accessRestriction);
+                       }
+
+                       @Override
+                       public void accept(ICompilationUnit unit,
+                                       AccessRestriction accessRestriction) {
+                               throw new IllegalStateException(
+                                               "compilation unit not implemented");
+                       }
+               };
+               le = new LookupEnvironment(tr, compilerOptions, pr, environment);
+               Parser parser = new Parser(pr,
+                               compilerOptions.parseLiteralExpressionsAsConstants);
+               CompilationUnit[] sourceUnits = m.getCompilationUnits();
+               CompilationUnitDeclaration[] parsedUnits = new CompilationUnitDeclaration[sourceUnits.length];
+               for (int i = 0; i < parsedUnits.length; i++) {
+
+                       CompilationResult unitResult = new CompilationResult(
+                                       sourceUnits[i], i, parsedUnits.length,
+                                       compilerOptions.maxProblemsPerUnit);
+                       CompilationUnitDeclaration parsedUnit = parser.parse(
+                                       sourceUnits[i], unitResult);
+                       le.buildTypeBindings(parsedUnit, null /*no access restriction*/);
+                       parsedUnits[i] = parsedUnit;
+               }
+               le.completeTypeBindings();
+               for (int i = 0; i < parsedUnits.length; i++) {
+                       CompilationUnitDeclaration parsedUnit = parsedUnits[i];
+
+                       parser.getMethodBodies(parsedUnit);
+                       parsedUnit.scope.faultInTypes();
+                       parsedUnit.scope.verifyMethods(le.methodVerifier());
+                       parsedUnit.resolve();
+               }
+               for (int i = 0; i < parsedUnits.length; i++) {
+                       CompilationUnitDeclaration parsedUnit = parsedUnits[i];
+                       if (parsedUnit.compilationResult.problems != null) {
+                               int err = 0;
+                               for (int c = 0; c < parsedUnit.compilationResult.problemCount; c++) {
+                                       CategorizedProblem problem = parsedUnit.compilationResult.problems[c];
+                                       if (problem.isError()) {
+                                               err++;
+                                       }
+                                       if (OUTPUT_WARNINGS || problem.isError()) {
+                                               System.out.println(problem);
+                                               StringBuilder prob = new StringBuilder();
+                                               prob.append(parsedUnit.compilationResult.fileName);
+                                               prob.append(":");
+                                               prob.append(problem.getSourceLineNumber());
+                                               System.out.println(prob.toString());
+                                       }
+                               }
+                               if (err > 0) {
+                                       throw new Error();
+                               }
+                       }
+
+                       if (parsedUnit.types == null) {
+                               System.out.println("No types");
+
+                       } else {
+                               TranslationCollectingVisitor v = new TranslationCollectingVisitor(
+                                               parsedUnit,
+                                               taint.toArray(new TaintSource[taint.size()]), this);
+                               for (TypeDeclaration td : parsedUnit.types) {
+                                       td.traverse(v, td.scope);
+                               }
+                       }
+                       parsedUnits[i] = parsedUnit;
+               }
+       }
+       private void scanTemplates() throws UnsupportedEncodingException,
+                       FileNotFoundException {
+               File[] ts = recurse(
+                               new File(new File(new File(base, "src"), "org"), "cacert"),
+                               new LinkedList<File>(), ".templ").toArray(new File[0]);
+               for (File file : ts) {
+                       Template t = new Template(new InputStreamReader(
+                                       new FileInputStream(file), "UTF-8"));
+                       LinkedList<String> i = new LinkedList<String>();
+                       t.addTranslations(i);
+                       for (String string : i) {
+                               add(string,
+                                               file.getAbsolutePath().substring(
+                                                               base.getAbsolutePath().length() + 1)
+                                                               + ":1");
+                       }
+               }
+       }
+
+       static LookupEnvironment le;
+       private static final boolean OUTPUT_WARNINGS = false;
+
+       private LinkedList<TaintSource> taint;
+       public static void main(String[] args) throws IOException {
+               new TranslationCollector(new File(args[1]), new File(args[0]))
+                               .run(new File(args[2]));
+       }
+
+       public static void writePOFile(File target,
+                       Collection<TranslationEntry> strings) throws IOException {
+               PrintWriter out = new PrintWriter(target);
+               for (TranslationEntry s : strings) {
+                       out.print("#:");
+                       for (String st : s.getOccur()) {
+                               out.print(" " + st);
+                       }
+                       out.println();
+                       out.println("msgid \""
+                                       + s.text.replace("\\", "\\\\").replace("\"", "\\\"") + "\"");
+                       out.println("msgstr \"\"");
+                       out.println();
+               }
+               out.close();
+       }
+
+       private static List<File> recurse(File file, List<File> toAdd, String pt) {
+               if (file.isDirectory()) {
+                       for (File f : file.listFiles()) {
+                               recurse(f, toAdd, pt);
+                       }
+               } else {
+                       if (file.getName().endsWith(pt)) {
+                               toAdd.add(file);
+                       }
+               }
+               return toAdd;
+       }
+}
diff --git a/util-testing/org/cacert/gigi/localisation/conf.txt b/util-testing/org/cacert/gigi/localisation/conf.txt
new file mode 100644 (file)
index 0000000..e661604
--- /dev/null
@@ -0,0 +1,19 @@
+org.cacert.gigi.localisation Language.getTranslation(String),0
+org.cacert.gigi.pages Page.translate(ServletRequest, String),1
+org.cacert.gigi.output.template Form.outputError(PrintWriter, ServletRequest, String, Object[]),2
+org.cacert.gigi Gigi.MenuBuilder.putPage(String, Page, String),2
+org.cacert.gigi Gigi.MenuBuilder.getMenu(String),0
+org.cacert.gigi GigiApiException.GigiApiException(String),0=>org.cacert.gigi GigiApiException.formatPlain(PrintWriter),0
+org.cacert.gigi.output Menu.Menu(String),0=>org.cacert.gigi.output Menu.output(PrintWriter, Language, Map),0
+org.cacert.gigi.output SimpleMenuItem.SimpleMenuItem(String,String),1=>org.cacert.gigi.output SimpleMenuItem.output(PrintWriter, Language, Map),0
+org.cacert.gigi.dbObjects Digest.Digest(String),0
+org.cacert.gigi.dbObjects Certificate.CertificateStatus.CertificateStatus(String),0
+org.cacert.gigi.pages Page.Page(String),0
+org.cacert.gigi.pages OneFormPage.OneFormPage(String, Class),0
+org.cacert.gigi.pages StaticPage.StaticPage(String, InputStream),0
+org.cacert.gigi.output.template SprintfCommand.SprintfCommand(String, List),0=>org.cacert.gigi.output.template SprintfCommand.output(PrintWriter, Language, Map),0
+org.cacert.gigi.output.template SprintfCom---invalid---mand.SprintfCommand(String),0=>org.cacert.gigi.output.template Template.parseCommand(String),0
+org.cacert.gigi.output.template TranslateCommand.TranslateCommand(String),0=>org.cacert.gigi.output.template TranslateCommand.output(PrintWriter, Language, Map),0
+org.cacert.gigi.pages.account.domain DomainOverview.DomainOverview(String),0
+org.cacert.gigi.dbObjects Group.Group(String, String),1
+org.cacert.gigi.output.template SprintfCommand.createSimple(String, String[]),0
diff --git a/util-testing/org/cacert/gigi/pages/Manager.java b/util-testing/org/cacert/gigi/pages/Manager.java
new file mode 100644 (file)
index 0000000..649b794
--- /dev/null
@@ -0,0 +1,381 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Signature;
+import java.util.Base64;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.Gigi;
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.crypto.SPKAC;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.dbObjects.Assurance.AssuranceType;
+import org.cacert.gigi.dbObjects.Certificate;
+import org.cacert.gigi.dbObjects.Certificate.CertificateStatus;
+import org.cacert.gigi.dbObjects.CertificateOwner;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.dbObjects.Domain;
+import org.cacert.gigi.dbObjects.DomainPingType;
+import org.cacert.gigi.dbObjects.EmailAddress;
+import org.cacert.gigi.dbObjects.Group;
+import org.cacert.gigi.dbObjects.Name;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.email.EmailProvider;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.IterableDataset;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.pages.account.certs.CertificateRequest;
+import org.cacert.gigi.ping.DomainPinger;
+import org.cacert.gigi.ping.PingerDaemon;
+import org.cacert.gigi.util.AuthorizationContext;
+import org.cacert.gigi.util.DayDate;
+import org.cacert.gigi.util.Notary;
+
+import sun.security.x509.X509Key;
+
+public class Manager extends Page {
+
+    public static final String PATH = "/manager";
+
+    private static HashMap<DomainPingType, DomainPinger> dps;
+
+    private Manager() {
+        super("Test Manager");
+
+        try {
+            Field gigiInstance = Gigi.class.getDeclaredField("instance");
+            gigiInstance.setAccessible(true);
+            Gigi g = (Gigi) gigiInstance.get(null);
+
+            Field gigiPinger = Gigi.class.getDeclaredField("pinger");
+            gigiPinger.setAccessible(true);
+            PingerDaemon pd = (PingerDaemon) gigiPinger.get(g);
+
+            Field f = PingerDaemon.class.getDeclaredField("pingers");
+            f.setAccessible(true);
+            dps = (HashMap<DomainPingType, DomainPinger>) f.get(pd);
+            HashMap<DomainPingType, DomainPinger> pingers = new HashMap<>();
+            for (DomainPingType dpt : DomainPingType.values()) {
+                pingers.put(dpt, new PingerFetcher(dpt));
+            }
+            f.set(pd, pingers);
+        } catch (ReflectiveOperationException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public User getAssurer(int i) {
+        if (assurers[i] != null) {
+            return assurers[i];
+        }
+        try {
+            User u = createAssurer(i);
+            assurers[i] = u;
+
+        } catch (ReflectiveOperationException | GigiApiException e) {
+            e.printStackTrace();
+        }
+        return assurers[i];
+    }
+
+    private User createAssurer(int i) throws GigiApiException, IllegalAccessException {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `notary` SET `from`=?, `to`=?, `points`=?, `location`=?, `date`=?")) {
+            String mail = "test-assurer" + i + "@example.com";
+            User u = User.getByEmail(mail);
+            if (u == null) {
+                System.out.println("Creating assurer");
+                createUser(mail);
+                u = User.getByEmail(mail);
+                passCATS(u);
+                ps.setInt(1, u.getId());
+                ps.setInt(2, u.getId());
+                ps.setInt(3, 100);
+                ps.setString(4, "Manager init code");
+                ps.setString(5, "1990-01-01");
+                ps.execute();
+            }
+            return u;
+        }
+    }
+
+    private void passCATS(User u) {
+        try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO cats_passed SET user_id=?, variant_id=1")) {
+            ps.setInt(1, u.getId());
+            ps.execute();
+        }
+    }
+
+    private static Manager instance;
+
+    Template t = new Template(Manager.class.getResource("ManagerMails.templ"));
+
+    HashMap<String, LinkedList<String>> emails = new HashMap<>();
+
+    private static TreeSet<String> pingExempt = new TreeSet<>();
+
+    public static Manager getInstance() {
+        if (instance == null) {
+            instance = new Manager();
+        }
+        return instance;
+    }
+
+    public static class MailFetcher extends EmailProvider {
+
+        public MailFetcher(Properties p) {}
+
+        @Override
+        public String checkEmailServer(int forUid, String address) throws IOException {
+            return OK;
+        }
+
+        @Override
+        public synchronized void sendmail(String to, String subject, String message, String from, String replyto, String toname, String fromname, String errorsto, boolean extra) throws IOException {
+            HashMap<String, LinkedList<String>> mails = Manager.getInstance().emails;
+            LinkedList<String> hismails = mails.get(to);
+            if (hismails == null) {
+                mails.put(to, hismails = new LinkedList<>());
+            }
+            hismails.addFirst(subject + "\n" + message);
+        }
+
+    }
+
+    public class PingerFetcher extends DomainPinger {
+
+        private DomainPingType dpt;
+
+        public PingerFetcher(DomainPingType dpt) {
+            this.dpt = dpt;
+        }
+
+        @Override
+        public void ping(Domain domain, String configuration, CertificateOwner target, int confId) {
+            System.out.println("Test: " + domain);
+            if (pingExempt.contains(domain.getSuffix())) {
+                enterPingResult(confId, DomainPinger.PING_SUCCEDED, "Succeeded by TestManager pass-by", null);
+            } else {
+                dps.get(dpt).ping(domain, configuration, target, confId);
+            }
+        }
+
+    }
+
+    public void batchCreateUsers(String mailPrefix, String domain, int amount, PrintWriter out) {
+
+        try {
+            if (amount > 100) {
+                out.print("100 at most, please.");
+                return;
+            }
+            for (int i = 0; i < amount; i++) {
+                String email = mailPrefix + i + "@" + domain;
+                createUser(email);
+            }
+        } catch (ReflectiveOperationException e) {
+            out.println("failed");
+            e.printStackTrace();
+        } catch (GigiApiException e) {
+            out.println("failed: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    private void createUser(String email) throws GigiApiException, IllegalAccessException {
+        Calendar gc = GregorianCalendar.getInstance();
+        gc.set(1990, 0, 1);
+        User u = new User(email, "xvXV12°§", new Name("Först", "Läst", "Müddle", "Süffix"), new DayDate(gc.getTime().getTime()), Locale.ENGLISH);
+        EmailAddress ea = u.getEmails()[0];
+        verify(email, ea);
+    }
+
+    private void verify(String email, EmailAddress ea) throws GigiApiException {
+        LinkedList<String> i = emails.get(email);
+        while (i.size() > 0 && !ea.isVerified()) {
+            String lst = i.getLast();
+            Pattern p = Pattern.compile("hash=([a-zA-Z0-9]+)");
+            Matcher m = p.matcher(lst);
+            if (m.find()) {
+                ea.verify(m.group(1));
+            }
+            i.removeLast();
+        }
+        // ea.verify(hash);
+    }
+
+    User[] assurers = new User[25];
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        if (req.getParameter("create") != null) {
+            batchCreateUsers(req.getParameter("prefix"), req.getParameter("suffix"), Integer.parseInt(req.getParameter("amount")), resp.getWriter());
+            resp.getWriter().println("User batch created.");
+        } else if (req.getParameter("addpriv") != null || req.getParameter("delpriv") != null) {
+            User u = User.getByEmail(req.getParameter("email"));
+            if (u == null) {
+                resp.getWriter().println("User not found.");
+                return;
+            }
+            if (req.getParameter("addpriv") != null) {
+                u.grantGroup(u, Group.getByString(req.getParameter("priv")));
+                resp.getWriter().println("Privilege granted");
+            } else {
+                u.revokeGroup(u, Group.getByString(req.getParameter("priv")));
+                resp.getWriter().println("Privilege revoked");
+            }
+        } else if (req.getParameter("fetch") != null) {
+            String mail = req.getParameter("femail");
+            fetchMails(req, resp, mail);
+        } else if (req.getParameter("cats") != null) {
+            String mail = req.getParameter("catsEmail");
+            User byEmail = User.getByEmail(mail);
+            if (byEmail == null) {
+                resp.getWriter().println("User not found.");
+                return;
+            }
+            passCATS(byEmail);
+            resp.getWriter().println("User has been passed CATS");
+        } else if (req.getParameter("assure") != null) {
+            String mail = req.getParameter("assureEmail");
+            User byEmail = User.getByEmail(mail);
+            if (byEmail == null) {
+                resp.getWriter().println("User not found.");
+                return;
+            }
+            try {
+                for (int i = 0; i < 10; i++) {
+                    Notary.assure(getAssurer(i), byEmail, byEmail.getName(), byEmail.getDoB(), 10, "Testmanager Assure up code", "2014-11-06", AssuranceType.FACE_TO_FACE);
+                }
+            } catch (GigiApiException e) {
+                throw new Error(e);
+            }
+            resp.getWriter().println("User has been assured.");
+        } else if (req.getParameter("letassure") != null) {
+            String mail = req.getParameter("letassureEmail");
+            User byEmail = User.getByEmail(mail);
+            try {
+                for (int i = 0; i < 25; i++) {
+                    User a = getAssurer(i);
+                    Notary.assure(byEmail, a, a.getName(), a.getDoB(), 10, "Testmanager exp up code", "2014-11-06", AssuranceType.FACE_TO_FACE);
+                }
+            } catch (GigiApiException e) {
+                throw new Error(e);
+            }
+        } else if (req.getParameter("addEmail") != null) {
+            User u = User.getByEmail(req.getParameter("addEmailEmail"));
+            try {
+                EmailAddress ea = new EmailAddress(u, req.getParameter("addEmailNew"), Locale.ENGLISH);
+                verify(ea.getAddress(), ea);
+            } catch (IllegalArgumentException e) {
+                e.printStackTrace();
+                resp.getWriter().println("An internal error occured.");
+            } catch (GigiApiException e) {
+                e.format(resp.getWriter(), Language.getInstance(Locale.ENGLISH));
+            }
+        } else if (req.getParameter("addCert") != null) {
+            User u = User.getByEmail(req.getParameter("addCertEmail"));
+            try {
+                KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+                kpg.initialize(4096);
+                KeyPair kp = kpg.generateKeyPair();
+                SPKAC s = new SPKAC((X509Key) kp.getPublic(), "challange");
+                Signature sign = Signature.getInstance("SHA512withRSA");
+                sign.initSign(kp.getPrivate());
+
+                byte[] res = s.getEncoded(sign);
+
+                CertificateRequest cr = new CertificateRequest(new AuthorizationContext(u, u), Base64.getEncoder().encodeToString(res), "challange");
+                cr.update(CertificateRequest.DEFAULT_CN, Digest.SHA512.toString(), "client", null, "", "email:" + u.getEmail(), resp.getWriter(), req);
+                Certificate draft = cr.draft();
+                draft.issue(null, "2y", u).waitFor(10000);
+                if (draft.getStatus() == CertificateStatus.ISSUED) {
+                    resp.getWriter().println("added certificate");
+                } else {
+                    resp.getWriter().println("signer failed");
+                }
+            } catch (GeneralSecurityException e1) {
+                e1.printStackTrace();
+                resp.getWriter().println("error");
+            } catch (GigiApiException e) {
+                e.format(resp.getWriter(), Language.getInstance(Locale.ENGLISH));
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+                resp.getWriter().println("interrupted");
+            }
+
+        } else if (req.getParameter("addExDom") != null) {
+            String dom = req.getParameter("exemtDom");
+            pingExempt.add(dom);
+            resp.getWriter().println("Updated domains exempt from pings. Current set: <br/>");
+            resp.getWriter().println(pingExempt);
+        } else if (req.getParameter("delExDom") != null) {
+            String dom = req.getParameter("exemtDom");
+            pingExempt.remove(dom);
+            resp.getWriter().println("Updated domains exempt from pings. Current set: <br/>");
+            resp.getWriter().println(pingExempt);
+        }
+    }
+
+    private void fetchMails(HttpServletRequest req, HttpServletResponse resp, String mail) throws IOException {
+        final LinkedList<String> mails = emails.get(mail);
+        HashMap<String, Object> vars = new HashMap<>();
+        vars.put("mail", mail);
+        if (mails != null) {
+            vars.put("mails", new IterableDataset() {
+
+                Iterator<String> s = mails.iterator();
+
+                @Override
+                public boolean next(Language l, Map<String, Object> vars) {
+                    if ( !s.hasNext()) {
+                        return false;
+                    }
+                    vars.put("body", s.next().replaceAll("(https?://\\S+)", "<a href=\"$1\">$1</a>"));
+                    return true;
+                }
+            });
+        }
+        t.output(resp.getWriter(), getLanguage(req), vars);
+        if (mails == null) {
+            resp.getWriter().println("No mails");
+
+        }
+    }
+
+    private Template form = new Template(Manager.class.getResource("Manager.templ"));
+
+    @Override
+    public boolean needsLogin() {
+        return false;
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        String pi = req.getPathInfo().substring(PATH.length());
+        if (pi.length() > 1 && pi.startsWith("/fetch-")) {
+            String mail = pi.substring(pi.indexOf('-', 2) + 1);
+            fetchMails(req, resp, mail);
+            return;
+        }
+
+        form.output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+    }
+}
diff --git a/util-testing/org/cacert/gigi/pages/Manager.templ b/util-testing/org/cacert/gigi/pages/Manager.templ
new file mode 100644 (file)
index 0000000..8c32a8c
--- /dev/null
@@ -0,0 +1,91 @@
+<form method='post'>
+<table class="table">
+<tr><td>
+Batch create users:
+</td><td></td><td>
+<div>
+  Email: 
+  <input type="text" name="prefix"/> NNN@
+  <input type="text" name="suffix"/>
+</div>
+Amount:  <input type="slider" name="amount"/> <input type="submit" name="create" value="Create Users"/>
+</td></tr>
+
+<tr><td>
+Add privilege:
+</td><td>
+Email: <input type="text" name="email"/> 
+</td><td>
+<select name="priv">
+<option>supporter</option>
+<option>arbitrator</option>
+<option>blockedassuree</option>
+<option>blockedassurer</option>
+<option>blockedlogin</option>
+<option>ttp-assurer</option>
+<option>ttp-applicant</option>
+<option>nucleus-assurer</option>
+<option>codesigning</option>
+<option>orgassurer</option>
+</select>
+<input type="submit" name="addpriv" value="Grant Privillege"/>
+<input type="submit" name="delpriv" value="Revoke Privillege"/>
+</td><tr>
+
+<tr><td>
+Receive Mails:
+</td><td>
+Email: <input type="text" name="femail"/>
+</td><td>
+<input type="submit" value="Receive Mails" name="fetch"/>
+</td><tr>
+
+<tr><td>
+Add CATs entry:
+</td><td>
+Email: <input type="text" name="catsEmail"/>
+</td><td>
+<input type="submit" value="Add CATs" name="cats"/>
+</td></tr>
+<tr><td>
+Add 100 Assurance points:
+</td><td>
+Email: <input type="text" name="assureEmail"/>
+</td><td>
+<input type="submit" value="Assure 100 Points" name="assure"/>
+</td></tr>
+
+<tr><td>
+Assure 25 others (get 25 experience points)    :
+</td><td>
+Email: <input type="text" name="letassureEmail"/>
+</td><td>
+<input type="submit" value="Add 25 experience Points" name="letassure"/>
+</td></tr>
+
+<tr><td>
+Add verified Email:
+</td><td>
+Email: <input type="text" name="addEmailEmail"/>
+</td><td>
+new Email: <input type="text" name="addEmailNew"/>
+<input type="submit" value="Add verified Email" name="addEmail"/>
+</td></tr>
+
+<tr><td>
+Add Client Cert:
+</td><td>
+Email: <input type="text" name="addCertEmail"/>
+</td><td>
+<input type="submit" value="Add an certificate" name="addCert"/>
+</td></tr>
+
+<tr><td>
+Exempt Domain from Pinging:
+</td><td>
+</td><td>
+Domain: <input type="text" name="exemtDom"/>
+<input type="submit" value="Exempt this domain from pings" name="addExDom"/>
+<input type="submit" value="Do not exempt this domain from pings" name="delExDom"/>
+</td></tr>
+</table>
diff --git a/util-testing/org/cacert/gigi/pages/ManagerMails.templ b/util-testing/org/cacert/gigi/pages/ManagerMails.templ
new file mode 100644 (file)
index 0000000..6298803
--- /dev/null
@@ -0,0 +1,5 @@
+<a href="/manager/fetch-<?=$mail?>"> Quick Link </a><br/>
+
+<? foreach($mails) { ?>
+<pre><?=$!body?></pre>
+<? } ?>
diff --git a/util-testing/org/cacert/gigi/util/IOUtils.java b/util-testing/org/cacert/gigi/util/IOUtils.java
new file mode 100644 (file)
index 0000000..c5dbd8d
--- /dev/null
@@ -0,0 +1,63 @@
+package org.cacert.gigi.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.net.URLConnection;
+
+public class IOUtils {
+
+    private IOUtils() {
+
+    }
+
+    public static String readURL(URLConnection in) {
+        try {
+            if ( !in.getContentType().equals("text/html; charset=UTF-8")) {
+                if (in instanceof HttpURLConnection && ((HttpURLConnection) in).getResponseCode() != 200) {
+                    System.err.println(readURL(new InputStreamReader(((HttpURLConnection) in).getErrorStream(), "UTF-8")));
+                }
+                throw new Error("Unrecognized content-type: " + in.getContentType());
+            }
+            return readURL(new InputStreamReader(in.getInputStream(), "UTF-8"));
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+
+    }
+
+    public static String readURL(Reader in) {
+        CharArrayWriter caw = new CharArrayWriter();
+        char[] buffer = new char[1024];
+        int len = 0;
+        try {
+            while ((len = in.read(buffer)) > 0) {
+                caw.write(buffer, 0, len);
+            }
+            in.close();
+            return new String(caw.toCharArray());
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+
+    }
+
+    public static byte[] readURL(InputStream in) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int len = 0;
+        try {
+            while ((len = in.read(buffer)) > 0) {
+                baos.write(buffer, 0, len);
+            }
+            in.close();
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+}
diff --git a/util-testing/org/cacert/gigi/util/SimpleSigner.java b/util-testing/org/cacert/gigi/util/SimpleSigner.java
new file mode 100644 (file)
index 0000000..296eb99
--- /dev/null
@@ -0,0 +1,607 @@
+package org.cacert.gigi.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.math.BigInteger;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Base64;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.TimeZone;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.cacert.gigi.crypto.SPKAC;
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.database.DatabaseConnection.Link;
+import org.cacert.gigi.database.GigiPreparedStatement;
+import org.cacert.gigi.database.GigiResultSet;
+import org.cacert.gigi.dbObjects.Certificate.CSRType;
+import org.cacert.gigi.dbObjects.Certificate.SANType;
+import org.cacert.gigi.dbObjects.Certificate.SubjectAlternateName;
+import org.cacert.gigi.dbObjects.CertificateProfile;
+import org.cacert.gigi.dbObjects.Digest;
+import org.cacert.gigi.output.DateSelector;
+
+import sun.security.pkcs10.PKCS10;
+import sun.security.util.DerOutputStream;
+import sun.security.util.DerValue;
+import sun.security.util.ObjectIdentifier;
+import sun.security.x509.AVA;
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.GeneralNameInterface;
+import sun.security.x509.RDN;
+import sun.security.x509.X500Name;
+
+public class SimpleSigner {
+
+    private static GigiPreparedStatement warnMail;
+
+    private static GigiPreparedStatement updateMail;
+
+    private static GigiPreparedStatement readyCerts;
+
+    private static GigiPreparedStatement getSANSs;
+
+    private static GigiPreparedStatement revoke;
+
+    private static GigiPreparedStatement revokeCompleted;
+
+    private static GigiPreparedStatement finishJob;
+
+    private static GigiPreparedStatement locateCA;
+
+    private static volatile boolean running = true;
+
+    private static Thread runner;
+
+    private static SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss'Z'");
+
+    static {
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+
+    public static void main(String[] args) throws IOException, SQLException, InterruptedException {
+        Properties p = new Properties();
+        try (Reader reader = new InputStreamReader(new FileInputStream("config/gigi.properties"), "UTF-8")) {
+            p.load(reader);
+        }
+        DatabaseConnection.init(p);
+
+        runSigner();
+    }
+
+    public static void stopSigner() throws InterruptedException {
+        Thread capturedRunner;
+        synchronized (SimpleSigner.class) {
+            if (runner == null) {
+                throw new IllegalStateException("already stopped");
+            }
+            capturedRunner = runner;
+            running = false;
+            SimpleSigner.class.notifyAll();
+        }
+        capturedRunner.join();
+    }
+
+    public synchronized static void runSigner() throws SQLException, IOException, InterruptedException {
+        if (runner != null) {
+            throw new IllegalStateException("already running");
+        }
+        running = true;
+
+        runner = new Thread() {
+
+            @Override
+            public void run() {
+                try (Link l = DatabaseConnection.newLink(false)) {
+                    readyCerts = new GigiPreparedStatement("SELECT certs.id AS id, certs.csr_name, jobs.id AS jobid, csr_type, md, `executeFrom`, `executeTo`, profile FROM jobs " + //
+                            "INNER JOIN certs ON certs.id=jobs.`targetId` " + //
+                            "INNER JOIN profiles ON profiles.id=certs.profile " + //
+                            "WHERE jobs.state='open' "//
+                            + "AND task='sign'");
+
+                    getSANSs = new GigiPreparedStatement("SELECT contents, type FROM `subjectAlternativeNames` " + //
+                            "WHERE `certId`=?");
+
+                    updateMail = new GigiPreparedStatement("UPDATE certs SET crt_name=?," + " created=NOW(), serial=?, caid=? WHERE id=?");
+                    warnMail = new GigiPreparedStatement("UPDATE jobs SET warning=warning+1, state=IF(warning<3, 'open','error') WHERE id=?");
+
+                    revoke = new GigiPreparedStatement("SELECT certs.id, certs.csr_name,jobs.id FROM jobs INNER JOIN certs ON jobs.`targetId`=certs.id" + " WHERE jobs.state='open' AND task='revoke'");
+                    revokeCompleted = new GigiPreparedStatement("UPDATE certs SET revoked=NOW() WHERE id=?");
+
+                    finishJob = new GigiPreparedStatement("UPDATE jobs SET state='done' WHERE id=?");
+
+                    locateCA = new GigiPreparedStatement("SELECT id FROM cacerts WHERE keyname=?");
+
+                    work();
+                } catch (InterruptedException e) {
+                    throw new Error(e);
+                }
+            }
+
+        };
+        runner.start();
+    }
+
+    private synchronized static void work() {
+        try {
+            gencrl();
+        } catch (IOException e2) {
+            e2.printStackTrace();
+        } catch (InterruptedException e2) {
+            e2.printStackTrace();
+        }
+
+        while (running) {
+            try {
+                signCertificates();
+                revokeCertificates();
+
+                SimpleSigner.class.wait(5000);
+            } catch (IOException e) {
+                e.printStackTrace();
+            } catch (SQLException e) {
+                e.printStackTrace();
+            } catch (InterruptedException e1) {
+            }
+        }
+        runner = null;
+    }
+
+    private static void revokeCertificates() throws SQLException, IOException, InterruptedException {
+        GigiResultSet rs = revoke.executeQuery();
+        boolean worked = false;
+        while (rs.next()) {
+            int id = rs.getInt(1);
+            worked = true;
+            System.out.println("Revoke faked: " + id);
+            revokeCompleted.setInt(1, id);
+            revokeCompleted.execute();
+            finishJob.setInt(1, rs.getInt(3));
+            finishJob.execute();
+        }
+        if (worked) {
+            gencrl();
+        }
+    }
+
+    private static void gencrl() throws IOException, InterruptedException {
+        if (true) {
+            return;
+        }
+        String[] call = new String[] {
+                "openssl", "ca",//
+                "-cert",
+                "../unassured.crt",//
+                "-keyfile",
+                "../unassured.key",//
+                "-gencrl",//
+                "-crlhours",//
+                "12",//
+                "-out",
+                "../unassured.crl",//
+                "-config",
+                "../selfsign.config"
+
+        };
+        Process p1 = Runtime.getRuntime().exec(call, null, new File("keys/unassured.ca"));
+        if (p1.waitFor() != 0) {
+            System.out.println("Error while generating crl.");
+        }
+    }
+
+    private static int counter = 0;
+
+    private static void signCertificates() throws SQLException {
+        GigiResultSet rs = readyCerts.executeQuery();
+
+        Calendar c = Calendar.getInstance();
+        c.setTimeZone(TimeZone.getTimeZone("UTC"));
+        while (rs.next()) {
+            String csrname = rs.getString("csr_name");
+            int id = rs.getInt("id");
+            System.out.println("sign: " + csrname);
+            try {
+                String csrType = rs.getString("csr_type");
+                CSRType ct = CSRType.valueOf(csrType);
+                File crt = KeyStorage.locateCrt(id);
+
+                Timestamp from = rs.getTimestamp("executeFrom");
+                String length = rs.getString("executeTo");
+                Date fromDate;
+                Date toDate;
+                if (from == null) {
+                    fromDate = new Date(System.currentTimeMillis());
+                } else {
+                    fromDate = new Date(from.getTime());
+                }
+                if (length.endsWith("m") || length.endsWith("y")) {
+                    String num = length.substring(0, length.length() - 1);
+                    int inter = Integer.parseInt(num);
+                    c.setTime(fromDate);
+                    if (length.endsWith("m")) {
+                        c.add(Calendar.MONTH, inter);
+                    } else {
+                        c.add(Calendar.YEAR, inter);
+                    }
+                    toDate = c.getTime();
+                } else {
+                    toDate = DateSelector.getDateFormat().parse(length);
+                }
+
+                getSANSs.setInt(1, id);
+                GigiResultSet san = getSANSs.executeQuery();
+
+                LinkedList<SubjectAlternateName> altnames = new LinkedList<>();
+                while (san.next()) {
+                    altnames.add(new SubjectAlternateName(SANType.valueOf(san.getString("type").toUpperCase()), san.getString("contents")));
+                }
+                // TODO look them up!
+                // cfg.println("keyUsage=critical," +
+                // "digitalSignature, keyEncipherment, keyAgreement");
+                // cfg.println("extendedKeyUsage=critical," + "clientAuth");
+                // cfg.close();
+
+                int profile = rs.getInt("profile");
+                CertificateProfile cp = CertificateProfile.getById(profile);
+                String s = cp.getId() + "";
+                while (s.length() < 4) {
+                    s = "0" + s;
+                }
+                s += "-" + cp.getKeyName() + ".cfg";
+                Properties caP = new Properties();
+                try (FileInputStream inStream = new FileInputStream("signer/profiles/" + s)) {
+                    caP.load(inStream);
+                }
+                String ca = caP.getProperty("ca") + "_2015_1";
+
+                HashMap<String, String> subj = new HashMap<>();
+                try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT name, value FROM `certAvas` WHERE `certId`=?")) {
+                    ps.setInt(1, rs.getInt("id"));
+                    GigiResultSet rs2 = ps.executeQuery();
+                    while (rs2.next()) {
+                        String name = rs2.getString("name");
+                        if (name.equals("EMAIL")) {
+                            name = "emailAddress";
+                        }
+                        subj.put(name, rs2.getString("value"));
+                    }
+                }
+                if (subj.size() == 0) {
+                    subj.put("CN", "<empty>");
+                    System.out.println("WARNING: DN was empty");
+                }
+                System.out.println(subj);
+
+                PublicKey pk;
+                byte[] data = IOUtils.readURL(new FileInputStream(csrname));
+                if (ct == CSRType.SPKAC) {
+                    String dt = new String(data, "UTF-8");
+                    if (dt.startsWith("SPKAC=")) {
+                        dt = dt.substring(6);
+                        data = dt.getBytes("UTF-8");
+                        System.out.println(dt);
+                    }
+                    SPKAC sp = new SPKAC(Base64.getDecoder().decode(data));
+                    pk = sp.getPubkey();
+                } else {
+                    PKCS10 p10 = new PKCS10(PEM.decode("(NEW )?CERTIFICATE REQUEST", new String(data, "UTF-8")));
+                    pk = p10.getSubjectPublicKeyInfo();
+                }
+                PrivateKey i = loadOpensslKey(new File("signer/ca/" + ca + "/ca.key"));
+
+                X509Certificate root = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new FileInputStream("signer/ca/" + ca + "/ca.crt"));
+                byte[] cert = generateCert(pk, i, subj, root.getSubjectX500Principal(), altnames, fromDate, toDate, Digest.valueOf(rs.getString("md").toUpperCase()), caP.getProperty("eku"));
+                PrintWriter out = new PrintWriter(crt);
+                out.println("-----BEGIN CERTIFICATE-----");
+                out.println(Base64.getMimeEncoder().encodeToString(cert));
+                out.println("-----END CERTIFICATE-----");
+                out.close();
+
+                try (InputStream is = new FileInputStream(crt)) {
+                    locateCA.setString(1, ca);
+                    GigiResultSet caRs = locateCA.executeQuery();
+                    if ( !caRs.next()) {
+                        throw new Error("ca " + ca + " was not found");
+                    }
+
+                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
+                    X509Certificate crtp = (X509Certificate) cf.generateCertificate(is);
+                    BigInteger serial = crtp.getSerialNumber();
+                    updateMail.setString(1, crt.getPath());
+                    updateMail.setString(2, serial.toString(16));
+                    updateMail.setInt(3, caRs.getInt("id"));
+                    updateMail.setInt(4, id);
+                    updateMail.execute();
+
+                    finishJob.setInt(1, rs.getInt("jobid"));
+                    finishJob.execute();
+                    System.out.println("signed: " + id);
+                    continue;
+                }
+
+            } catch (GeneralSecurityException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            } catch (ParseException e) {
+                e.printStackTrace();
+            }
+            System.out.println("Error with: " + id);
+            warnMail.setInt(1, rs.getInt("jobid"));
+            warnMail.execute();
+
+        }
+        rs.close();
+    }
+
+    private static PrivateKey loadOpensslKey(File f) throws FileNotFoundException, IOException, InvalidKeySpecException, NoSuchAlgorithmException {
+        byte[] p8b = PEM.decode("RSA PRIVATE KEY", new String(IOUtils.readURL(new FileInputStream(f))));
+        DerOutputStream dos = new DerOutputStream();
+        dos.putInteger(0);
+        new AlgorithmId(new ObjectIdentifier(new int[] {
+                1, 2, 840, 113549, 1, 1, 1
+        })).encode(dos);
+        dos.putOctetString(p8b);
+        byte[] ctx = dos.toByteArray();
+        dos.reset();
+        dos.write(DerValue.tag_Sequence, ctx);
+        PKCS8EncodedKeySpec p8 = new PKCS8EncodedKeySpec(dos.toByteArray());
+        PrivateKey i = KeyFactory.getInstance("RSA").generatePrivate(p8);
+        return i;
+    }
+
+    public static synchronized byte[] generateCert(PublicKey pk, PrivateKey prk, Map<String, String> subj, X500Principal issuer, List<SubjectAlternateName> altnames, Date fromDate, Date toDate, Digest digest, String eku) throws IOException, GeneralSecurityException {
+        File f = Paths.get("signer", "serial").toFile();
+        if ( !f.exists()) {
+            try (FileOutputStream fos = new FileOutputStream(f)) {
+                fos.write("1".getBytes("UTF-8"));
+            }
+        }
+        try (FileInputStream fr = new FileInputStream(f)) {
+            byte[] serial = IOUtils.readURL(fr);
+            BigInteger ser = new BigInteger(new String(serial).trim());
+            ser = ser.add(BigInteger.ONE);
+
+            PrintWriter pw = new PrintWriter(f);
+            pw.println(ser);
+            pw.close();
+            if (digest != Digest.SHA256 && digest != Digest.SHA512) {
+                System.err.println("assuming sha256 either way ;-): " + digest);
+                digest = Digest.SHA256;
+            }
+            ObjectIdentifier sha512withrsa = new ObjectIdentifier(new int[] {
+                    1, 2, 840, 113549, 1, 1, digest == Digest.SHA256 ? 11 : 13
+            });
+            AlgorithmId aid = new AlgorithmId(sha512withrsa);
+            Signature s = Signature.getInstance(digest == Digest.SHA256 ? "SHA256withRSA" : "SHA512withRSA");
+
+            DerOutputStream cert = new DerOutputStream();
+            DerOutputStream content = new DerOutputStream();
+            {
+                DerOutputStream version = new DerOutputStream();
+                version.putInteger(2); // v3
+                content.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 0), version);
+            }
+            content.putInteger(ser); // Serial
+            aid.encode(content);
+
+            {
+                content.write(issuer.getEncoded());
+            }
+            {
+                DerOutputStream notAround = new DerOutputStream();
+                notAround.putUTCTime(fromDate);
+                notAround.putUTCTime(toDate);
+                content.write(DerValue.tag_Sequence, notAround);
+            }
+            {
+
+                X500Name xn = genX500Name(subj);
+                content.write(xn.getEncoded());
+            }
+            {
+                content.write(pk.getEncoded());
+            }
+            {
+                DerOutputStream extensions = new DerOutputStream();
+                {
+                    addExtension(extensions, new ObjectIdentifier(new int[] {
+                            2, 5, 29, 17
+                    }), generateSAN(altnames));
+                    addExtension(extensions, new ObjectIdentifier(new int[] {
+                            2, 5, 29, 15
+                    }), generateKU());
+                    addExtension(extensions, new ObjectIdentifier(new int[] {
+                            2, 5, 29, 37
+                    }), generateEKU(eku));
+                }
+                DerOutputStream extensionsSeq = new DerOutputStream();
+                extensionsSeq.write(DerValue.tag_Sequence, extensions);
+                content.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte) 3), extensionsSeq);
+            }
+
+            DerOutputStream contentSeq = new DerOutputStream();
+
+            contentSeq.write(DerValue.tag_Sequence, content.toByteArray());
+
+            s.initSign(prk);
+            s.update(contentSeq.toByteArray());
+
+            aid.encode(contentSeq);
+            contentSeq.putBitString(s.sign());
+            cert.write(DerValue.tag_Sequence, contentSeq);
+
+            // X509Certificate c = (X509Certificate)
+            // CertificateFactory.getInstance("X509").generateCertificate(new
+            // ByteArrayInputStream(cert.toByteArray()));
+            // c.verify(pk); only for self-signeds
+
+            byte[] res = cert.toByteArray();
+            cert.close();
+            return res;
+        }
+
+    }
+
+    private static byte[] generateKU() throws IOException {
+        try (DerOutputStream dos = new DerOutputStream()) {
+            dos.putBitString(new byte[] {
+                (byte) 0b10101000
+            });
+            return dos.toByteArray();
+        }
+    }
+
+    private static byte[] generateEKU(String eku) throws IOException {
+
+        DerOutputStream dos = new DerOutputStream();
+        for (String name : eku.split(",")) {
+            ObjectIdentifier oid;
+            switch (name) {
+            case "serverAuth":
+                oid = new ObjectIdentifier("1.3.6.1.5.5.7.3.1");
+                break;
+            case "clientAuth":
+                oid = new ObjectIdentifier("1.3.6.1.5.5.7.3.2");
+                break;
+            case "codeSigning":
+                oid = new ObjectIdentifier("1.3.6.1.5.5.7.3.3");
+                break;
+            case "emailProtection":
+                oid = new ObjectIdentifier("1.3.6.1.5.5.7.3.4");
+                break;
+            case "OCSPSigning":
+                oid = new ObjectIdentifier("1.3.6.1.5.5.7.3.9");
+                break;
+
+            default:
+                throw new Error(name);
+            }
+            dos.putOID(oid);
+        }
+        byte[] data = dos.toByteArray();
+        dos.reset();
+        dos.write(DerValue.tag_Sequence, data);
+        return dos.toByteArray();
+    }
+
+    public static X500Name genX500Name(Map<String, String> subj) throws IOException {
+        LinkedList<RDN> rdns = new LinkedList<>();
+        for (Entry<String, String> i : subj.entrySet()) {
+            RDN rdn = genRDN(i);
+            rdns.add(rdn);
+        }
+        return new X500Name(rdns.toArray(new RDN[rdns.size()]));
+    }
+
+    private static RDN genRDN(Entry<String, String> i) throws IOException {
+        DerOutputStream dos = new DerOutputStream();
+        dos.putUTF8String(i.getValue());
+        int[] oid;
+        String key = i.getKey();
+        switch (key) {
+        case "CN":
+            oid = new int[] {
+                    2, 5, 4, 3
+            };
+            break;
+        case "EMAIL":
+        case "emailAddress":
+            oid = new int[] {
+                    1, 2, 840, 113549, 1, 9, 1
+            };
+            break;
+        case "O":
+            oid = new int[] {
+                    2, 5, 4, 10
+            };
+            break;
+        case "OU":
+            oid = new int[] {
+                    2, 5, 4, 11
+            };
+            break;
+        case "ST":
+            oid = new int[] {
+                    2, 5, 4, 8
+            };
+            break;
+        case "L":
+            oid = new int[] {
+                    2, 5, 4, 7
+            };
+            break;
+        case "C":
+            oid = new int[] {
+                    2, 5, 4, 6
+            };
+            break;
+        default:
+            dos.close();
+            throw new Error("unknown RDN-type: " + key);
+        }
+        RDN rdn = new RDN(new AVA(new ObjectIdentifier(oid), new DerValue(dos.toByteArray())));
+        dos.close();
+        return rdn;
+    }
+
+    private static void addExtension(DerOutputStream extensions, ObjectIdentifier oid, byte[] extContent) throws IOException {
+        DerOutputStream SANs = new DerOutputStream();
+        SANs.putOID(oid);
+        SANs.putOctetString(extContent);
+
+        extensions.write(DerValue.tag_Sequence, SANs);
+    }
+
+    private static byte[] generateSAN(List<SubjectAlternateName> altnames) throws IOException {
+        DerOutputStream SANContent = new DerOutputStream();
+        for (SubjectAlternateName san : altnames) {
+            byte type = 0;
+            if (san.getType() == SANType.DNS) {
+                type = (byte) GeneralNameInterface.NAME_DNS;
+            } else if (san.getType() == SANType.EMAIL) {
+                type = (byte) GeneralNameInterface.NAME_RFC822;
+            } else {
+                SANContent.close();
+                throw new Error("" + san.getType());
+            }
+            SANContent.write(DerValue.createTag(DerValue.TAG_CONTEXT, false, type), san.getName().getBytes("UTF-8"));
+        }
+        DerOutputStream SANSeqContent = new DerOutputStream();
+        SANSeqContent.write(DerValue.tag_Sequence, SANContent);
+        byte[] byteArray = SANSeqContent.toByteArray();
+        SANContent.close();
+        SANSeqContent.close();
+        return byteArray;
+    }
+}
index 547922e6130cb78f02c2d8b7e1fad29e361791d1..da164a56b04f3207d36bf2186d3476b76a5b1f91 100644 (file)
@@ -1,57 +1,70 @@
 package org.cacert.gigi.util;
 
 import java.io.File;
-import java.io.FileReader;
+import java.io.FileInputStream;
 import java.io.IOException;
-import java.nio.file.Files;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.Properties;
 
+import org.cacert.gigi.database.DatabaseConnection;
+import org.cacert.gigi.database.SQLFileManager;
+import org.cacert.gigi.database.SQLFileManager.ImportType;
+
 public class DatabaseManager {
-       public static String readFile(File f) throws IOException {
-               return new String(Files.readAllBytes(f.toPath()));
-       }
-       public static void main(String[] args) throws SQLException,
-                       ClassNotFoundException, IOException {
-               if (args.length == 0) {
-                       Properties p = new Properties();
-                       p.load(new FileReader("config/gigi.properties"));
-                       args = new String[]{p.getProperty("sql.driver"),
-                                       p.getProperty("sql.url"), p.getProperty("sql.user"),
-                                       p.getProperty("sql.password")};
-               }
-               if (args.length < 4) {
-                       System.err
-                                       .println("Usage: com.mysql.jdbc.Driver jdbc:mysql://localhost/cacert user password");
-                       return;
-               }
-               run(args);
-       }
-       public static void run(String[] args) throws ClassNotFoundException,
-                       SQLException, IOException {
-               Class.forName(args[0]);
-               Connection conn = DriverManager
-                               .getConnection(args[1], args[2], args[3]);
-               Statement stmt = conn.createStatement();
-               addFile(stmt, new File("doc/tableStructure.sql"));
-               File localData = new File("doc/sampleData.sql");
-               if (localData.exists()) {
-                       addFile(stmt, localData);
-               }
-               stmt.executeBatch();
-               stmt.close();
-       }
-       private static void addFile(Statement stmt, File f) throws IOException,
-                       SQLException {
-               String sql = readFile(f);
-               String[] stmts = sql.split(";");
-               for (String string : stmts) {
-                       if (!string.trim().equals("")) {
-                               stmt.addBatch(string);
-                       }
-               }
-       }
+
+    public static void main(String[] args) throws SQLException, ClassNotFoundException, IOException {
+        boolean test = false;
+        if (args.length >= 1 && args[0].equals("--test")) {
+            test = true;
+            String[] ne = new String[args.length - 1];
+            System.arraycopy(args, 1, ne, 0, ne.length);
+            args = ne;
+        }
+        if (args.length == 0) {
+            Properties p = new Properties();
+            try (Reader reader = new InputStreamReader(new FileInputStream("config/gigi.properties"), "UTF-8")) {
+                p.load(reader);
+            }
+            args = new String[] {
+                    p.getProperty("sql.driver"), p.getProperty("sql.url"), p.getProperty("sql.user"), p.getProperty("sql.password")
+            };
+        }
+        if (args.length < 4) {
+            System.err.println("Usage: com.mysql.jdbc.Driver jdbc:mysql://localhost/cacert user password");
+            return;
+        }
+        run(args, test ? ImportType.TEST : ImportType.PRODUCTION);
+    }
+
+    public static void run(String[] args, ImportType truncate) throws ClassNotFoundException, SQLException, IOException {
+        Class.forName(args[0]);
+        final Connection conn = DriverManager.getConnection(args[1], args[2], args[3]);
+        try {
+            conn.setAutoCommit(false);
+            Statement stmt = conn.createStatement();
+            try {
+                try (InputStream structure = DatabaseConnection.class.getResourceAsStream("tableStructure.sql")) {
+                    SQLFileManager.addFile(stmt, structure, truncate);
+                }
+                File localData = new File("doc/sampleData.sql");
+                if (localData.exists()) {
+                    try (FileInputStream f = new FileInputStream(localData)) {
+                        SQLFileManager.addFile(stmt, f, ImportType.PRODUCTION);
+                    }
+                }
+                stmt.executeBatch();
+                conn.commit();
+            } finally {
+                stmt.close();
+            }
+        } finally {
+            conn.close();
+        }
+    }
 }
index 3839e8bb5998d840777a8d8b787c6452bfaf3654..0b61dcca65763b1196678a7fa4b74daa83e10cb8 100644 (file)
@@ -1,8 +1,11 @@
 package org.cacert.gigi.util;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.util.Scanner;
@@ -12,8 +15,10 @@ import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.transform.OutputKeys;
 import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 
@@ -22,95 +27,121 @@ import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
 public class FetchLocales {
-       public static final String DOWNLOAD_SERVER = "translations.cacert.org";
-       public static final String PO_URL_TEMPLATE = "http://" + DOWNLOAD_SERVER
-                       + "/export/cacert/%/messages.po";
-       public static final String[] AUTO_LANGS = new String[]{"en", "de", "nl",
-                       "pt_BR", "fr", "sv", "it", "es", "hu", "fi", "ja", "bg", "pt",
-                       "da", "pl", "zh_CN", "ru", "lv", "cs", "zh_TW", "el", "tr", "ar"};
-       public static void main(String[] args) throws IOException,
-                       ParserConfigurationException, TransformerException {
-               File locale = new File("locale");
-               locale.mkdir();
-
-               DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-               DocumentBuilder db = dbf.newDocumentBuilder();
-               for (String lang : AUTO_LANGS) {
-                       Document doc = db.newDocument();
-                       doc.appendChild(doc.createElement("translations"));
-                       URL fetch = new URL(PO_URL_TEMPLATE.replace("%", lang));
-                       URLConnection uc = fetch.openConnection();
-                       Scanner sc = new Scanner(uc.getInputStream());
-                       String s = readLine(sc);
-                       StringBuffer contents = new StringBuffer();
-                       String id = "";
-                       while (s != null) {
-                               if (s.startsWith("msgid")) {
-                                       contents.delete(0, contents.length());
-                                       s = readString(s, sc, contents);
-                                       id = contents.toString();
-                                       continue;
-                               } else if (s.startsWith("msgstr")) {
-                                       contents.delete(0, contents.length());
-                                       // System.out.println("msgstr");
-                                       s = readString(s, sc, contents);
-                                       String msg = contents.toString().replace("\\\"", "\"")
-                                                       .replace("\\n", "\n");
-                                       insertTranslation(doc, id, msg);
-                               } else if (s.startsWith("#")) {
-                                       // System.out.println(s);
-                               } else if (s.equals("") || s.equals("\r")) {
-
-                               } else {
-                                       System.out.println("unknown line: " + s);
-                               }
-                               s = readLine(sc);
-                       }
-                       TransformerFactory tFactory = TransformerFactory.newInstance();
-                       Transformer transformer = tFactory.newTransformer();
-
-                       DOMSource source = new DOMSource(doc);
-                       FileOutputStream fos = new FileOutputStream(new File(locale, lang
-                                       + ".xml"));
-                       StreamResult result = new StreamResult(fos);
-                       transformer.setOutputProperty(OutputKeys.INDENT, "yes");
-                       transformer.setOutputProperty(
-                                       "{http://xml.apache.org/xslt}indent-amount", "2");
-                       transformer.transform(source, result);
-                       fos.close();
-               }
-       }
-       private static String readLine(Scanner sc) {
-               String line = sc.findWithinHorizon("[^\n]*\n", 0);
-               if (line == null) {
-                       return null;
-               }
-               return line.substring(0, line.length() - 1);
-       }
-       private static void insertTranslation(Document doc, String id, String msg) {
-               Node idN = doc.createTextNode(id);
-               Node textN = doc.createTextNode(msg);
-               Element tr = doc.createElement("translation");
-               Element e = doc.createElement("id");
-               e.appendChild(idN);
-               tr.appendChild(e);
-               e = doc.createElement("msg");
-               e.appendChild(textN);
-               tr.appendChild(e);
-               doc.getDocumentElement().appendChild(tr);
-       }
-       private static String readString(String head, Scanner sc,
-                       StringBuffer contents) throws IOException {
-               head = head.split(" ", 2)[1];
-               contents.append(head.substring(1, head.length() - 1));
-               String s;
-               while ((s = readLine(sc)) != null) {
-                       if (!s.startsWith("\"")) {
-                               break;
-                       }
-                       contents.append(s.substring(1, s.length() - 1));
-               }
-               return s;
-       }
+
+    public static final String DOWNLOAD_SERVER = "pootle.cacert1.dogcraft.de";
+
+    public static String PO_URL_TEMPLATE = "https://" + DOWNLOAD_SERVER + "/%/gigi/messages.po";
+
+    public static final String[] AUTO_LANGS = new String[] {
+            "de"
+    };
+
+    public static void main(String[] args) throws IOException, ParserConfigurationException, TransformerException {
+        if (args.length != 0) {
+            PO_URL_TEMPLATE = args[0];
+        }
+        System.out.println("downloading locales ...");
+        File locale = new File("locale");
+        if ( !locale.isDirectory() && !locale.mkdir()) {
+            throw new IOException("Could not create locales directory.");
+        }
+
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        DocumentBuilder db = dbf.newDocumentBuilder();
+        for (String lang : AUTO_LANGS) {
+            Document doc = db.newDocument();
+            doc.appendChild(doc.createElement("translations"));
+
+            int count = addTranslationsFromPo(doc, new URL(PO_URL_TEMPLATE.replace("%", lang)));
+
+            System.out.println("Strings for language " + lang + ": " + count);
+
+            writeTranslationToFile(doc, new File(locale, lang + ".xml"));
+        }
+        Document doc = db.newDocument();
+        doc.appendChild(doc.createElement("translations"));
+        System.out.println("Creating empty en.xml");
+        writeTranslationToFile(doc, new File(locale, "en.xml"));
+        System.out.println("Done.");
+    }
+
+    private static void writeTranslationToFile(Document doc, File file) throws TransformerFactoryConfigurationError, TransformerConfigurationException, FileNotFoundException, TransformerException, IOException {
+        TransformerFactory tFactory = TransformerFactory.newInstance();
+        Transformer transformer = tFactory.newTransformer();
+
+        DOMSource source = new DOMSource(doc);
+        FileOutputStream fos = new FileOutputStream(file);
+        StreamResult result = new StreamResult(fos);
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
+        transformer.transform(source, result);
+        fos.close();
+    }
+
+    private static int addTranslationsFromPo(Document doc, URL fetch) throws IOException, UnsupportedEncodingException {
+        URLConnection uc = fetch.openConnection();
+        Scanner sc = new Scanner(new InputStreamReader(uc.getInputStream(), "UTF-8"));
+        String s = readLine(sc);
+        StringBuffer contents = new StringBuffer();
+        String id = "";
+        int count = 0;
+        while (s != null) {
+            if (s.startsWith("msgid")) {
+                count++;
+                contents.delete(0, contents.length());
+                s = readString(s, sc, contents);
+                id = contents.toString();
+                continue;
+            } else if (s.startsWith("msgstr")) {
+                contents.delete(0, contents.length());
+                // System.out.println("msgstr");
+                s = readString(s, sc, contents);
+                String msg = contents.toString().replace("\\\"", "\"").replace("\\n", "\n");
+                insertTranslation(doc, id, msg);
+            } else if (s.startsWith("#")) {
+                // System.out.println(s);
+            } else if (s.equals("") || s.equals("\r")) {
+
+            } else {
+                System.out.println("unknown line: " + s);
+            }
+            s = readLine(sc);
+        }
+        return count;
+    }
+
+    private static String readLine(Scanner sc) {
+        String line = sc.findWithinHorizon("[^\n]*\n", 0);
+        if (line == null) {
+            return null;
+        }
+        return line.substring(0, line.length() - 1);
+    }
+
+    private static void insertTranslation(Document doc, String id, String msg) {
+        Node idN = doc.createTextNode(id);
+        Node textN = doc.createTextNode(msg);
+        Element tr = doc.createElement("translation");
+        Element e = doc.createElement("id");
+        e.appendChild(idN);
+        tr.appendChild(e);
+        e = doc.createElement("msg");
+        e.appendChild(textN);
+        tr.appendChild(e);
+        doc.getDocumentElement().appendChild(tr);
+    }
+
+    private static String readString(String head, Scanner sc, StringBuffer contents) throws IOException {
+        head = head.split(" ", 2)[1];
+        contents.append(head.substring(1, head.length() - 1));
+        String s;
+        while ((s = readLine(sc)) != null) {
+            if ( !s.startsWith("\"")) {
+                break;
+            }
+            contents.append(s.substring(1, s.length() - 1));
+        }
+        return s;
+    }
 
 }