--- /dev/null
+/*\r
+ * ============LICENSE_START========================================================================\r
+ * ONAP : tr-069-adapter\r
+ * =================================================================================================\r
+ * Copyright (C) 2020 CommScope Inc Intellectual Property.\r
+ * =================================================================================================\r
+ * This tr-069-adapter software file is distributed by CommScope Inc under the Apache License,\r
+ * Version 2.0 (the "License"); you may not use this file except in compliance with the License. You\r
+ * may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,\r
+ * either express or implied. See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ * ===============LICENSE_END=======================================================================\r
+ */\r
+\r
+package org.commscope.tr069adapter.acs.cpe;\r
+\r
+import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.ACS_SESSIONID;\r
+import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.CWMP_VERSION;\r
+import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.INFORM;\r
+import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.TRANSFER_COMPLETE;\r
+\r
+import java.io.ByteArrayOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.io.InputStreamReader;\r
+import java.io.OutputStream;\r
+import java.io.OutputStreamWriter;\r
+import java.io.PipedInputStream;\r
+import java.io.PipedOutputStream;\r
+import java.io.Reader;\r
+import java.io.UnsupportedEncodingException;\r
+import java.io.Writer;\r
+import java.net.URLDecoder;\r
+import java.nio.charset.StandardCharsets;\r
+import java.util.Enumeration;\r
+\r
+import javax.servlet.http.Cookie;\r
+import javax.servlet.http.HttpServletRequest;\r
+import javax.servlet.http.HttpServletResponse;\r
+import javax.ws.rs.core.Context;\r
+import javax.xml.soap.MessageFactory;\r
+import javax.xml.soap.MimeHeaders;\r
+import javax.xml.soap.SOAPException;\r
+import javax.xml.soap.SOAPMessage;\r
+\r
+import org.commscope.tr069adapter.acs.common.exception.TR069EventProcessingException;\r
+import org.commscope.tr069adapter.acs.common.response.DeviceInformResponse;\r
+import org.commscope.tr069adapter.acs.common.utils.ErrorCode;\r
+import org.commscope.tr069adapter.acs.cpe.handler.DeviceEventHandler;\r
+import org.commscope.tr069adapter.acs.cpe.rpc.Fault;\r
+import org.commscope.tr069adapter.acs.cpe.rpc.Inform;\r
+import org.commscope.tr069adapter.acs.cpe.rpc.InformResponse;\r
+import org.commscope.tr069adapter.acs.cpe.rpc.TransferComplete;\r
+import org.commscope.tr069adapter.acs.cpe.rpc.TransferCompleteResponse;\r
+import org.slf4j.Logger;\r
+import org.slf4j.LoggerFactory;\r
+import org.springframework.beans.factory.annotation.Autowired;\r
+import org.springframework.web.bind.annotation.PostMapping;\r
+import org.springframework.web.bind.annotation.RequestMapping;\r
+import org.springframework.web.bind.annotation.RestController;\r
+\r
+@RestController\r
+@RequestMapping("/CPEMgmt")\r
+public class CPEManagementService {\r
+\r
+ private static final String UTF_8 = "UTF-8";\r
+\r
+ private static final Logger logger = LoggerFactory.getLogger(CPEManagementService.class);\r
+\r
+ private static final int MY_MAX_ENVELOPES = 1;\r
+\r
+ @Autowired\r
+ DeviceEventHandler deviceEventHandler;\r
+\r
+ /**\r
+ * @param request\r
+ * @param response\r
+ */\r
+ @SuppressWarnings("static-access")\r
+ @PostMapping("/acs")\r
+ public void processDeviceEvent(@Context HttpServletRequest request,\r
+ @Context HttpServletResponse response) {\r
+\r
+ logger.debug("A device event occurred");\r
+ logHeaderElements(request.getHeaderNames());\r
+\r
+ try {\r
+ Boolean isEmptyCPERequest = true;\r
+ SOAPMessage soapMsg = null;\r
+ ByteArrayOutputStream out = new ByteArrayOutputStream();\r
+\r
+ String ct = request.getContentType();\r
+ int csix = -1;\r
+ String csFrom = "ISO-8859-1";\r
+ if (ct != null) {\r
+ csix = ct.indexOf("charset=");\r
+ response.setContentType(ct);\r
+ } else {\r
+ response.setContentType("text/xml;charset=UTF-8");\r
+ }\r
+\r
+ if (csix != -1)\r
+ csFrom = ct.substring(csix + 8).replaceAll("\"", "");\r
+\r
+ Cookie[] cookies = request.getCookies();\r
+ String acsSessionID = getACSSessionCookieData(cookies);\r
+ String cwmpVersion = getCWMPVersionCookieData(cookies);\r
+\r
+ XmlFilterInputStream f =\r
+ new XmlFilterInputStream(request.getInputStream(), request.getContentLength());\r
+ MessageFactory mf = getSOAPMessageFactory();\r
+ while (f.next()) {\r
+ isEmptyCPERequest = false;\r
+ MimeHeaders hdrs = new MimeHeaders();\r
+ hdrs.setHeader("Content-Type", "text/xml; charset=UTF-8");\r
+ InputStream in = getInputStream(csFrom, f);\r
+ soapMsg = mf.createMessage(hdrs, in);\r
+\r
+ logSoapMsg(soapMsg);\r
+\r
+ TR069RPC msg = null;\r
+ msg = TR069RPC.parse(soapMsg);\r
+\r
+ String reqType = getRequestType(msg);\r
+ logger.info("Event notified by the device is of type: {}", reqType);\r
+\r
+ if (reqType != null) {\r
+ if (reqType.equals(INFORM)) {\r
+ processDeviceInform(msg, request, response, out);\r
+ } else if (reqType.equals(TRANSFER_COMPLETE)) {\r
+ processTransferComplete(msg, response, out);\r
+ } else {\r
+ processOperationResult(msg, response, reqType, acsSessionID, out);\r
+ }\r
+ }\r
+ }\r
+\r
+ if (isEmptyCPERequest.booleanValue()) {\r
+ processEmptyCPERequest(response, cwmpVersion, acsSessionID, out);\r
+ }\r
+\r
+ if (out.size() < 1) {// To delete dm_sessionId cookie\r
+ clearCookies(cookies, response);\r
+ }\r
+\r
+ response.setContentLength(out.size());\r
+ response.setHeader("SOAPAction", "");\r
+ String sout = out.toString().trim();\r
+ logger.info(sout);\r
+ response.getOutputStream().print(sout);\r
+ response.getOutputStream().flush();\r
+ logger.debug("End of processing");\r
+\r
+ } catch (Exception e) {\r
+ logger.error("An error occurred while processing device event, Exception: {}",\r
+ e.getMessage());\r
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
+ }\r
+\r
+ logger.debug("End of processing the HTTP post request");\r
+ }\r
+\r
+ private InputStream getInputStream(String csFrom, XmlFilterInputStream f) throws IOException {\r
+ InputStream in = null;\r
+ if (csFrom.equalsIgnoreCase(UTF_8)) {\r
+ in = new XmlFilterNS(f);\r
+ } else {\r
+ in = new CharsetConverterInputStream(csFrom, UTF_8, new XmlFilterNS(f));\r
+ }\r
+ return in;\r
+ }\r
+\r
+ private void processDeviceInform(TR069RPC msg, HttpServletRequest request,\r
+ HttpServletResponse response, ByteArrayOutputStream out) throws IOException {\r
+ Inform inform = (Inform) msg;\r
+ DeviceInformResponse deviceInformResponse = null;\r
+ try {\r
+ deviceInformResponse =\r
+ deviceEventHandler.processDeviceInform(inform, request.getHeader("Authorization"));\r
+ Cookie cookie = new Cookie(ACS_SESSIONID, deviceInformResponse.getSessionId());\r
+ Cookie cwmpVerCookie = new Cookie(CWMP_VERSION, msg.getCWMPVersion());\r
+ response.addCookie(cookie);\r
+ response.addCookie(cwmpVerCookie);\r
+ } catch (TR069EventProcessingException tr069ex) {\r
+ ErrorCode errorCode = tr069ex.getErrorCode();\r
+ if (ErrorCode.OUI_OR_PC_MISMATCH.equals(errorCode)) {\r
+ sendFault(response, out, Fault.FCODE_ACS_REQUEST_DENIED, "OUIandProductClassNotValid",\r
+ inform.getId());\r
+ } else {\r
+ int httpStatusCode = deviceEventHandler.handleException(tr069ex);\r
+ response.setStatus(httpStatusCode);\r
+ }\r
+ int httpStatusCode = deviceEventHandler.handleException(tr069ex);\r
+ response.setStatus(httpStatusCode);\r
+ }\r
+\r
+ InformResponse resp = new InformResponse(inform.getId(), MY_MAX_ENVELOPES);\r
+ resp.setCWMPVersion(msg.getCWMPVersion());\r
+ resp.writeTo(out);\r
+ }\r
+\r
+ private void processTransferComplete(TR069RPC msg, HttpServletResponse response,\r
+ ByteArrayOutputStream out) {\r
+ TransferComplete tc = (TransferComplete) msg;\r
+ try {\r
+ DeviceInformResponse deviceInformResponse = deviceEventHandler.processTransferComplete(tc);\r
+ Cookie cookie = new Cookie(ACS_SESSIONID, deviceInformResponse.getSessionId());\r
+ Cookie cwmpVerCookie = new Cookie(CWMP_VERSION, msg.getCWMPVersion() + ";");\r
+ response.addCookie(cookie);\r
+ response.addCookie(cwmpVerCookie);\r
+ } catch (Exception e) {\r
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
+ return;\r
+ }\r
+ TransferCompleteResponse tr = new TransferCompleteResponse(tc.getId());\r
+ tr.setCWMPVersion(msg.getCWMPVersion());\r
+ tr.writeTo(out);\r
+ }\r
+\r
+ private void processOperationResult(TR069RPC msg, HttpServletResponse response, String reqType,\r
+ String acsSessionID, ByteArrayOutputStream out) {\r
+ logger.debug("Received Operation Result response {}", msg);\r
+ if (null == acsSessionID) {\r
+ logger.error("Received response without session ID, response: {}", reqType);\r
+ } else {\r
+ try {\r
+ TR069RPC message = deviceEventHandler.processRPCResponse(msg, acsSessionID);\r
+ if (null != message) {\r
+ message.setCWMPVersion(msg.getCWMPVersion());\r
+ message.writeTo(out);\r
+ } else {\r
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);\r
+ }\r
+ } catch (TR069EventProcessingException tr069ex) {\r
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
+ }\r
+ }\r
+ }\r
+\r
+ private void processEmptyCPERequest(HttpServletResponse response, String cwmpVersion,\r
+ String acsSessionID, ByteArrayOutputStream out) {\r
+ if (null == acsSessionID) {\r
+ logger.error("Received empty response without session ID");\r
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
+ return;\r
+ }\r
+\r
+ try {\r
+ logger.info("Received Empty Device response");\r
+ TR069RPC message = deviceEventHandler.processEmptyRequest(acsSessionID);\r
+ if (null != message) {\r
+ message.setCWMPVersion(cwmpVersion);\r
+ message.writeTo(out);\r
+ } else {\r
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);\r
+ }\r
+ } catch (TR069EventProcessingException tr069ex) {\r
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
+ }\r
+ }\r
+\r
+ private void clearCookies(Cookie[] cookies, HttpServletResponse response) {\r
+ Cookie cookieToDelete = null;\r
+ if (null != cookies) {\r
+ logger.debug("Clearing the cookies");\r
+ for (int i = 0; i < cookies.length; i++) {\r
+ if (cookies[i].getName().equals(ACS_SESSIONID)\r
+ || cookies[i].getName().equals(CWMP_VERSION)) {\r
+ cookieToDelete = cookies[i];\r
+ cookieToDelete.setMaxAge(0);\r
+ response.addCookie(cookieToDelete);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ private static class XmlFilterInputStream extends InputStream {\r
+\r
+ private InputStream istream;\r
+ private int lvl;\r
+ private int lastchar;\r
+ @SuppressWarnings("unused")\r
+ private int len;\r
+ private int nextchar;\r
+ private boolean intag = false;\r
+ private StringBuilder buff = new StringBuilder(16);\r
+\r
+ /** Creates a new instance of xmlFilterInputStream */\r
+ public XmlFilterInputStream(InputStream is, int l) {\r
+ len = l;\r
+ istream = is;\r
+ }\r
+\r
+ @Override\r
+ public int read() throws IOException {\r
+ if (lastchar == '>' && lvl == 0) {\r
+ return -1;\r
+ }\r
+ int l = lastchar;\r
+ if (!readLastChar())\r
+ return lastchar;\r
+\r
+ if (!intag && lastchar == '&') {\r
+ int amppos = buff.length();\r
+ updateBuffer();\r
+ String s = buff.substring(amppos);\r
+ replaceSpecialChars(s, amppos);\r
+ return read();\r
+ }\r
+\r
+ if (l == '<') {\r
+ intag = true;\r
+ if (lastchar == '/') {\r
+ lvl--;\r
+ } else {\r
+ lvl++;\r
+ }\r
+ }\r
+\r
+ len--;\r
+ return lastchar;\r
+ }\r
+\r
+ public boolean next() throws IOException {\r
+ if ((nextchar = istream.read()) == -1) {\r
+ logger.debug("Next char is {}", nextchar);\r
+ lvl = 0;\r
+ lastchar = 0;\r
+ }\r
+ return (nextchar != -1);\r
+ }\r
+\r
+ private boolean readLastChar() throws IOException {\r
+ if (nextchar != -1) {\r
+ lastchar = nextchar;\r
+ nextchar = -1;\r
+ } else {\r
+ if (buff.length() > 0) {\r
+ lastchar = buff.charAt(0);\r
+ buff.deleteCharAt(0);\r
+ return false;\r
+ } else {\r
+ lastchar = istream.read();\r
+ }\r
+ }\r
+\r
+ if (lastchar == '<') {\r
+ intag = true;\r
+ } else if (lastchar == '>') {\r
+ intag = false;\r
+ }\r
+\r
+ return true;\r
+ }\r
+\r
+ private void updateBuffer() throws IOException {\r
+ // fix up broken xml not encoding &\r
+ buff.append((char) lastchar);\r
+ for (int c = 0; c < 10; c++) {\r
+ int ch = istream.read();\r
+ boolean breakLoop = false;\r
+ if (ch == -1) {\r
+ breakLoop = true;\r
+ }\r
+ if (ch == '&') {\r
+ nextchar = ch;\r
+ breakLoop = true;\r
+ }\r
+ if (breakLoop)\r
+ break;\r
+\r
+ buff.append((char) ch);\r
+ }\r
+ }\r
+\r
+ private void replaceSpecialChars(String s, int amppos) {\r
+ if (!s.startsWith("&") && !s.startsWith("<") && !s.startsWith(">")\r
+ && !s.startsWith("'") && !s.startsWith(""") && !s.startsWith("&#")) {\r
+ buff.replace(amppos, amppos + 1, "&");\r
+ }\r
+ }\r
+ }\r
+\r
+ private static class XmlFilterNS extends InputStream {\r
+ // Dumb class to filter out declaration of default xmlns\r
+\r
+ private String pat = "xmlns=\"urn:dslforum-org:cwmp-1-0\"";\r
+ private String pat2 = "xmlns=\"urn:dslforum-org:cwmp-1-1\"";\r
+ private int length = 0;\r
+ private int pos = 0;\r
+ private boolean f = false;\r
+ private byte[] buff = new byte[1024];\r
+ private InputStream is;\r
+\r
+ @Override\r
+ public int read() throws IOException {\r
+ if (!f) {\r
+ length = is.read(buff);\r
+ if (length < buff.length) {\r
+ byte[] b2 = new byte[length];\r
+ System.arraycopy(buff, 0, b2, 0, length);\r
+ buff = b2;\r
+ }\r
+\r
+ String b = new String(buff, StandardCharsets.UTF_8);\r
+ b = b.replace(pat, "");\r
+ b = b.replace(pat2, "");\r
+ buff = b.getBytes(StandardCharsets.UTF_8);\r
+ length = buff.length;\r
+ f = true;\r
+ }\r
+\r
+ if (pos < length) {\r
+ return buff[pos++] & 0xFF;\r
+ }\r
+ return is.read();\r
+ }\r
+\r
+ public XmlFilterNS(InputStream is) {\r
+ this.is = is;\r
+ }\r
+ }\r
+\r
+ private static class CharsetConverterInputStream extends InputStream {\r
+\r
+ @SuppressWarnings("unused")\r
+ private InputStream in;\r
+ private PipedInputStream pipein;\r
+ private OutputStream pipeout;\r
+ private Reader r;\r
+ private Writer w;\r
+\r
+ public CharsetConverterInputStream(String csFrom, String csTo, InputStream in)\r
+ throws IOException {\r
+ this.in = in;\r
+ r = new InputStreamReader(in, csFrom);\r
+ pipein = new PipedInputStream();\r
+ pipeout = new PipedOutputStream(pipein);\r
+ w = new OutputStreamWriter(pipeout, csTo);\r
+ }\r
+\r
+ @Override\r
+ public int read() throws IOException {\r
+ if (pipein.available() > 0) {\r
+ return pipein.read();\r
+ }\r
+ int c = r.read();\r
+ if (c == -1) {\r
+ return -1;\r
+ }\r
+ w.write(c);\r
+ w.flush();\r
+ return pipein.read();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * @return\r
+ * @throws Exception\r
+ */\r
+ private MessageFactory getSOAPMessageFactory() throws SOAPException {\r
+ MessageFactory mf = null;\r
+ mf = MessageFactory.newInstance();\r
+ return mf;\r
+ }\r
+\r
+ /**\r
+ * @param cookies\r
+ * @return\r
+ */\r
+ private String getACSSessionCookieData(Cookie[] cookies) {\r
+ String acsSessionID = null;\r
+ if (null != cookies) {\r
+ for (Cookie cookie : cookies) {\r
+ if (cookie.getName().equals(ACS_SESSIONID)) {\r
+ acsSessionID = cookie.getValue();\r
+ logger.debug("The session id is {}", acsSessionID);\r
+ }\r
+ }\r
+ }\r
+ return acsSessionID;\r
+ }\r
+\r
+ /**\r
+ * @param cookies\r
+ * @return\r
+ * @throws TR069EventProcessingException\r
+ */\r
+ private String getCWMPVersionCookieData(Cookie[] cookies) throws TR069EventProcessingException {\r
+ String cwmpVersion = null;\r
+ try {\r
+ if (null != cookies) {\r
+ for (Cookie cookie : cookies) {\r
+ if (cookie.getName().equals(CWMP_VERSION)) {\r
+ cwmpVersion = cookie.getValue();\r
+ if (cwmpVersion != null) {\r
+ cwmpVersion = URLDecoder.decode(cwmpVersion, StandardCharsets.UTF_8.name());\r
+ }\r
+ logger.debug("The CWMP version supported by the device is: {}", cwmpVersion);\r
+ }\r
+ }\r
+ }\r
+ } catch (UnsupportedEncodingException e) {\r
+ logger.error(e.getMessage());\r
+ TR069EventProcessingException ex = new TR069EventProcessingException(\r
+ ErrorCode.UNSUPPORTED_CHARACTER_ENCODING, StandardCharsets.UTF_8.name());\r
+ logger.error(ex.getMessage());\r
+ throw ex;\r
+ }\r
+ return cwmpVersion;\r
+ }\r
+\r
+ /**\r
+ * @param soapMsg\r
+ */\r
+ private void logSoapMsg(SOAPMessage soapMsg) {\r
+ StringBuilder buffer = new StringBuilder();\r
+ buffer.append(soapMsg.toString());\r
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();\r
+ try {\r
+ soapMsg.writeTo(baos);\r
+ } catch (SOAPException | IOException e) {\r
+ logger.error("Error while writting soap message");\r
+ }\r
+ buffer.append(baos);\r
+ String soapMessage = buffer.toString();\r
+ logger.debug(soapMessage);\r
+ }\r
+\r
+ /**\r
+ * @param response\r
+ * @param out\r
+ * @param fcodeAcsRequestDenied\r
+ * @param faultString\r
+ * @param id\r
+ * @throws IOException\r
+ */\r
+ private void sendFault(HttpServletResponse response, ByteArrayOutputStream out,\r
+ String fcodeAcsRequestDenied, String faultString, String id) throws IOException {\r
+ Fault fault = new Fault(fcodeAcsRequestDenied, faultString, id);\r
+ fault.writeTo(out);\r
+ response.setContentLength(out.size());\r
+ String sout = out.toString(UTF_8);\r
+ sout = sout.replace('\'', '"');\r
+ response.getOutputStream().print(sout);\r
+ }\r
+\r
+ private void logHeaderElements(Enumeration<String> headerName) {\r
+ while (headerName.hasMoreElements()) {\r
+ String requestHeader = headerName.nextElement();\r
+ logger.debug("Request Headers {}", requestHeader);\r
+ }\r
+ }\r
+\r
+ private String getRequestType(TR069RPC msg) {\r
+ String requestType = msg.getName();\r
+ if (requestType == null)\r
+ requestType = "Empty Request";\r
+\r
+ return requestType;\r
+ }\r
+\r
+ /******************************************************************************************************************/\r
+\r
+ public DeviceEventHandler getDeviceEventHandler() {\r
+ return deviceEventHandler;\r
+ }\r
+\r
+ public void setDeviceEventHandler(DeviceEventHandler deviceEventHandler) {\r
+ this.deviceEventHandler = deviceEventHandler;\r
+ }\r
+\r
+}\r