/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* 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 net.java.sip.communicator.impl.dns;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import net.java.sip.communicator.service.dns.*;
import net.java.sip.communicator.service.notification.*;
import net.java.sip.communicator.util.Logger;
import net.java.sip.communicator.plugin.desktoputil.*;
import org.jitsi.dnssec.validator.ValidatingResolver;
import org.jitsi.service.configuration.*;
import org.jitsi.service.resources.*;
import org.jitsi.util.*;
import org.xbill.DNS.*;
/**
* Resolver that wraps a DNSSEC capable resolver and handles validation
* failures according to the user's settings.
*
* @author Ingo Bauersachs
*/
public class ConfigurableDnssecResolver
extends ValidatingResolver
implements CustomResolver
{
private final static Logger logger
= Logger.getLogger(ConfigurableDnssecResolver.class);
/**
* Name of the property that defines the default DNSSEC validation
* behavior.
*/
public final static String PNAME_DNSSEC_VALIDATION_MODE
= "net.java.sip.communicator.util.dns.DNSSEC_VALIDATION_MODE";
/**
* Default value of {@link #PNAME_DNSSEC_VALIDATION_MODE}
*/
public final static String PNAME_BASE_DNSSEC_PIN
= "net.java.sip.communicator.util.dns.pin";
final static String EVENT_TYPE = "DNSSEC_NOTIFICATION";
private ConfigurationService config
= DnsUtilActivator.getConfigurationService();
private ResourceManagementService R
= DnsUtilActivator.getResources();
private Map<String, Date> lastNotifications
= new HashMap<String, Date>();
private ExtendedResolver headResolver;
/**
* Creates a new instance of this class. Tries to use the system's
* default forwarders.
*/
public ConfigurableDnssecResolver(ExtendedResolver headResolver)
{
super(headResolver);
List<String> propNames
= config.getPropertyNamesByPrefix("org.jitsi.dnssec", false);
Properties config = new Properties();
for (String propName : propNames)
{
String value = config.getProperty(propName);
if (!StringUtils.isNullOrEmpty(value))
{
config.put(propName, value);
}
}
try
{
super.init(config);
}
catch (IOException e)
{
logger.error("Extended dnssec properties contained an error", e);
}
this.headResolver = headResolver;
reset();
Lookup.setDefaultResolver(this);
DnsUtilActivator.getNotificationService().
registerDefaultNotificationForEvent(
ConfigurableDnssecResolver.EVENT_TYPE,
NotificationAction.ACTION_POPUP_MESSAGE,
null, null);
}
/**
* Inspects a DNS answer message and handles validation results according to
* the user's preferences.
*
* @throws DnssecRuntimeException when the validation failed and the user
* did not choose to ignore it.
*/
@Override
public Message send(Message query)
throws DnssecRuntimeException, IOException
{
//---------------------------------------------------------------------
// || 1 | 2 | 3 | 4 | 5
//---------------------------------------------------------------------
// Sec. | Bog. || Ign. | Sec.Only | Sec.Or.Unsig | Warn.Bog | WarnAll
//---------------------------------------------------------------------
//a) 1 | 0 || ok | ok | ok | ok | ok
//b) 0 | 1 || ok | nok | nok | ask | ask
//c) 0 | 0 || ok | nok | ok | ok | ask
//---------------------------------------------------------------------
SecureMessage msg = new SecureMessage(super.send(query));
String fqdn = msg.getQuestion().getName().toString();
String type = Type.string(msg.getQuestion().getType());
String propName = createPropNameUnsigned(fqdn, type);
SecureResolveMode defaultAction = Enum.valueOf(SecureResolveMode.class,
config.getString(
PNAME_DNSSEC_VALIDATION_MODE,
SecureResolveMode.WarnIfBogus.name()
)
);
SecureResolveMode pinned = Enum.valueOf(SecureResolveMode.class,
config.getString(
propName,
defaultAction.name()
)
);
//create default entry
if(pinned == defaultAction)
config.setProperty(propName, pinned.name());
//check domain policy
//[abc]1, a[2-5]
if(pinned == SecureResolveMode.IgnoreDnssec || msg.isSecure())
return msg;
if(
//b2, c2
(pinned == SecureResolveMode.SecureOnly && !msg.isSecure())
||
//b3
(pinned == SecureResolveMode.SecureOrUnsigned && msg.isBogus())
)
{
String text = getExceptionMessage(msg);
Date last = lastNotifications.get(text);
if(last == null
//wait at least 5 minutes before showing the same info again
|| last.before(new Date(new Date().getTime() - 1000*60*5)))
{
DnsUtilActivator.getNotificationService().fireNotification(
EVENT_TYPE,
R.getI18NString("util.dns.INSECURE_ANSWER_TITLE"),
text,
null);
lastNotifications.put(text, new Date());
}
throw new DnssecRuntimeException(text);
}
//c3
if(pinned == SecureResolveMode.SecureOrUnsigned && !msg.isBogus())
return msg;
//c4
if(pinned == SecureResolveMode.WarnIfBogus && !msg.isBogus())
return msg;
//b4, b5, c5
String reason = msg.isBogus()
? R.getI18NString("util.dns.DNSSEC_ADVANCED_REASON_BOGUS",
new String[]{fqdn, msg.getBogusReason()})
: R.getI18NString("util.dns.DNSSEC_ADVANCED_REASON_UNSIGNED",
new String[]{type, fqdn});
DnssecDialog dlg = new DnssecDialog(fqdn, reason);
dlg.setVisible(true);
DnssecDialogResult result = dlg.getResult();
switch(result)
{
case Accept:
break;
case Deny:
throw new DnssecRuntimeException(getExceptionMessage(msg));
case AlwaysAccept:
if(msg.isBogus())
config.setProperty(propName,
SecureResolveMode.IgnoreDnssec.name());
else
config.setProperty(propName,
SecureResolveMode.WarnIfBogus.name());
break;
case AlwaysDeny:
config.setProperty(propName, SecureResolveMode.SecureOnly);
throw new DnssecRuntimeException(getExceptionMessage(msg));
}
return msg;
}
/**
* Defines the return code from the DNSSEC verification dialog.
*/
private enum DnssecDialogResult
{
/** The DNS result shall be accepted. */
Accept,
/** The result shall be rejected. */
Deny,
/** The result shall be accepted permanently. */
AlwaysAccept,
/**
* The result shall be rejected automatically unless it is valid
* according to DNSSEC.
*/
AlwaysDeny
}
/**
* Dialog to ask and warn the user if he wants to continue to accept an
* invalid dnssec result.
*/
private class DnssecDialog extends SIPCommDialog implements ActionListener
{
/**
* Serial version UID.
*/
private static final long serialVersionUID = 0L;
//UI controls
private JPanel pnlAdvanced;
private JPanel pnlStandard;
private final String domain;
private final String reason;
private JButton cmdAck;
private JButton cmdShowDetails;
//state
private DnssecDialogResult result = DnssecDialogResult.Deny;
/**
* Creates a new instance of this class.
* @param domain The FQDN of the domain that failed.
* @param reason String describing why the validation failed.
*/
public DnssecDialog(String domain, String reason)
{
super(false);
setModal(true);
this.domain = domain;
this.reason = reason;
initComponents();
}
/**
* Creates the UI controls
*/
private void initComponents()
{
setLayout(new BorderLayout(15, 15));
setTitle(R.getI18NString("util.dns.INSECURE_ANSWER_TITLE"));
// warning text
JLabel imgWarning =
new JLabel(R.getImage("service.gui.icons.WARNING_ICON"));
imgWarning.setBorder(BorderFactory
.createEmptyBorder(10, 10, 10, 10));
add(imgWarning, BorderLayout.WEST);
JLabel lblWarning = new JLabel(
R.getI18NString("util.dns.DNSSEC_WARNING", new String[]{
R.getSettingsString("service.gui.APPLICATION_NAME"),
domain
})
);
add(lblWarning, BorderLayout.CENTER);
//standard panel (deny option)
cmdAck = new JButton(R.getI18NString("service.gui.OK"));
cmdAck.addActionListener(this);
cmdShowDetails = new JButton(
R.getI18NString("util.dns.DNSSEC_ADVANCED_OPTIONS"));
cmdShowDetails.addActionListener(this);
pnlStandard = new TransparentPanel(new BorderLayout());
pnlStandard.setBorder(BorderFactory
.createEmptyBorder(10, 10, 10, 10));
pnlStandard.add(cmdShowDetails, BorderLayout.WEST);
pnlStandard.add(cmdAck, BorderLayout.EAST);
add(pnlStandard, BorderLayout.SOUTH);
//advanced panel
pnlAdvanced = new TransparentPanel(new BorderLayout());
JPanel pnlAdvancedButtons = new TransparentPanel(
new FlowLayout(FlowLayout.RIGHT));
pnlAdvancedButtons.setBorder(BorderFactory
.createEmptyBorder(10, 10, 10, 10));
pnlAdvanced.add(pnlAdvancedButtons, BorderLayout.EAST);
for(DnssecDialogResult r : DnssecDialogResult.values())
{
JButton cmd = new JButton(R.getI18NString(
"net.java.sip.communicator.util.dns."
+ "ConfigurableDnssecResolver$DnssecDialogResult."
+ r.name()));
cmd.setActionCommand(r.name());
cmd.addActionListener(this);
pnlAdvancedButtons.add(cmd);
}
JLabel lblReason = new JLabel(reason);
lblReason.setBorder(BorderFactory
.createEmptyBorder(10, 10, 10, 10));
pnlAdvanced.add(lblReason, BorderLayout.NORTH);
}
/**
* Handles the events coming from the buttons.
*/
public void actionPerformed(ActionEvent e)
{
if(e.getSource() == cmdAck)
{
result = DnssecDialogResult.Deny;
dispose();
}
else if(e.getSource() == cmdShowDetails)
{
getContentPane().remove(pnlStandard);
add(pnlAdvanced, BorderLayout.SOUTH);
pack();
}
else
{
result = Enum.valueOf(DnssecDialogResult.class,
e.getActionCommand());
dispose();
}
}
/**
* Gets the option that user has chosen.
* @return the option that user has chosen.
*/
public DnssecDialogResult getResult()
{
return result;
}
}
private String getExceptionMessage(SecureMessage msg)
{
return msg.getBogusReason() == null
? R.getI18NString(
"util.dns.INSECURE_ANSWER_MESSAGE_NO_REASON",
new String[]{msg.getQuestion().getName().toString()}
)
: R.getI18NString(
"util.dns.INSECURE_ANSWER_MESSAGE_REASON",
new String[]{msg.getQuestion().getName().toString(),
//TODO parse bogus reason text and translate
msg.getBogusReason()}
);
}
private String createPropNameUnsigned(String fqdn, String type)
{
return PNAME_BASE_DNSSEC_PIN + "." + fqdn.replace(".", "__");
}
/**
* Reloads the configuration of forwarders and trust anchors.
*/
@Override
public void reset()
{
String forwarders = DnsUtilActivator.getConfigurationService()
.getString(DnsUtilActivator.PNAME_DNSSEC_NAMESERVERS);
if(!StringUtils.isNullOrEmpty(forwarders, true))
{
if(logger.isTraceEnabled())
{
logger.trace("Setting DNSSEC forwarders to: " + forwarders);
}
synchronized (Lookup.class)
{
Lookup.refreshDefault();
String[] fwds = forwarders.split(",");
Resolver[] rs = headResolver.getResolvers();
for (Resolver r : rs)
{
headResolver.deleteResolver(r);
}
for (String fwd : fwds)
{
try
{
SimpleResolver sr = new SimpleResolver(fwd);
// these properties are normally set by the
// ValidatingResolver in the constructor
sr.setEDNS(0, 0, ExtendedFlags.DO, null);
sr.setIgnoreTruncation(false);
headResolver.addResolver(sr);
}
catch (UnknownHostException e)
{
logger.error("Invalid forwarder, ignoring", e);
}
}
Lookup.setDefaultResolver(this);
}
}
StringBuilder sb = new StringBuilder();
for(int i = 1;;i++)
{
String anchor = DnsUtilActivator.getResources().getSettingsString(
"net.java.sip.communicator.util.dns.DS_ROOT." + i);
if(anchor == null)
{
break;
}
sb.append(anchor);
sb.append('\n');
}
try
{
super.loadTrustAnchors(new ByteArrayInputStream(
sb.toString().getBytes("ASCII")));
}
catch (IOException e)
{
logger.error("Could not load the trust anchors", e);
}
if(logger.isTraceEnabled())
logger.trace("Loaded trust anchors " + sb.toString());
}
}