001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.net.Authenticator.RequestorType;
007import java.net.HttpURLConnection;
008import java.nio.ByteBuffer;
009import java.nio.CharBuffer;
010import java.nio.charset.CharacterCodingException;
011import java.nio.charset.Charset;
012import java.nio.charset.CharsetEncoder;
013
014import oauth.signpost.OAuthConsumer;
015import oauth.signpost.exception.OAuthException;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.oauth.OAuthParameters;
019import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
020import org.openstreetmap.josm.io.auth.CredentialsAgentException;
021import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
022import org.openstreetmap.josm.io.auth.CredentialsManager;
023import org.openstreetmap.josm.tools.Base64;
024
025/**
026 * Base class that handles common things like authentication for the reader and writer
027 * to the osm server.
028 *
029 * @author imi
030 */
031public class OsmConnection {
032    protected boolean cancel = false;
033    protected HttpURLConnection activeConnection;
034    protected OAuthParameters oauthParameters;
035
036    /**
037     * Initialize the http defaults and the authenticator.
038     */
039    static {
040        try {
041            HttpURLConnection.setFollowRedirects(true);
042        } catch (SecurityException e) {
043            e.printStackTrace();
044        }
045    }
046
047    public void cancel() {
048        cancel = true;
049        synchronized (this) {
050            if (activeConnection != null) {
051                activeConnection.setConnectTimeout(100);
052                activeConnection.setReadTimeout(100);
053            }
054        }
055        try {
056            Thread.sleep(100);
057        } catch (InterruptedException ex) {
058            Main.warn("InterruptedException in "+getClass().getSimpleName()+" during cancel");
059        }
060
061        synchronized (this) {
062            if (activeConnection != null) {
063                activeConnection.disconnect();
064            }
065        }
066    }
067
068    /**
069     * Adds an authentication header for basic authentication
070     *
071     * @param con the connection
072     * @throws OsmTransferException thrown if something went wrong. Check for nested exceptions
073     */
074    protected void addBasicAuthorizationHeader(HttpURLConnection con) throws OsmTransferException {
075        CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
076        CredentialsAgentResponse response;
077        String token;
078        try {
079            synchronized (CredentialsManager.getInstance()) {
080                response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
081                con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
082            }
083        } catch (CredentialsAgentException e) {
084            throw new OsmTransferException(e);
085        }
086        if (response == null) {
087            token = ":";
088        } else if (response.isCanceled()) {
089            cancel = true;
090            return;
091        } else {
092            String username= response.getUsername() == null ? "" : response.getUsername();
093            String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword());
094            token = username + ":" + password;
095            try {
096                ByteBuffer bytes = encoder.encode(CharBuffer.wrap(token));
097                con.addRequestProperty("Authorization", "Basic "+Base64.encode(bytes));
098            } catch(CharacterCodingException e) {
099                throw new OsmTransferException(e);
100            }
101        }
102    }
103
104    /**
105     * Signs the connection with an OAuth authentication header
106     *
107     * @param connection the connection
108     *
109     * @throws OsmTransferException thrown if there is currently no OAuth Access Token configured
110     * @throws OsmTransferException thrown if signing fails
111     */
112    protected void addOAuthAuthorizationHeader(HttpURLConnection connection) throws OsmTransferException {
113        if (oauthParameters == null) {
114            oauthParameters = OAuthParameters.createFromPreferences(Main.pref);
115        }
116        OAuthConsumer consumer = oauthParameters.buildConsumer();
117        OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
118        if (! holder.containsAccessToken())
119            throw new MissingOAuthAccessTokenException();
120        consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret());
121        try {
122            consumer.sign(connection);
123        } catch(OAuthException e) {
124            throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
125        }
126    }
127
128    protected void addAuth(HttpURLConnection connection) throws OsmTransferException {
129        String authMethod = Main.pref.get("osm-server.auth-method", "basic");
130        if (authMethod.equals("basic")) {
131            addBasicAuthorizationHeader(connection);
132        } else if (authMethod.equals("oauth")) {
133            addOAuthAuthorizationHeader(connection);
134        } else {
135            String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
136            Main.warn(msg);
137            throw new OsmTransferException(msg);
138        }
139    }
140
141    /**
142     * Replies true if this connection is canceled
143     *
144     * @return true if this connection is canceled
145     */
146    public boolean isCanceled() {
147        return cancel;
148    }
149}