/* This file is part of jTotus. jTotus 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 3 of the License, or (at your option) any later version. jTotus 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 jTotus. If not, see <http://www.gnu.org/licenses/>. */ package org.jtotus.network; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.jlucrum.realtime.eventtypes.StockTick; import java.io.BufferedReader; import java.io.FileReader; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jtotus.config.ConfigLoader; import org.jtotus.config.GUIConfig; import org.jtotus.engine.StartUpLoader; import org.jtotus.network.BrokerConnector.ConnectorState; /** * * @author Evgeni Kappinen */ public class NordnetConnect implements NetworkTickConnector { ConnectorState state = BrokerConnector.state.INITIAL; //private static final String _LOGIN_URL_ = "https://www.nordnet.fi/mux/login/login.html"; //private static final String _LOGIN_URL_ = "https://www.nordnet.fi/mux/login/start.html"; private static final String _LOGIN_URL_ = "https://www.nordnet.fi/mux/login/startFI.html"; private static final String _LOGININPUT_URL_ = "https://www.nordnet.fi//mux/login/login.html"; //private static final String _PORTFOLIO_URL_ = "https://www.nordnet.fi/mux/web/depa/mindepa/depaoversikt.html"; private static final String _PORTFOLIO_URL_ = "https://www.nordnet.fi/mux/web/user/overview.html"; private static final String _STOCK_INFO_URL_ = "https://www.nordnet.fi/mux/web/marknaden/aktiehemsidan/index.html"; private static final String _ECRYPT_JS_ = "https://www.nordnet.fi/now/js/encrypt.js"; private HashMap<String, Integer> stockNameToIndex = null; private BrokerConnector connector = null; private final static Log log = LogFactory.getLog( NordnetConnect.class ); // Connects to login page, get seeds for user and password // POST data to server, by calling NordnetConnector private void fillStockNamesConverter() { if (stockNameToIndex != null) { return; } stockNameToIndex = new HashMap<String, Integer>(); stockNameToIndex.put("Cargotec Oyj", 29983); //stockNameToIndex.put("Elisa Oyj","ELI1V.HSE"); stockNameToIndex.put("Fortum Oyj", 24271); stockNameToIndex.put("Kemira Oyj", 24292); stockNameToIndex.put("KONE Oyj", 75061); stockNameToIndex.put("Konecranes Oyj", 24284); stockNameToIndex.put("Metso Oyj", 24302); stockNameToIndex.put("Neste Oil", 29375); stockNameToIndex.put("Nokia Oyj", 24311); //stockNameToIndex.put("Nokian Renkaat Oyj","NRE1V.HSE"); stockNameToIndex.put("Nordea Bank AB", 24308); stockNameToIndex.put("Outokumpu Oyj", 24321); stockNameToIndex.put("Outotec Oyj", 36695); stockNameToIndex.put("Pohjola Bank A", 24316); stockNameToIndex.put("Rautaruukki Oyj", 24342); stockNameToIndex.put("Sampo Oyj A", 24346); stockNameToIndex.put("Sanoma Oyj", 24366); stockNameToIndex.put("Stora Enso Oyj A", 24359); stockNameToIndex.put("TeliaSonera AB", 24381); stockNameToIndex.put("Tieto Oyj", 24376); stockNameToIndex.put("UPM-Kymmene Oyj", 24386); stockNameToIndex.put("Wärtsilä Corporation", 24394); stockNameToIndex.put("YIT Oyj", 24397); } public boolean authenticated() { String loginPage = null; if (connector == null) { System.err.printf("Failure connector is empty\n"); return false; } loginPage = connector.getPage(_PORTFOLIO_URL_); if (loginPage == null) { System.err.printf("Failure unable to fetch portfolio\n"); return false; } Document doc = Jsoup.parse(loginPage); Elements elements = doc.select("title"); //FIXME: UTF-8 for httpclient! if (elements.html().equals("Yleisnäkymä - Nordnet")) { return true; } else { System.err.printf("Failure in match for : %s \n", elements.html()); } return false; } public String fetchEncryptedPassword(String encryptJS, String pass, String pubKey, String sessionId) { String password = null; StartUpLoader loader = StartUpLoader.getInstance(); //ScriptEngineManager mgr = loader.getLoadedScriptManager(); // Bindings bindings = mgr.getBindings(); ScriptEngine engine = loader.getLoadedEngine(); Bindings bindings = engine.getBindings(ScriptContext.GLOBAL_SCOPE); try { StringBuilder strBuild = new StringBuilder(); strBuild.append(encryptJS); strBuild.append(" \n var keyObj = RSA.getPublicKey(\'"+pubKey+"\');\n" + " var encryptedPass = RSA.encrypt(\'"+pass+"\', keyObj, \'"+sessionId+"\');\n"); engine.eval(strBuild.toString(), bindings); password = (String)bindings.get("encryptedPass"); } catch (ScriptException ex) { Logger.getLogger(NordnetConnector.class.getName()).log(Level.SEVERE, null, ex); } log.info("JavaScript engine loaded:" + engine.NAME); return password; } private String fetchEncryptionScript(String filename) { String script = null; String line = null; BufferedReader input = null; StringBuilder data = new StringBuilder(); File file =new File(filename); if (!file.isFile() || !file.exists()) { return null; } try { input = new BufferedReader(new FileReader(file)); while ((line = input.readLine()) != null) { data.append(line); data.append(System.getProperty("line.separator")); } script = data.toString(); } catch (IOException ex) { // Logger.getLogger(NordnetConnect.class.getName()).log(Level.SEVERE, null, ex); System.out.printf(" Could not find js for authentication : %s", filename); } finally { try { if (input != null) input.close(); } catch (IOException ex) { Logger.getLogger(NordnetConnect.class.getName()).log(Level.SEVERE, null, ex); } } return script; } private boolean connectAndAuth(String user, String password) { ArrayList<String> inputList = new ArrayList(); connector = new NordnetConnector(); String encryptJS = fetchEncryptionScript("./lib/encrypt.js"); if (encryptJS == null) { encryptJS = connector.getPage(_ECRYPT_JS_); if (encryptJS == null) { System.err.printf("Failed to get encrypt javascript\n"); return false; } } String loginPage = connector.getPage(_LOGIN_URL_); if (loginPage == null) { System.err.printf("Failed to get login page\n"); return false; } Document doc = Jsoup.parse(loginPage); Elements elements = doc.select("input"); Iterator<Element> iter = elements.iterator(); while (iter.hasNext()) { Element elem = iter.next(); inputList.add(elem.attr("name")); } if (inputList.size()<2) { System.err.printf("Failure: \n %s \n", loginPage); return false; } elements = doc.select("script"); if (elements.size() < 4) { System.err.printf("Incorrect size of script elements\n"); return false; } Element elem = elements.get(4); String []data = elem.data().split("'"); if (data.length < 8) { System.err.printf("Incorrect size of splitted elements for pass and login tokens\n"); return false; } log.info("Got element: data:"+data[7]+" html:" + data[5]); String encryptedPassword = fetchEncryptedPassword(encryptJS, password, data[5].trim() /*pubKey*/, data[7].trim() /*sessionId*/); loginPage = connector.authenticate(_LOGININPUT_URL_, inputList.get(3), user, inputList.get(5), encryptedPassword); System.err.printf("login: %s = %s pass: %s = %s\n", inputList.get(3), user, inputList.get(5), encryptedPassword); if (loginPage == null) { System.err.printf("Failed to get authenticate\n"); return false; } if (!authenticated()) { return false; } return true; } public boolean connect() { ConfigLoader<GUIConfig> loader = new ConfigLoader<GUIConfig>("GUIConfig"); GUIConfig config = loader.getConfig(); if (config == null) { return false; } this.fillStockNamesConverter(); return this.connectAndAuth(config.getBrokerLogin(), config.getBrokerPassword()); } private StockTick parseAuthenticatedStream(String infoPage, String stockName) { StockTick tick = null; Document doc = Jsoup.parse(infoPage); Elements elements = doc.select("tr[class=first]"); doc = Jsoup.parse(elements.html()); elements = doc.select("td"); if (elements.size() != 15) { //not authenticated 13 return tick; } tick = new StockTick(); tick.setStockName(stockName); Iterator<Element> iter = elements.iterator(); for (int count = 0; iter.hasNext(); count++) { Element elem = iter.next(); log.info("Element value ("+count+"):"+elem.text()); switch (count) { case 3: if (!elem.text().equalsIgnoreCase("OMX Helsinki")) { System.err.printf("Data corruption in broker site? :%s for: %s\n", elem.text(), stockName); return null; } break; case 4://latest price tick.setLatestPrice(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 5://latest buy tick.setLatestBuy(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 6://latest sell tick.setLatestSell(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 7://latest Highest tick.setLatestHighest(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 8://latest Lowest tick.setLatestLowest(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 11://latest Lowest tick.setVolume(Double.parseDouble(elem.text().replace(" ", "").trim())); break; case 12://latest Lowest tick.setTradesSum(Double.parseDouble(elem.text().replace(" ", "").trim())); break; case 14://Time tick.setTime(elem.text().trim()); break; //TODO:currency and time default: log.info("Not matched(" +count+ ") = " + elem.text()); break; } } log.info("StockTick:" + tick.toString()); return tick; } private StockTick parseNonAuthenticatedStream(String infoPage, String stockName) { StockTick tick = null; Document doc = Jsoup.parse(infoPage); Elements elements = doc.select("tr[class=first]"); doc = Jsoup.parse(elements.html()); elements = doc.select("td"); if (elements.size() != 13) { //not authenticated 13 return tick; } tick = new StockTick(); tick.setStockName(stockName); Iterator<Element> iter = elements.iterator(); for (int count = 0; iter.hasNext(); count++) { Element elem = iter.next(); System.out.printf("Non-Auth Element value (%d):%s for:%s\n", count, elem.text(), stockName); switch (count) { case 1: if (!elem.text().equalsIgnoreCase("OMX Helsinki")) { System.err.printf("Data corruption in broker site? :%s for: %s\n", elem.text(), stockName); return null; } break; case 2://latest price tick.setLatestPrice(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 3://latest buy tick.setLatestBuy(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 4://latest sell tick.setLatestSell(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 5://latest Highest tick.setLatestHighest(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 6://latest Lowest tick.setLatestLowest(Double.parseDouble(elem.text().replace(",", ".").trim())); break; case 9://Volume tick.setVolume(Double.parseDouble(elem.text().replace(" ", "").trim())); break; case 10://Trade Sum tick.setTradesSum(Double.parseDouble(elem.text().replace(" ", "").trim())); break; case 12://Time tick.setTime(elem.text().trim()); break; //TODO:currency and time default: System.out.printf("Not matched(%d) = %s \n", count, elem.text()); break; } } System.out.printf("StockTick:%s\n", tick.toString()); return tick; } // http://jsoup.org/apidocs/org/jsoup/select/Selector.html public StockTick getTick(String stockName) { Integer index = stockNameToIndex.get(stockName); if (index == null) { System.err.printf("Index is not found for :%s\n", stockName); return null; } String infoPage = connector.getPage("%s?identifier=%d&marketid=%d", _STOCK_INFO_URL_, index.intValue(), 24); //Try to reconnect to server once. if (infoPage == null) { if (!this.connect()) { return null; } else { infoPage = connector.getPage("%s?identifier=%d&marketid=%d", _STOCK_INFO_URL_, index.intValue(), 24); if (infoPage == null) { return null; } } } if(authenticated()) { return parseAuthenticatedStream(infoPage, stockName); }else { return parseNonAuthenticatedStream(infoPage, stockName); } } }