package com.owera.xaps.tr069.methods;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import com.owera.common.db.NoAvailableConnectionException;
import com.owera.common.log.Context;
import com.owera.common.util.NaturalComparator;
import com.owera.xaps.base.BaseCache;
import com.owera.xaps.base.Log;
import com.owera.xaps.base.NoDataAvailableException;
import com.owera.xaps.dbi.tr069.TR069DMLoader;
import com.owera.xaps.dbi.tr069.TR069DMParameter;
import com.owera.xaps.dbi.tr069.TR069DMParameter.StringType;
import com.owera.xaps.dbi.tr069.TR069DMParameterMap;
import com.owera.xaps.dbi.tr069.TR069DMType;
import com.owera.xaps.dbi.tr069.TestCase;
import com.owera.xaps.dbi.tr069.TestCaseParameter;
import com.owera.xaps.dbi.tr069.TestCaseParameter.TestCaseParameterType;
import com.owera.xaps.dbi.tr069.TestDB;
import com.owera.xaps.dbi.tr069.TestHistory;
import com.owera.xaps.tr069.HTTPReqData.XMLFormatter;
import com.owera.xaps.tr069.HTTPReqResData;
import com.owera.xaps.tr069.Properties;
import com.owera.xaps.tr069.SessionData;
import com.owera.xaps.tr069.UnknownMethodException;
import com.owera.xaps.tr069.exception.TR069Exception;
import com.owera.xaps.tr069.exception.TR069ExceptionShortMessage;
import com.owera.xaps.tr069.test.system1.KillDatabase;
import com.owera.xaps.tr069.test.system1.KillDatabaseObject;
import com.owera.xaps.tr069.test.system1.TestDatabase;
import com.owera.xaps.tr069.test.system1.TestDatabaseObject;
import com.owera.xaps.tr069.test.system2.TestException;
import com.owera.xaps.tr069.test.system2.TestUnit;
import com.owera.xaps.tr069.test.system2.TestUnitCache;
import com.owera.xaps.tr069.test.system2.Util;
import com.owera.xaps.tr069.xml.ParameterAttributeStruct;
import com.owera.xaps.tr069.xml.ParameterValueStruct;
/**
* RequestProcessor will parse the xml from the CPE. Any vital information will be stored in the SessionData or
* RequestResponse objects. Some logging.
* @author Morten
*
*/
public class HTTPRequestProcessor {
private static TR069DMParameterMap tr069ParameterMap = null;
public static TR069DMParameterMap getTR069ParameterMap() throws Exception {
if (tr069ParameterMap == null) {
tr069ParameterMap = TR069DMLoader.load();
}
return tr069ParameterMap;
}
private static void verifyKillResponse(String xml, String unitId) {
try {
KillDatabaseObject kdo = new KillDatabaseObject(KillDatabase.database.select(unitId));
kdo.setTestRunning(false);
if (xml.indexOf("GetParameterNamesResponse") > -1) {
FileWriter gpnW = new FileWriter("GPN-" + unitId + ".txt");
gpnW.write(xml);
gpnW.close();
kdo.setGpnFile("GPN-" + unitId + ".txt");
} else if (xml.indexOf("GetParameterValuesResponse") > -1) {
FileWriter gpvW = new FileWriter("GPV-" + unitId + ".txt");
gpvW.write(xml);
gpvW.close();
kdo.setGpvFile("GPV-" + unitId + ".txt");
} else {
FileWriter fw = new FileWriter("kill-report-" + unitId + ".txt", true);
FileWriter fwDetails = new FileWriter("kill-report-details-" + unitId + ".txt", true);
if (xml.indexOf("Fault") > -1) {
fw.write("failed");
fwDetails.write("\nCPE-response was Fault:\n" + xml + "\n");
} else {
fw.write("ok");
fwDetails.write("\nCPE-response was OK:\n" + xml + "\n");
}
fw.close();
fwDetails.close();
}
KillDatabase.database.insert(unitId, kdo.toString());
} catch (Throwable t) {
Log.warn(HTTPRequestProcessor.class, "Error occurred in verifyKillResponse: " + t);
}
}
/**
* Verify response from device according to the new, 2.generation test-system.
* The verification is based on several sources, like CWMP fault, what parameter values
* might be expected.
* @param reqRes
* @throws NoAvailableConnectionException
* @throws SQLException
*/
private static void verifyResponseNEW(String requestMethodName, HTTPReqResData reqRes) throws Exception {
// TODO:TF - verify result - completed
// TR069 Test Framework:
// Get TestUnit-object from TestUnitCache
// Get STEPS from Unit
// Get CurrentStep from TestUnit-object
// Verify results - update TestCaseResult object if necessary (using TestDB)
// 1. CWMP FAULT -> fail
// 2. Device parameters do not match GET/FAC-parameters from tc -> fail
// 3. Device parameters do not match datamodel -> fail
// 4. Exception in TR-069 server -> fail
if (requestMethodName.equals(TR069Method.FAULT)) {
Log.debug(HTTPRequestProcessor.class, "Fault on the client side has been discovered - will process");
FAres.process(reqRes);
} else
Log.debug(HTTPRequestProcessor.class, "No fault on the client side - moving on");
SessionData sessionData = reqRes.getSessionData();
TestUnit tu = TestUnitCache.get(sessionData.getUnitId());
tu.setUnit(reqRes.getSessionData().getUnit()); // Update TestUnit with latest Unit-information
TestCase tc = tu.getCurrentCase();
TestHistory history = tu.getHistory();
if (history == null) { // Make a history-entry if not specified
history = new TestHistory(sessionData.getUnittype(), new Date(sessionData.getStartupTmsForSession()), sessionData.getUnitId(), tc.getId(), tc.getExpectError());
tu.setHistory(history);
}
TestDB testDB = new TestDB(sessionData.getDbAccess().getXaps());
Log.debug(HTTPRequestProcessor.class, "Have retrieved the TestDB object");
try {
try {
tr069ParameterMap = getTR069ParameterMap();
} catch (Exception e) {
throw new TestException("Could not load TR-069 data model - aborting test");
}
if (reqRes.getThrowable() != null) {
history.setFailed(true);
history.addResult("TR-069 Server failed in step " + tu.getSteps().getCurrentStep() + ":\n" + reqRes.getThrowable().getMessage());
}
boolean validParameter = true;
if (reqRes.getRequest().getFault() != null) {
if (reqRes.getRequest().getFault().getFaultCode() != null && reqRes.getRequest().getFault().getFaultCode().trim().equals("9005")) {
validParameter = false;
} else {
history.setFailed(true);
String faultMsg = reqRes.getRequest().getFault().toString().trim().replaceAll("\\n", ",").replaceAll("\\s+:\\s+", ":");
history.addResult("A CWMP Fault occurred after executing step " + tu.getSteps().getCurrentStep() + ": " + faultMsg);
}
}
if (validParameter && sessionData.getPreviousResponseMethod().equals(TR069Method.GET_PARAMETER_VALUES)) {
GPVres.process(reqRes);
Map<String, String> deviceParamMap = buildDeviceParamMapFromValues(sessionData.getFromCPE());
for (TestCaseParameter tcp : tc.getParams()) {
if (tcp.getType() != TestCaseParameterType.GET)
continue;
String deviceValue = deviceParamMap.get(tcp.getUnittypeParameter().getName());
if (deviceValue == null) {
history.setFailed(true);
history.addResult("GETVALUE from device on parameter " + tcp.getUnittypeParameter().getName() + " returned no value at all");
continue;
}
TR069DMParameter tr069Parameter = tr069ParameterMap.getParameter(tcp.getUnittypeParameter().getName());
if (tcp.getType() == tu.getSteps().getCompareType() && !validAccordingToTestCase(tcp, deviceValue, tr069Parameter)) {
history.setFailed(true);
history.addResult("GETVALUE from device on parameter " + tcp.getUnittypeParameter().getName() + " returned \"" + lengthCheck(deviceValue) + "\", expected (test case "
+ tcp.getType() + ") : " + tcp.getValue());
}
String valid = validAccordingToDataModel(tr069Parameter, deviceValue);
if (!valid.equals("OK")) {
history.setFailed(true);
history.addResult("GETVALUE from device on parameter " + tcp.getUnittypeParameter().getName() + " returned \"" + lengthCheck(deviceValue) + "\", violates TR-069 data model: "
+ valid);
}
}
} else if (validParameter && sessionData.getPreviousResponseMethod().equals(TR069Method.GET_PARAMETER_ATTRIBUTES)) {
GPAres.process(reqRes);
Map<String, Integer> deviceParamMap = buildDeviceParamMapFromAttributes(sessionData.getAttributesFromCPE());
for (TestCaseParameter tcp : tc.getParams()) {
if (tcp.getType() != TestCaseParameterType.GET)
continue;
Integer notification = deviceParamMap.get(tcp.getUnittypeParameter().getName());
if (notification == null) {
history.setFailed(true);
history.addResult("GETATTRIBUTE from device on parameter " + tcp.getUnittypeParameter().getName() + " returned no notification at all");
continue;
}
TR069DMParameter tr069Parameter = tr069ParameterMap.getParameter(tcp.getUnittypeParameter().getName());
String valid = validAccordingToTestCase(tcp, notification, tr069Parameter);
if (!valid.equals("OK")) {
history.setFailed(true);
history.addResult("GETATTRIBUTE from device on parameter " + tcp.getUnittypeParameter().getName() + " returned " + notification + ", expected: " + valid);
}
valid = validAccordingToDataModel(tr069Parameter, notification);
if (!valid.equals("OK")) {
history.setFailed(true);
history.addResult("GETATTRIBUTE from device on parameter " + tcp.getUnittypeParameter().getName() + " returned " + notification + ", violates TR-069 data model: " + valid);
}
}
}
if (tu.getSteps().lastStep()) { // Extra checks on the last step
if (tc.getExpectError() != null && !tc.getExpectError() && history.getFailed() != null && history.getFailed()) {
history.setFailed(false); // will overwrite the previously set flag
}
}
} catch (TestException te) {
Log.debug(HTTPRequestProcessor.class, "TestException occurred, will be logged to history");
history.setFailed(true);
history.addResult("TR-069 Server failed in step " + tu.getSteps().getCurrentStep() + ":\n" + reqRes.getThrowable().getMessage());
}
Log.debug(HTTPRequestProcessor.class, "History has result " + history.getResult() + ", should be written to database");
testDB.addOrChangeTestHistory(history);
}
private static String lengthCheck(String value) {
if (value.length() > 64)
return value.substring(0, 64) + "...";
else
return value;
}
private static Map<String, String> buildDeviceParamMapFromValues(List<ParameterValueStruct> deviceParams) {
Map<String, String> deviceParamMap = new HashMap<String, String>();
for (ParameterValueStruct pvs : deviceParams) {
deviceParamMap.put(pvs.getName(), pvs.getValue());
}
return deviceParamMap;
}
private static Map<String, Integer> buildDeviceParamMapFromAttributes(List<ParameterAttributeStruct> deviceParams) {
Map<String, Integer> deviceParamMap = new HashMap<String, Integer>();
for (ParameterAttributeStruct pas : deviceParams) {
deviceParamMap.put(pas.getName(), pas.getNotifcation());
}
return deviceParamMap;
}
private static String validAccordingToTestCase(TestCaseParameter tcp, int notification, TR069DMParameter param) {
if (tcp.getNotification() > -1 && notification != tcp.getNotification()) {
if (param.getNotification() != null && param.getNotification().equals("canDeny")) {
return "OK";
} else {
return "" + tcp.getNotification();
}
}
return "OK";
}
/**
* Checks to see whether the parameter returned in a GET-method from the device
* is equal to the parameter expected by the test-case
* @param tcp
* @param deviceValue
* @return
*/
private static boolean validAccordingToTestCase(TestCaseParameter tcp, String deviceValue, TR069DMParameter dmp) {
if (tcp.getValue() == null && deviceValue == null)
return true;
if (tcp.getValue() == null && deviceValue != null)
return true;
String tcpVal = tcp.getValue();
if (tcpVal.equalsIgnoreCase("[STRING]"))
return true;
if (deviceValue == null || deviceValue.trim().equals("")) {
if (tcpVal.equalsIgnoreCase("[EMPTY]"))
return true;
else
return false;
}
if (tcpVal.equals(deviceValue))
return true;
if (tcpVal.equalsIgnoreCase("[NUMBER]")) {
try {
Integer.parseInt(deviceValue);
return true;
} catch (NumberFormatException nfe) {
return false;
}
}
if (dmp.getDatatype() == TR069DMType.BOOLEAN) {
if (tcp.getValue().equals("false") && deviceValue.equals("0"))
return true;
if (tcp.getValue().equals("true") && deviceValue.equals("1"))
return true;
}
return false;
}
private static String validAccordingToDataModel(TR069DMParameter param, Integer notification) {
if (param == null)
return "OK";
if (notification < 0 || notification > 2)
return "Notification is outside limits (0-2)";
if (param.getNotification() != null && param.getNotification().equals("forceEnabled") && notification != 2)
return "ForceEnabled is on, notification must be 2 (Active)";
return "OK";
}
private static boolean validAccordingToEnumeration(TR069DMParameter param, String deviceValue) {
boolean enumMatch = false;
for (StringType st : param.getEnumeration()) {
if (st.getValue() != null && st.getValue().equals(deviceValue))
enumMatch = true;
if (st.getPattern() != null && Pattern.matches(st.getPattern(), deviceValue))
enumMatch = true;
}
return enumMatch;
}
/**
* Checks to see whether the parameter returned in a GET-method from the device
* is equal to the parameter expected by the TR069 data model (loaded from XML schema
* published by Broadband Forum).
* @param param
* @param deviceValue
* @return
*/
private static String validAccordingToDataModel(TR069DMParameter param, String deviceValue) {
if (param == null)
return "OK"; // No TR-069 parameter officially defined - no validation possible
TR069DMType dt = param.getDatatype();
if (param.getEnumeration() != null && param.getEnumeration().size() > 0) { // Probably only found for String-type data
if (deviceValue == null)
return "Did not match enumeration - no value";
if (param.isList()) {
for (String s : deviceValue.split(",")) {
boolean enumMatch = validAccordingToEnumeration(param, s.trim());
if (!enumMatch)
return "Did not match enumeration";
}
return "OK";
} else {
if (validAccordingToEnumeration(param, deviceValue))
return "OK";
else
return "Did not match enumeration";
}
}
if (dt == TR069DMType.STRING || dt == TR069DMType.ALIAS || dt == TR069DMType.HEXBINARY || dt == TR069DMType.BASE64 || //
dt == TR069DMType.MACADDRESS || dt == TR069DMType.IPADDRESS || dt == TR069DMType.IPV6ADDRESS || dt == TR069DMType.IPV4ADDRESS || //
dt == TR069DMType.IPPREFIX || dt == TR069DMType.IPV4PREFIX || dt == TR069DMType.IPV6PREFIX) {
if (deviceValue == null || deviceValue.trim().equals(""))
return "OK";
if (param.getRange().getMin() != null && deviceValue.length() < param.getRange().getMin())
return "Size too small (" + deviceValue.length() + " < " + param.getRange().getMin() + ")";
if (param.getRange().getMax() != null && deviceValue.length() > param.getRange().getMax())
return "Size too big (" + deviceValue.length() + " > " + param.getRange().getMax() + ")";
if (dt == TR069DMType.HEXBINARY && !Pattern.matches("[a-fA-F-0-9]*", deviceValue))
return "Not hexBinary";
if (dt == TR069DMType.BASE64 && !Pattern.matches("[0-9a-zA-Z\\+/=]*", deviceValue))
return "Not base64";
// if (!param.hasSpecificRange() && (deviceValue.length() < dt.getMin() || deviceValue.length() > dt.getMax())) { // Default range
// return "Size outside range";
// }
} else if (dt == TR069DMType.BOOLEAN) {
if (!deviceValue.equals("0") && !deviceValue.equals("1") && !deviceValue.equals("false") && !deviceValue.equals("true"))
return "Not a boolean";
} else if (dt == TR069DMType.DATETIME) {
// Currently no validation of datetime - may perhaps look into SimpleDateFormat.parse()
} else if (dt == TR069DMType.INTEGER || dt == TR069DMType.INT || dt == TR069DMType.LONG || dt == TR069DMType.UNSIGNEDINT || //
dt == TR069DMType.UNSIGNEDLONG || dt == TR069DMType.STATSCOUNTER32 || dt == TR069DMType.STATSCOUNTER64 || dt == TR069DMType.DBM1000) {
try {
int deviceInt = Integer.parseInt(deviceValue);
if (param.getRange().getMin() != null && deviceInt < param.getRange().getMin())
return "Number too low";
if (param.getRange().getMax() != null && deviceInt > param.getRange().getMax())
return "Number too high";
} catch (NumberFormatException nfe) {
// We might very well try to parse a long, unsigned int, unsignedlong or statscounter64 - but this may fail
// from time to time - the range object does not support more than Integer-range anyway, so we ignore this
}
// if (!param.hasSpecificRange()) {
// try {
// long deviceLong = Long.parseLong(deviceValue);
// if (deviceLong < dt.getMin() || deviceLong > dt.getMax()) // Default range
// return "Number outside range";
// } catch (NumberFormatException nfe) {
// // Even a Long might not be big enough for a unisignedLong and statscounter64
// }
// }
}
return "OK";
}
/**
* Verify response from device according to the old, 1.generation test-system.
* The verification is based on whether or not a test signals a CWMP fault or
* not. For the kill-test is slightly different.
* @param reqRes
*/
private static void verifyResponseOLD(HTTPReqResData reqRes) {
String xml = reqRes.getRequest().getXml();
String unitId = reqRes.getSessionData().getUnitId();
String row = TestDatabase.database.select(unitId);
TestDatabaseObject tdo = new TestDatabaseObject(row);
if (tdo.getTestType().equals("Kill"))
verifyKillResponse(xml, unitId);
else {
try {
FileWriter fw = new FileWriter("tests/results/" + unitId + "-" + tdo.getStep());
fw.write(xml);
fw.close();
} catch (IOException e) {
Log.warn(HTTPRequestProcessor.class, "Failed to write to result file: " + e);
}
if (xml.indexOf("Fault") == -1 || ((tdo.getStep().equals("7.5.4.txt") || tdo.getStep().equals("7.7.3.txt")) && xml.indexOf("Fault") > -1)) {
tdo.addOk(tdo.getStep());
if (tdo.getTestType().equalsIgnoreCase("Auto")) {
String[] files = (new File("tests")).list();
Arrays.sort(files, new NaturalComparator());
boolean match = false;
boolean nextStepFound = false;
for (String file : files) {
if (match && !(new File(file).isDirectory())) {
tdo.setStep(file);
nextStepFound = true;
break;
}
if (tdo.getStep().equals(file)) {
match = true;
}
}
if (!nextStepFound) {
tdo.setRun("false");
}
}
try {
TestDatabase.database.insert(unitId, tdo.toString());
} catch (IOException e) {
Log.warn(HTTPRequestProcessor.class, "Failed to write to test database: " + e);
}
}
}
}
/**
* Process the request from the CPE. For some CPE-requests processing is simple (do-nothing), for
* others it may involve more logic. Keep in mind that a HTTP-request from the CPE, might actually
* be a TR-69 response!
*
* If in Testmode, then the request is processed and validated in an entirely different way than
* normal provisioning!
* @param reqRes
* @throws TR069Exception
*/
public static void processRequest(HTTPReqResData reqRes) throws TR069Exception {
try {
String requestMethodName = extractMethodName(reqRes.getRequest().getXml());
if (requestMethodName == null)
requestMethodName = TR069Method.EMPTY;
reqRes.getRequest().setMethod(requestMethodName);
if (reqRes.getSessionData().isTestMode()) {
Log.debug(HTTPRequestProcessor.class, "Will verify response according to test setup");
if (Util.testEnabled(reqRes, false)) {
verifyResponseNEW(requestMethodName, reqRes);
} else {
if (reqRes.getRequest().getXml().length() > 1) {
// TR069 Plugfest/Kill Test
verifyResponseOLD(reqRes);
}
requestMethodName = TR069Method.EMPTY;
reqRes.getRequest().setMethod(requestMethodName);
}
} else {
Log.debug(HTTPRequestProcessor.class, "Will process method " + requestMethodName + " (incoming request/response from CPE)");
HTTPRequestAction reqAction = TR069Method.requestMap.get(requestMethodName);
if (reqAction != null) {
reqRes.getRequest().setXml(XMLFormatter.filter(reqRes.getRequest().getXml()));
reqAction.getProcessRequestMethod().invoke(null, reqRes);
} else {
throw new UnknownMethodException(requestMethodName);
}
}
} catch (Throwable t) {
if (t instanceof InvocationTargetException && t.getCause() != null)
t = t.getCause();
if (t instanceof TR069Exception)
throw (TR069Exception) t;
if (t instanceof NoDataAvailableException)
throw new TR069Exception("Device was not found in database - can only provision device if in server is in discovery mode and device supports basic authentication",
TR069ExceptionShortMessage.NODATA);
else
throw new TR069Exception("Could not process HTTP-request (from TR-069 client)", TR069ExceptionShortMessage.MISC, t);
} finally {
if (reqRes.getRequest().getMethod() == null) {
reqRes.getRequest().setMethod(TR069Method.EMPTY);
reqRes.getRequest().setXml("");
}
if (Log.isConversationLogEnabled()) {
String unitId = reqRes.getSessionData().getUnitId();
if (unitId != null)
Context.put(Context.X, unitId, BaseCache.SESSIONDATA_CACHE_TIMEOUT);
String xml = reqRes.getRequest().getXml();
if (Properties.isPrettyPrintQuirk(reqRes.getSessionData()))
xml = XMLFormatter.prettyprint(reqRes.getRequest().getXml());
Log.conversation(reqRes.getSessionData(), "============== FROM CPE ===============\n" + xml);
}
}
}
/**
* Fastest way to extract the method name without actually parsing the XML - the method name is crucial to
* the next steps in TR-069 processing
*
* The TR-069 Method is found after the first "<cwmp:" after ":Body"
* T
*
* @param reqStr (TR-069 XML)
* @return TR-069 methodName
*/
private static String extractMethodName(String reqStr) {
int soapenvBodyPos = reqStr.indexOf(":Body");
// if (soapenvBodyPos == -1)
// soapenvBodyPos = reqStr.indexOf("<soap:Body");
// if (soapenvBodyPos == -1)
// soapenvBodyPos = reqStr.indexOf("<SOAP-ENV:Body");
// if (soapenvBodyPos == -1)
// soapenvBodyPos = reqStr.indexOf("<soap_env:Body");
// if (soapenvBodyPos == -1)
// soapenvBodyPos = reqStr.indexOf("<soap-env:Body");
if (soapenvBodyPos > -1) {
int cwmpPos = reqStr.indexOf("<cwmp:", soapenvBodyPos);
int methodPos = cwmpPos + 6;
int cwmpEndBracketPos = reqStr.indexOf(">", methodPos);
int cwmpEndSpacePos = reqStr.indexOf(" ", methodPos);
int cwmpEndPos = cwmpEndBracketPos;
// String reqStrPart = "";
if (cwmpPos > -1) {
int partEndPos = cwmpPos + 200;
if (partEndPos > reqStr.length()) {
partEndPos = reqStr.length();
}
// reqStrPart = reqStr.substring(cwmpPos, partEndPos);
}
// Log.debug(HTTPRequestProcessor.class, "\n" + reqStrPart + "\nsbp:" + soapenvBodyPos + ", cp:" + cwmpPos + ", cebp:" + cwmpEndBracketPos + ", cesp:" + cwmpEndSpacePos + ", cep:"
// + cwmpEndPos);
if (cwmpEndSpacePos > -1 && cwmpEndSpacePos < cwmpEndPos)
cwmpEndPos = cwmpEndSpacePos;
String methodStr = reqStr.substring(methodPos, cwmpEndPos);
if (methodStr.equals("GetRPCMethodsResponse"))
return methodStr;
if (methodStr.endsWith("Response"))
methodStr = methodStr.substring(0, methodStr.length() - 8);
return methodStr;
} else {
return null;
}
}
}