From 4ad1ebce249519369d24b4ea7da1da62a685627a Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 23 Nov 2012 12:01:47 -0500 Subject: [PATCH] Mostly functional Gitblit Certificate Authority tool --- build.xml | 216 +++++++---- docs/04_releases.mkd | 3 +- resources/bullet_delete.png | Bin 0 -> 333 bytes resources/bullet_key.png | Bin 0 -> 449 bytes resources/rosette_16x16.png | Bin 0 -> 646 bytes resources/vcard_16x16.png | Bin 0 -> 642 bytes .../gitblit/authority/CertificateStatus.java | 20 ++ .../authority/CertificateStatusRenderer.java | 82 +++++ .../authority/CertificatesTableModel.java | 166 +++++++++ .../gitblit/authority/GitblitAuthority.java | 2 +- .../authority/GitblitAuthorityLauncher.java | 113 ++++++ .../authority/NewClientCertificateDialog.java | 164 +++++++++ .../authority/UserCertificatePanel.java | 334 ++++++++++++++++++ .../authority/UserCertificateTableModel.java | 131 +++++++ src/com/gitblit/authority/Utils.java | 101 ++++++ .../authority/X509CertificateViewer.java | 129 +++++++ src/com/gitblit/client/DateCellRenderer.java | 10 +- src/com/gitblit/utils/StringUtils.java | 18 +- src/com/gitblit/utils/TimeUtils.java | 16 + .../gitblit/wicket/GitBlitWebApp.properties | 41 +++ 20 files changed, 1470 insertions(+), 76 deletions(-) create mode 100644 resources/bullet_delete.png create mode 100644 resources/bullet_key.png create mode 100644 resources/rosette_16x16.png create mode 100644 resources/vcard_16x16.png create mode 100644 src/com/gitblit/authority/CertificateStatus.java create mode 100644 src/com/gitblit/authority/CertificateStatusRenderer.java create mode 100644 src/com/gitblit/authority/CertificatesTableModel.java create mode 100644 src/com/gitblit/authority/GitblitAuthorityLauncher.java create mode 100644 src/com/gitblit/authority/NewClientCertificateDialog.java create mode 100644 src/com/gitblit/authority/UserCertificatePanel.java create mode 100644 src/com/gitblit/authority/UserCertificateTableModel.java create mode 100644 src/com/gitblit/authority/Utils.java create mode 100644 src/com/gitblit/authority/X509CertificateViewer.java diff --git a/build.xml b/build.xml index 7c5e8cc5..8a7fd54b 100644 --- a/build.xml +++ b/build.xml @@ -92,6 +92,7 @@ + @@ -117,6 +118,7 @@ + @@ -200,12 +202,12 @@ - + - + @@ -460,6 +462,7 @@ + @@ -487,6 +490,7 @@ + @@ -668,6 +672,7 @@ + @@ -745,73 +750,138 @@ + + + + Building Gitblit Authority ${gb.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - Building Gitblit API Library ${gb.version} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Build the Gitblit API client library + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + --> + + Building Gitblit API Library ${gb.version} + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1046,6 +1116,16 @@ summary="Gitblit Manager v${gb.version} (Swing tool to remotely administer a Gitblit server)" labels="Featured, Type-Package, OpSys-All" /> + + + `FpzH{j04!3oJaW zCNB{G_xJbog(|w|KKy@M+*W^Hz9~~cUd3;U_`mroZ^$9+> z5u{`w_GH@*sc)VI8`zsu9Pd?peEd9W{@v%@4X@+{lNmZDF((D8JiPAkha=YZaYkGM c1BU^FXGHpWo(SJZK(8}+y85}Sb4q9e02}X(tpET3 literal 0 HcmV?d00001 diff --git a/resources/bullet_key.png b/resources/bullet_key.png new file mode 100644 index 0000000000000000000000000000000000000000..62600135097178647456c89e2a62302e9f06b782 GIT binary patch literal 449 zcmV;y0Y3hTP)f zaAun6ee*=#~D~zSs0jESQy@X`m@%*?s5@O&m*iZxG>x5zofJ<13xbt!@Ea!8SY=c z!m#no_wY$;AKZm1c@4ymaC+g%i;suCeEDj3@AeahkKh08_Ncg62^78t#NTm+6}lHx rfD}KJ`yPnzVbg@7XXs}pfB*vkhzW_j1;lht00000NkvXXu0mjfK|slC literal 0 HcmV?d00001 diff --git a/resources/rosette_16x16.png b/resources/rosette_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..6dedc271f3faceb070bde8e125d000a4c2dda6ee GIT binary patch literal 646 zcmV;10(t$3P)QSfNT6Qr&8s&L9uz!#@E_2dLXld`$%Dn4;Ln3WEVP#hNlHwl*lg2o zKHuA9L(|3&2HwuRc{AU9vz9RimSy4mc?}#G;FB<;OKU(DBaU@mfQcpmb}#4ivzh7X z(AU`C$M0%&HwMhEg?j%HV6x5R;AXLiiChkyRx9*7sT2-tHGJ6I1Vv&L=?)YmptB9! zFWI*FI5%hhY;2e>lauB>!!lv{6cb0>DBq623*e0vi(!6c1(i|>L8AdL9uG}eKE=cl z_kzGW@HU;zqSI``_I)IhNhDmiXT77tmv|`>o`5@ccGX#;$9HA2iyo)>1Vl&v8gGzOh~e-Ojtg}#1VJY zGv;0aKiGO|f$y)oJN*~xFzz28<16=p%+@Vnp6=R^0C!;B<6|852a{my>GAPUpm}rz zpY5L#Ptg7Xx48>1hpxcQcz%6#6*FwPz{Jy&6YRBGHCkP`;CYy#s5s&d&0q^yypm44 z9y@U;p!PGo_a5-{8}Lv#sbOj_$Pt%03xiGMW|iB^%jTd^FcofHJ)bu_j-vr&XqVo( zt}kCDlQ9i)#AVL+!3?%7%V@bxG+9~|cy<@KqwmMSgKNN}vvIp;@o0!kBmPus;8!23 gldB^*pZ^Ik00a}{`D_ykmH+?%07*qoM6N<$g0d4RZU6uP literal 0 HcmV?d00001 diff --git a/resources/vcard_16x16.png b/resources/vcard_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..b8a9fea4cfd7eb7fe7f98032eeae955608692162 GIT binary patch literal 642 zcmV-|0)737P)LnAlbF}f{C64AD50RDWx z6iZlI`-V3jqN3{EjT`~aFnQ}H4z?e05o}`6cP3rC6#e7#|D3f=uD0YXFZPYiYvJ4| z-+D)9VG0A5t&qKzbaTFZvC%o0+*y-Yyk1!o#EuQzUHDm+dYjXV$y6aToJb^Zzj&w{ z8hfOyG{~vYge+vPBonEL**Ocm#W}WtvrQ*<{B@9{&Ij@~^6-{-Qzp-mA9eP6ImzmM c&VK|L01%Kt#mW9DMgRZ+07*qoM6N<$f_qsSYybcN literal 0 HcmV?d00001 diff --git a/src/com/gitblit/authority/CertificateStatus.java b/src/com/gitblit/authority/CertificateStatus.java new file mode 100644 index 00000000..79c51628 --- /dev/null +++ b/src/com/gitblit/authority/CertificateStatus.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +public enum CertificateStatus { + unknown, ok, expiring, expired, revoked +} diff --git a/src/com/gitblit/authority/CertificateStatusRenderer.java b/src/com/gitblit/authority/CertificateStatusRenderer.java new file mode 100644 index 00000000..7a708ea4 --- /dev/null +++ b/src/com/gitblit/authority/CertificateStatusRenderer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.awt.Component; + +import javax.swing.ImageIcon; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +import com.gitblit.client.Translation; + +/** + * Displays a subscribed icon on the left of the repository name, if there is at + * least one subscribed branch. + * + * @author James Moger + * + */ +public class CertificateStatusRenderer extends DefaultTableCellRenderer { + + private static final long serialVersionUID = 1L; + + private final ImageIcon unknownIcon; + private final ImageIcon revokedIcon; + private final ImageIcon expiredIcon; + private final ImageIcon expiringIcon; + private final ImageIcon okIcon; + + public CertificateStatusRenderer() { + super(); + unknownIcon = new ImageIcon(getClass().getResource("/bullet_white.png")); + revokedIcon = new ImageIcon(getClass().getResource("/bullet_delete.png")); + expiredIcon = new ImageIcon(getClass().getResource("/bullet_red.png")); + expiringIcon = new ImageIcon(getClass().getResource("/bullet_orange.png")); + okIcon = new ImageIcon(getClass().getResource("/bullet_green.png")); + } + + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (value instanceof CertificateStatus) { + CertificateStatus status = (CertificateStatus) value; + switch(status) { + case revoked: + setText(Translation.get("gb.revoked")); + setIcon(revokedIcon); + break; + case expiring: + setText(Translation.get("gb.expiring")); + setIcon(expiringIcon); + break; + case expired: + setText(Translation.get("gb.expired")); + setIcon(expiredIcon); + break; + case unknown: + setText(""); + setIcon(unknownIcon); + break; + default: + setText(Translation.get("gb.ok")); + setIcon(okIcon); + break; + } + } + return this; + } +} diff --git a/src/com/gitblit/authority/CertificatesTableModel.java b/src/com/gitblit/authority/CertificatesTableModel.java new file mode 100644 index 00000000..44d80e3a --- /dev/null +++ b/src/com/gitblit/authority/CertificatesTableModel.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; + +import javax.swing.table.AbstractTableModel; + +import com.gitblit.client.Translation; +import com.gitblit.utils.X509Utils.RevocationReason; + +/** + * Table model of a list of user certificate models. + * + * @author James Moger + * + */ +public class CertificatesTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + + UserCertificateModel ucm; + + enum Columns { + SerialNumber, Status, Reason, Issued, Expires; + + @Override + public String toString() { + return name().replace('_', ' '); + } + } + + public CertificatesTableModel() { + } + + @Override + public int getRowCount() { + return ucm == null || ucm.certs == null ? 0 : ucm.certs.size(); + } + + @Override + public int getColumnCount() { + return Columns.values().length; + } + + @Override + public String getColumnName(int column) { + Columns col = Columns.values()[column]; + switch (col) { + case SerialNumber: + return Translation.get("gb.serialNumber"); + case Issued: + return Translation.get("gb.issued"); + case Expires: + return Translation.get("gb.expires"); + case Status: + return Translation.get("gb.status"); + case Reason: + return Translation.get("gb.reason"); + } + return ""; + } + + /** + * Returns Object.class regardless of columnIndex. + * + * @param columnIndex + * the column being queried + * @return the Object.class + */ + public Class getColumnClass(int columnIndex) { + Columns col = Columns.values()[columnIndex]; + switch (col) { + case Status: + return CertificateStatus.class; + case Issued: + return Date.class; + case Expires: + return Date.class; + case SerialNumber: + return BigInteger.class; + default: + return String.class; + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + Columns col = Columns.values()[columnIndex]; + switch (col) { + default: + return false; + } + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + X509Certificate cert = ucm.certs.get(rowIndex); + Columns col = Columns.values()[columnIndex]; + switch (col) { + case Status: + return ucm.getStatus(cert); + case SerialNumber: + return cert.getSerialNumber(); + case Issued: + return cert.getNotBefore(); + case Expires: + return cert.getNotAfter(); + case Reason: + if (ucm.getStatus(cert).equals(CertificateStatus.revoked)) { + RevocationReason r = ucm.getRevocationReason(cert.getSerialNumber()); + return Translation.get("gb." + r.name()); + } + } + return null; + } + + public X509Certificate get(int modelRow) { + return ucm.certs.get(modelRow); + } + + public void setUserCertificateModel(UserCertificateModel ucm) { + this.ucm = ucm; + Collections.sort(ucm.certs, new Comparator() { + @Override + public int compare(X509Certificate o1, X509Certificate o2) { + // sort by issue date in reverse chronological order + int result = o2.getNotBefore().compareTo(o1.getNotBefore()); + if (result == 0) { + // same issue date, show expiring first + boolean r1 = CertificatesTableModel.this.ucm.isRevoked(o1.getSerialNumber()); + boolean r2 = CertificatesTableModel.this.ucm.isRevoked(o2.getSerialNumber()); + if ((r1 && r2) || (!r1 && !r2)) { + // both revoked or both not revoked + // chronlogical order by expiration dates + result = o1.getNotAfter().compareTo(o2.getNotAfter()); + } else if (r1) { + // r1 is revoked, r2 first + return 1; + } else { + // r2 is revoked, r1 first + return -1; + } + } + return result; + } + }); + } +} diff --git a/src/com/gitblit/authority/GitblitAuthority.java b/src/com/gitblit/authority/GitblitAuthority.java index ff48ecf9..84162000 100644 --- a/src/com/gitblit/authority/GitblitAuthority.java +++ b/src/com/gitblit/authority/GitblitAuthority.java @@ -136,7 +136,7 @@ public class GitblitAuthority extends JFrame { public void initialize() { setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage()); - setTitle("Gitblit PKI Authority v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")"); + setTitle("Gitblit Certificate Authority v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")"); setContentPane(getUI()); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); addWindowListener(new WindowAdapter() { diff --git a/src/com/gitblit/authority/GitblitAuthorityLauncher.java b/src/com/gitblit/authority/GitblitAuthorityLauncher.java new file mode 100644 index 00000000..584ac01f --- /dev/null +++ b/src/com/gitblit/authority/GitblitAuthorityLauncher.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.SplashScreen; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import com.gitblit.Constants; +import com.gitblit.Launcher; +import com.gitblit.build.Build; +import com.gitblit.build.Build.DownloadListener; +import com.gitblit.client.Translation; + +/** + * Downloads dependencies and launches Gitblit Authority. + * + * @author James Moger + * + */ +public class GitblitAuthorityLauncher { + + public static void main(String[] args) { + final SplashScreen splash = SplashScreen.getSplashScreen(); + + DownloadListener downloadListener = new DownloadListener() { + @Override + public void downloading(String name) { + updateSplash(splash, Translation.get("gb.downloading") + " " + name); + } + }; + + // download authority runtime dependencies + Build.authority(downloadListener); + + File libFolder = new File("ext"); + List jars = Launcher.findJars(libFolder.getAbsoluteFile()); + + // sort the jars by name and then reverse the order so the newer version + // of the library gets loaded in the event that this is an upgrade + Collections.sort(jars); + Collections.reverse(jars); + for (File jar : jars) { + try { + updateSplash(splash, Translation.get("gb.loading") + " " + jar.getName() + "..."); + Launcher.addJarFile(jar); + } catch (IOException e) { + + } + } + + updateSplash(splash, Translation.get("gb.starting") + " Gitblit Authority..."); + GitblitAuthority.main(args); + } + + private static void updateSplash(final SplashScreen splash, final String string) { + if (splash == null) { + return; + } + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + Graphics2D g = splash.createGraphics(); + if (g != null) { + // Splash is 320x120 + FontMetrics fm = g.getFontMetrics(); + + // paint startup status + g.setColor(Color.darkGray); + int h = fm.getHeight() + fm.getMaxDescent(); + int x = 5; + int y = 115; + int w = 320 - 2 * x; + g.fillRect(x, y - h, w, h); + g.setColor(Color.lightGray); + g.drawRect(x, y - h, w, h); + g.setColor(Color.WHITE); + int xw = fm.stringWidth(string); + g.drawString(string, x + ((w - xw) / 2), y - 5); + + // paint version + String ver = "v" + Constants.VERSION; + int vw = g.getFontMetrics().stringWidth(ver); + g.drawString(ver, 320 - vw - 5, 34); + g.dispose(); + splash.update(); + } + } + }); + } catch (Throwable t) { + t.printStackTrace(); + } + } +} diff --git a/src/com/gitblit/authority/NewClientCertificateDialog.java b/src/com/gitblit/authority/NewClientCertificateDialog.java new file mode 100644 index 00000000..ad4fe9aa --- /dev/null +++ b/src/com/gitblit/authority/NewClientCertificateDialog.java @@ -0,0 +1,164 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.awt.BorderLayout; +import java.awt.Frame; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Date; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import org.bouncycastle.util.Arrays; + +import com.gitblit.client.HeaderPanel; +import com.gitblit.client.Translation; +import com.gitblit.utils.StringUtils; +import com.toedter.calendar.JDateChooser; + +public class NewClientCertificateDialog extends JDialog { + + private static final long serialVersionUID = 1L; + + JDateChooser expirationDate; + JPasswordField pw1; + JPasswordField pw2; + JTextField hint; + JCheckBox sendEmail; + boolean isCanceled = true; + + public NewClientCertificateDialog(Frame owner, String displayname, Date defaultExpiration) { + super(owner); + + setTitle(Translation.get("gb.newCertificate")); + + JPanel content = new JPanel(new BorderLayout(5, 5)) { + private static final long serialVersionUID = 1L; + + @Override + public Insets getInsets() { + + return Utils.INSETS; + } + }; + content.add(new HeaderPanel(Translation.get("gb.newCertificate") + " (" + displayname + ")", "rosette_16x16.png"), BorderLayout.NORTH); + + expirationDate = new JDateChooser(defaultExpiration); + pw1 = new JPasswordField(20); + pw2 = new JPasswordField(20); + hint = new JTextField(20); + sendEmail = new JCheckBox(Translation.get("gb.sendEmail")); + + JPanel panel = new JPanel(new GridLayout(0, 2, 5, 5)); + + panel.add(new JLabel(Translation.get("gb.expires"))); + panel.add(expirationDate); + + panel.add(new JLabel(Translation.get("gb.password"))); + panel.add(pw1); + + panel.add(new JLabel(Translation.get("gb.confirmPassword"))); + panel.add(pw2); + + panel.add(new JLabel(Translation.get("gb.passwordHint"))); + panel.add(hint); + + panel.add(new JLabel("")); + panel.add(sendEmail); + + content.add(panel, BorderLayout.CENTER); + + JButton ok = new JButton(Translation.get("gb.ok")); + ok.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (validateInputs()) { + isCanceled = false; + setVisible(false); + } + } + }); + JButton cancel = new JButton(Translation.get("gb.cancel")); + cancel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + isCanceled = true; + setVisible(false); + } + }); + + JPanel controls = new JPanel(); + controls.add(ok); + controls.add(cancel); + + content.add(controls, BorderLayout.SOUTH); + + getContentPane().add(content, BorderLayout.CENTER); + pack(); + + setLocationRelativeTo(owner); + } + + private boolean validateInputs() { + if (getExpiration().getTime() < System.currentTimeMillis()) { + // expires before now + JOptionPane.showMessageDialog(this, Translation.get("gb.invalidExpiraitonDate"), + Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE); + return false; + } + if (pw1.getPassword().length == 0 || !Arrays.areEqual(pw1.getPassword(), pw2.getPassword())) { + // password mismatch + JOptionPane.showMessageDialog(this, Translation.get("gb.passwordsDoNotMatch"), + Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE); + return false; + } + if (StringUtils.isEmpty(getPasswordHint())) { + // must have hint + JOptionPane.showMessageDialog(this, Translation.get("gb.passwordHintRequired"), + Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE); + return false; + } + return true; + } + + public String getPassword() { + return new String(pw1.getPassword()); + } + + public String getPasswordHint() { + return hint.getText(); + } + + public Date getExpiration() { + return expirationDate.getDate(); + } + + public boolean sendEmail() { + return sendEmail.isSelected(); + } + + public boolean isCanceled() { + return isCanceled; + } +} diff --git a/src/com/gitblit/authority/UserCertificatePanel.java b/src/com/gitblit/authority/UserCertificatePanel.java new file mode 100644 index 00000000..20727f64 --- /dev/null +++ b/src/com/gitblit/authority/UserCertificatePanel.java @@ -0,0 +1,334 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.security.cert.X509Certificate; +import java.util.Date; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableRowSorter; + +import com.gitblit.client.HeaderPanel; +import com.gitblit.client.Translation; +import com.gitblit.models.UserModel; +import com.gitblit.utils.X509Utils.RevocationReason; +import com.gitblit.utils.X509Utils.X509Metadata; + +public abstract class UserCertificatePanel extends JPanel { + + private static final long serialVersionUID = 1L; + + private Frame owner; + + private UserCertificateModel ucm; + + private JTextField displayname; + private JTextField username; + private JTextField emailAddress; + private JTextField organizationalUnit; + private JTextField organization; + private JTextField locality; + private JTextField stateProvince; + private JTextField countryCode; + + private CertificatesTableModel tableModel; + + private JButton saveUserButton; + + private JButton editUserButton; + + private JButton newCertificateButton; + + private JButton revokeCertificateButton; + + private JTable table; + + public UserCertificatePanel(Frame owner) { + super(new BorderLayout()); + + this.owner = owner; + + displayname = new JTextField(20); + username = new JTextField(20); + username.setEditable(false); + emailAddress = new JTextField(20); + organizationalUnit = new JTextField(20); + organization = new JTextField(20); + locality = new JTextField(20); + stateProvince = new JTextField(20); + countryCode = new JTextField(20); + + JPanel fields = new JPanel(new GridLayout(0, 1, 5, 5)); + fields.add(newFieldPanel(Translation.get("gb.displayName"), displayname)); + fields.add(newFieldPanel(Translation.get("gb.username") + " (CN)", username)); + fields.add(newFieldPanel(Translation.get("gb.emailAddress") + " (E)", emailAddress)); + fields.add(newFieldPanel(Translation.get("gb.organizationalUnit") + " (OU)", organizationalUnit)); + fields.add(newFieldPanel(Translation.get("gb.organization") + " (O)", organization)); + fields.add(newFieldPanel(Translation.get("gb.locality") + " (L)", locality)); + fields.add(newFieldPanel(Translation.get("gb.stateProvince") + " (ST)", stateProvince)); + fields.add(newFieldPanel(Translation.get("gb.countryCode") + " (C)", countryCode)); + + JPanel fp = new JPanel(new BorderLayout(5, 5)); + fp.add(fields, BorderLayout.NORTH); + + JPanel fieldsPanel = new JPanel(new BorderLayout()); + fieldsPanel.add(new HeaderPanel(Translation.get("gb.properties"), "vcard_16x16.png"), BorderLayout.NORTH); + fieldsPanel.add(fp, BorderLayout.CENTER); + + saveUserButton = new JButton(Translation.get("gb.save")); + saveUserButton.setEnabled(false); + saveUserButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setEditable(false); + String username = ucm.user.username; + updateUser(); + saveUser(username, ucm); + } + }); + + editUserButton = new JButton(Translation.get("gb.edit")); + editUserButton.setEnabled(false); + editUserButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setEditable(true); + } + }); + + JPanel userControls = new JPanel(new FlowLayout(FlowLayout.LEFT)); + userControls.add(editUserButton); + userControls.add(saveUserButton); + fieldsPanel.add(userControls, BorderLayout.SOUTH); + + JPanel certificatesPanel = new JPanel(new BorderLayout()); + certificatesPanel.add(new HeaderPanel(Translation.get("gb.certificates"), "rosette_16x16.png"), BorderLayout.NORTH); + tableModel = new CertificatesTableModel(); + table = Utils.newTable(tableModel, Utils.DATE_FORMAT); + table.setRowSorter(new TableRowSorter(tableModel)); + table.setDefaultRenderer(CertificateStatus.class, new CertificateStatusRenderer()); + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) { + return; + } + boolean enable = false; + int row = table.getSelectedRow(); + if (row > -1) { + int modelIndex = table.convertRowIndexToModel(row); + X509Certificate cert = tableModel.get(modelIndex); + enable = !ucm.isRevoked(cert.getSerialNumber()); + } + revokeCertificateButton.setEnabled(enable); + } + }); + table.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + int row = table.rowAtPoint(e.getPoint()); + int modelIndex = table.convertRowIndexToModel(row); + X509Certificate cert = tableModel.get(modelIndex); + X509CertificateViewer viewer = new X509CertificateViewer(UserCertificatePanel.this.owner, cert); + viewer.setVisible(true); + } + } + }); + certificatesPanel.add(new JScrollPane(table), BorderLayout.CENTER); + + newCertificateButton = new JButton(Translation.get("gb.newCertificate")); + newCertificateButton.setEnabled(false); + newCertificateButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + try { + if (saveUserButton.isEnabled()) { + // save changes + String username = ucm.user.username; + setEditable(false); + updateUser(); + saveUser(username, ucm); + } + + NewClientCertificateDialog dialog = new NewClientCertificateDialog(UserCertificatePanel.this.owner, + ucm.user.getDisplayName(), getDefaultExpiration()); + dialog.setModal(true); + dialog.setVisible(true); + if (dialog.isCanceled()) { + return; + } + + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + UserModel user = ucm.user; + X509Metadata metadata = new X509Metadata(user.username, dialog.getPassword()); + metadata.userDisplayname = user.getDisplayName(); + metadata.emailAddress = user.emailAddress; + metadata.passwordHint = dialog.getPasswordHint(); + metadata.notAfter = dialog.getExpiration(); + + newCertificate(ucm, metadata, dialog.sendEmail()); + } catch (Exception x) { + Utils.showException(UserCertificatePanel.this, x); + } finally { + setCursor(Cursor.getDefaultCursor()); + } + } + }); + + revokeCertificateButton = new JButton(Translation.get("gb.revokeCertificate")); + revokeCertificateButton.setEnabled(false); + revokeCertificateButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + try { + int row = table.getSelectedRow(); + if (row < 0) { + return; + } + int modelIndex = table.convertRowIndexToModel(row); + X509Certificate cert = tableModel.get(modelIndex); + + String [] choices = new String[RevocationReason.reasons.length]; + for (int i = 0; i < choices.length; i++) { + choices[i] = Translation.get("gb." + RevocationReason.reasons[i].name()); + } + + Object choice = JOptionPane.showInputDialog(UserCertificatePanel.this.owner, + Translation.get("gb.revokeCertificateReason"), Translation.get("gb.revokeCertificate"), + JOptionPane.PLAIN_MESSAGE, new ImageIcon(getClass().getResource("/rosette_16x16.png")), choices, Translation.get("gb.unspecified")); + if (choice == null) { + return; + } + RevocationReason reason = RevocationReason.unspecified; + for (int i = 0 ; i < choices.length; i++) { + if (choices[i].equals(choice)) { + reason = RevocationReason.reasons[i]; + break; + } + } + if (!ucm.isRevoked(cert.getSerialNumber())) { + if (ucm.certs.size() == 1) { + // no other certificates + ucm.expires = null; + } else { + // determine new expires date for user + Date newExpires = null; + for (X509Certificate c : ucm.certs) { + if (!c.equals(cert)) { + if (!ucm.isRevoked(c.getSerialNumber())) { + if (newExpires == null || c.getNotAfter().after(newExpires)) { + newExpires = c.getNotAfter(); + } + } + } + } + ucm.expires = newExpires; + } + revoke(ucm, cert, reason); + } + } catch (Exception x) { + Utils.showException(UserCertificatePanel.this, x); + } finally { + setCursor(Cursor.getDefaultCursor()); + } + } + }); + + JPanel certificateControls = new JPanel(new FlowLayout(FlowLayout.LEFT)); + certificateControls.add(newCertificateButton); + certificateControls.add(revokeCertificateButton); + certificatesPanel.add(certificateControls, BorderLayout.SOUTH); + + add(fieldsPanel, BorderLayout.NORTH); + add(certificatesPanel, BorderLayout.CENTER); + setEditable(false); + } + + private JPanel newFieldPanel(String label, Component c) { + JLabel jlabel = new JLabel(label); + jlabel.setPreferredSize(new Dimension(150, 20)); + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + panel.add(jlabel); + panel.add(c); + return panel; + } + + public void setUserCertificateModel(UserCertificateModel ucm) { + this.ucm = ucm; + setEditable(false); + displayname.setText(ucm.user.getDisplayName()); + username.setText(ucm.user.username); + emailAddress.setText(ucm.user.emailAddress); + organizationalUnit.setText(ucm.user.organizationalUnit); + organization.setText(ucm.user.organization); + locality.setText(ucm.user.locality); + stateProvince.setText(ucm.user.stateProvince); + countryCode.setText(ucm.user.countryCode); + + tableModel.setUserCertificateModel(ucm); + tableModel.fireTableDataChanged(); + } + + public void setEditable(boolean editable) { + displayname.setEditable(editable); +// username.setEditable(editable); + emailAddress.setEditable(editable); + organizationalUnit.setEditable(editable); + organization.setEditable(editable); + locality.setEditable(editable); + stateProvince.setEditable(editable); + countryCode.setEditable(editable); + + editUserButton.setEnabled(!editable && ucm != null); + saveUserButton.setEnabled(editable && ucm != null); + + newCertificateButton.setEnabled(ucm != null); + revokeCertificateButton.setEnabled(false); + } + + private void updateUser() { + ucm.user.displayName = displayname.getText(); + ucm.user.username = username.getText(); + ucm.user.emailAddress = emailAddress.getText(); + ucm.user.organizationalUnit = organizationalUnit.getText(); + ucm.user.organization = organization.getText(); + ucm.user.locality = locality.getText(); + ucm.user.stateProvince = stateProvince.getText(); + ucm.user.countryCode = countryCode.getText(); + } + + public abstract Date getDefaultExpiration(); + + public abstract void saveUser(String username, UserCertificateModel ucm); + public abstract void newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail); + public abstract void revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason); +} diff --git a/src/com/gitblit/authority/UserCertificateTableModel.java b/src/com/gitblit/authority/UserCertificateTableModel.java new file mode 100644 index 00000000..dde73fc0 --- /dev/null +++ b/src/com/gitblit/authority/UserCertificateTableModel.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import javax.swing.table.AbstractTableModel; + +import com.gitblit.client.Translation; + +/** + * Table model of a list of user certificate models. + * + * @author James Moger + * + */ +public class UserCertificateTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + + List list; + + enum Columns { + Username, DisplayName, Status, Expires; + + @Override + public String toString() { + return name().replace('_', ' '); + } + } + + public UserCertificateTableModel() { + this(new ArrayList()); + } + + public UserCertificateTableModel(List list) { + this.list = list; + Collections.sort(this.list); + } + + @Override + public int getRowCount() { + return list.size(); + } + + @Override + public int getColumnCount() { + return Columns.values().length; + } + + @Override + public String getColumnName(int column) { + Columns col = Columns.values()[column]; + switch (col) { + case Username: + return Translation.get("gb.username"); + case DisplayName: + return Translation.get("gb.displayName"); + case Expires: + return Translation.get("gb.expires"); + case Status: + return Translation.get("gb.status"); + } + return ""; + } + + /** + * Returns Object.class regardless of columnIndex. + * + * @param columnIndex + * the column being queried + * @return the Object.class + */ + public Class getColumnClass(int columnIndex) { + Columns col = Columns.values()[columnIndex]; + switch (col) { + case Expires: + return Date.class; + case Status: + return CertificateStatus.class; + default: + return String.class; + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + Columns col = Columns.values()[columnIndex]; + switch (col) { + default: + return false; + } + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + UserCertificateModel model = list.get(rowIndex); + Columns col = Columns.values()[columnIndex]; + switch (col) { + case Username: + return model.user.username; + case DisplayName: + return model.user.getDisplayName(); + case Expires: + return model.expires; + case Status: + return model.getStatus(); + } + return null; + } + + public UserCertificateModel get(int modelRow) { + return list.get(modelRow); + } +} diff --git a/src/com/gitblit/authority/Utils.java b/src/com/gitblit/authority/Utils.java new file mode 100644 index 00000000..5c824934 --- /dev/null +++ b/src/com/gitblit/authority/Utils.java @@ -0,0 +1,101 @@ +package com.gitblit.authority; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Insets; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Date; + +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.table.DefaultTableColumnModel; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableModel; + +import com.gitblit.client.DateCellRenderer; +import com.gitblit.client.Translation; + +public class Utils { + + public final static int MARGIN = 5; + + public final static Insets INSETS = new Insets(MARGIN, MARGIN, MARGIN, MARGIN); + + public final static String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm"; + + public final static String DATE_FORMAT = "yyyy-MM-dd"; + + public static JTable newTable(TableModel model, String datePattern) { + JTable table = new JTable(model); + table.setRowHeight(table.getFont().getSize() + 8); + table.setCellSelectionEnabled(false); + table.setRowSelectionAllowed(true); + table.getTableHeader().setReorderingAllowed(false); + table.setGridColor(new Color(0xd9d9d9)); + table.setBackground(Color.white); + table.setDefaultRenderer(Date.class, + new DateCellRenderer(datePattern, Color.orange.darker())); + return table; + } + + public static void showException(Component c, Throwable t) { + StringWriter writer = new StringWriter(); + t.printStackTrace(new PrintWriter(writer)); + String stacktrace = writer.toString(); + try { + writer.close(); + } catch (Throwable x) { + } + JTextArea textArea = new JTextArea(stacktrace); + textArea.setFont(new Font("monospaced", Font.PLAIN, 11)); + JScrollPane jsp = new JScrollPane(textArea); + jsp.setPreferredSize(new Dimension(800, 400)); + JOptionPane.showMessageDialog(c, jsp, Translation.get("gb.error"), + JOptionPane.ERROR_MESSAGE); + } + + public static void packColumns(JTable table, int margin) { + for (int c = 0; c < table.getColumnCount(); c++) { + packColumn(table, c, 4); + } + } + + // Sets the preferred width of the visible column specified by vColIndex. + // The column will be just wide enough to show the column head and the + // widest cell in the column. margin pixels are added to the left and right + // (resulting in an additional width of 2*margin pixels). + private static void packColumn(JTable table, int vColIndex, int margin) { + DefaultTableColumnModel colModel = (DefaultTableColumnModel) table.getColumnModel(); + TableColumn col = colModel.getColumn(vColIndex); + int width = 0; + + // Get width of column header + TableCellRenderer renderer = col.getHeaderRenderer(); + if (renderer == null) { + renderer = table.getTableHeader().getDefaultRenderer(); + } + Component comp = renderer.getTableCellRendererComponent(table, col.getHeaderValue(), false, + false, 0, 0); + width = comp.getPreferredSize().width; + + // Get maximum width of column data + for (int r = 0; r < table.getRowCount(); r++) { + renderer = table.getCellRenderer(r, vColIndex); + comp = renderer.getTableCellRendererComponent(table, table.getValueAt(r, vColIndex), + false, false, r, vColIndex); + width = Math.max(width, comp.getPreferredSize().width); + } + + // Add margin + width += 2 * margin; + + // Set the width + col.setPreferredWidth(width); + } +} diff --git a/src/com/gitblit/authority/X509CertificateViewer.java b/src/com/gitblit/authority/X509CertificateViewer.java new file mode 100644 index 00000000..d29c6e7d --- /dev/null +++ b/src/com/gitblit/authority/X509CertificateViewer.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.authority; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.text.DateFormat; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingConstants; + +import com.gitblit.client.HeaderPanel; +import com.gitblit.client.Translation; +import com.gitblit.utils.StringUtils; + +public class X509CertificateViewer extends JDialog { + + private static final long serialVersionUID = 1L; + + public X509CertificateViewer(Frame owner, X509Certificate cert) { + super(owner); + + setTitle(Translation.get("gb.viewCertificate")); + + JPanel content = new JPanel(new BorderLayout(5, 5)) { + private static final long serialVersionUID = 1L; + + @Override + public Insets getInsets() { + + return Utils.INSETS; + } + }; + content.add(new HeaderPanel("certificiate", "rosette_16x16.png"), BorderLayout.NORTH); + + DateFormat df = DateFormat.getDateTimeInstance(); + + int l1 = 15; + int l2 = 25; + int l3 = 45; + JPanel panel = new JPanel(new GridLayout(0, 1, 0, 10)); + panel.add(newField(Translation.get("gb.version"), "" + cert.getVersion(), 3)); + panel.add(newField(Translation.get("gb.subject"), cert.getSubjectDN().getName(), l3)); + panel.add(newField(Translation.get("gb.issuer"), cert.getIssuerDN().getName(), l3)); + panel.add(newField(Translation.get("gb.serialNumber"), "0x" + cert.getSerialNumber().toString(16), l2)); + panel.add(newField(Translation.get("gb.serialNumber"), cert.getSerialNumber().toString(), l2)); + panel.add(newField(Translation.get("gb.validFrom"), df.format(cert.getNotBefore()), l2)); + panel.add(newField(Translation.get("gb.validUntil"), df.format(cert.getNotAfter()), l2)); + panel.add(newField(Translation.get("gb.publicKey"), cert.getPublicKey().getAlgorithm(), l1)); + panel.add(newField(Translation.get("gb.signatureAlgorithm"), cert.getSigAlgName(), l1)); + try { + panel.add(newField(Translation.get("gb.sha1FingerPrint"), fingerprint(StringUtils.getSHA1(cert.getEncoded())), l3)); + } catch (CertificateEncodingException e1) { + } + try { + panel.add(newField(Translation.get("gb.md5FingerPrint"), fingerprint(StringUtils.getMD5(cert.getEncoded())), l3)); + } catch (CertificateEncodingException e1) { + } + + content.add(panel, BorderLayout.CENTER); + + JButton ok = new JButton(Translation.get("gb.ok")); + ok.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setVisible(false); + } + }); + + JPanel controls = new JPanel(); + controls.add(ok); + + content.add(controls, BorderLayout.SOUTH); + + getContentPane().add(content, BorderLayout.CENTER); + pack(); + + setLocationRelativeTo(owner); + } + + private JPanel newField(String label, String value, int cols) { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); + JLabel lbl = new JLabel(label); + lbl.setHorizontalAlignment(SwingConstants.RIGHT); + lbl.setPreferredSize(new Dimension(125, 20)); + panel.add(lbl); + JTextField tf = new JTextField(value, cols); + tf.setCaretPosition(0); + tf.setEditable(false); + panel.add(tf); + return panel; + } + + private String fingerprint(String value) { + value = value.toUpperCase(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < value.length(); i += 2) { + sb.append(value.charAt(i)); + sb.append(value.charAt(i + 1)); + sb.append(':'); + } + sb.setLength(sb.length() - 1); + return sb.toString(); + } +} diff --git a/src/com/gitblit/client/DateCellRenderer.java b/src/com/gitblit/client/DateCellRenderer.java index 954dad2a..751c7dbb 100644 --- a/src/com/gitblit/client/DateCellRenderer.java +++ b/src/com/gitblit/client/DateCellRenderer.java @@ -24,8 +24,6 @@ import javax.swing.JTable; import javax.swing.SwingConstants; import javax.swing.table.DefaultTableCellRenderer; -import com.gitblit.utils.TimeUtils; - /** * Time ago cell renderer with real date tooltip. * @@ -55,7 +53,13 @@ public class DateCellRenderer extends DefaultTableCellRenderer { title = "--"; dateString = "never"; } else { - title = Translation.getTimeUtils().timeAgo(date); + if (date.getTime() - System.currentTimeMillis() > 0) { + // future + title = Translation.getTimeUtils().inFuture(date); + } else { + // past + title = Translation.getTimeUtils().timeAgo(date); + } dateString = new SimpleDateFormat(pattern).format((Date) value); } diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java index 325b529f..86840048 100644 --- a/src/com/gitblit/utils/StringUtils.java +++ b/src/com/gitblit/utils/StringUtils.java @@ -262,14 +262,26 @@ public class StringUtils { * @return md5 of the string */ public static String getMD5(String string) { + try { + return getMD5(string.getBytes("iso-8859-1")); + } catch (UnsupportedEncodingException u) { + throw new RuntimeException(u); + } + } + + /** + * Calculates the MD5 of the string. + * + * @param string + * @return md5 of the string + */ + public static String getMD5(byte [] bytes) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.reset(); - md.update(string.getBytes("iso-8859-1")); + md.update(bytes); byte[] digest = md.digest(); return toHex(digest); - } catch (UnsupportedEncodingException u) { - throw new RuntimeException(u); } catch (NoSuchAlgorithmException t) { throw new RuntimeException(t); } diff --git a/src/com/gitblit/utils/TimeUtils.java b/src/com/gitblit/utils/TimeUtils.java index 7f695625..ec8871c6 100644 --- a/src/com/gitblit/utils/TimeUtils.java +++ b/src/com/gitblit/utils/TimeUtils.java @@ -268,6 +268,22 @@ public class TimeUtils { } } + public String inFuture(Date date) { + long diff = date.getTime() - System.currentTimeMillis(); + if (diff > ONEDAY) { + double days = ((double) diff)/ONEDAY; + return translate((int) Math.round(days), "gb.time.inDays", "in {0} days"); + } else { + double hours = ((double) diff)/ONEHOUR; + if (hours > 2) { + return translate((int) Math.round(hours), "gb.time.inHours", "in {0} hours"); + } else { + int mins = (int) (diff/MIN); + return translate(mins, "gb.time.inMinutes", "in {0} minutes"); + } + } + } + private String translate(String key, String defaultValue) { String value = defaultValue; if (translation != null && translation.containsKey(key)) { diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties index 22ae92f7..6b2102ef 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/com/gitblit/wicket/GitBlitWebApp.properties @@ -374,3 +374,44 @@ gb.missingPermission = the repository for this permission is missing! gb.mutable = mutable gb.specified = specified gb.effective = effective +gb.organizationalUnit = organizational unit +gb.organization = organization +gb.locality = locality +gb.stateProvince = state or province +gb.countryCode = country code +gb.properties = properties +gb.issued = issued +gb.expires = expires +gb.expired = expired +gb.expiring = expiring +gb.revoked = revoked +gb.serialNumber = serial number +gb.certificates = certificates +gb.newCertificate = new certificate +gb.revokeCertificate = revoke certificate +gb.sendEmail = send email +gb.passwordHint = password hint +gb.ok = ok +gb.invalidExpirationDate = invalid expiration date! +gb.passwordHintRequired = password hint required! +gb.viewCertificate = view certificate +gb.subject = subject +gb.issuer = issuer +gb.validFrom = valid from +gb.validUntil = valid until +gb.publicKey = public key +gb.signatureAlgorithm = signature algorithm +gb.sha1FingerPrint = SHA-1 Fingerprint +gb.md5FingerPrint = MD5 Fingerprint +gb.reason = reason +gb.revokeCertificateReason = Please select a reason for certificate revocation +gb.unspecified = unspecified +gb.keyCompromise = key compromise +gb.caCompromise = CA compromise +gb.affiliationChanged = affiliation changed +gb.superseded = superseded +gb.cessationOfOperation = cessation of operation +gb.privilegeWithdrawn = privilege withdrawn +gb.time.inMinutes = in {0} mins +gb.time.inHours = in {0} hours +gb.time.inDays = in {0} days \ No newline at end of file -- 2.39.5