6f07e366c1d14d492f26972895afde0389c8e36b
[oam/tr069-adapter.git] / acs / cpe / src / main / java / org / commscope / tr069adapter / acs / cpe / CPEManagementService.java
1 /*\r
2  * ============LICENSE_START========================================================================\r
3  * ONAP : tr-069-adapter\r
4  * =================================================================================================\r
5  * Copyright (C) 2020 CommScope Inc Intellectual Property.\r
6  * =================================================================================================\r
7  * This tr-069-adapter software file is distributed by CommScope Inc under the Apache License,\r
8  * Version 2.0 (the "License"); you may not use this file except in compliance with the License. You\r
9  * may obtain a copy of the License at\r
10  *\r
11  * http://www.apache.org/licenses/LICENSE-2.0\r
12  *\r
13  * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,\r
14  * either express or implied. See the License for the specific language governing permissions and\r
15  * limitations under the License.\r
16  * ===============LICENSE_END=======================================================================\r
17  */\r
18 \r
19 package org.commscope.tr069adapter.acs.cpe;\r
20 \r
21 import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.ACS_SESSIONID;\r
22 import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.CWMP_VERSION;\r
23 import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.INFORM;\r
24 import static org.commscope.tr069adapter.acs.common.utils.AcsConstants.TRANSFER_COMPLETE;\r
25 \r
26 import java.io.ByteArrayOutputStream;\r
27 import java.io.IOException;\r
28 import java.io.InputStream;\r
29 import java.io.InputStreamReader;\r
30 import java.io.OutputStream;\r
31 import java.io.OutputStreamWriter;\r
32 import java.io.PipedInputStream;\r
33 import java.io.PipedOutputStream;\r
34 import java.io.Reader;\r
35 import java.io.UnsupportedEncodingException;\r
36 import java.io.Writer;\r
37 import java.net.URLDecoder;\r
38 import java.nio.charset.StandardCharsets;\r
39 import java.util.Enumeration;\r
40 \r
41 import javax.servlet.http.Cookie;\r
42 import javax.servlet.http.HttpServletRequest;\r
43 import javax.servlet.http.HttpServletResponse;\r
44 import javax.ws.rs.core.Context;\r
45 import javax.xml.soap.MessageFactory;\r
46 import javax.xml.soap.MimeHeaders;\r
47 import javax.xml.soap.SOAPException;\r
48 import javax.xml.soap.SOAPMessage;\r
49 \r
50 import org.commscope.tr069adapter.acs.common.exception.TR069EventProcessingException;\r
51 import org.commscope.tr069adapter.acs.common.response.DeviceInformResponse;\r
52 import org.commscope.tr069adapter.acs.common.utils.ErrorCode;\r
53 import org.commscope.tr069adapter.acs.cpe.handler.DeviceEventHandler;\r
54 import org.commscope.tr069adapter.acs.cpe.rpc.Fault;\r
55 import org.commscope.tr069adapter.acs.cpe.rpc.Inform;\r
56 import org.commscope.tr069adapter.acs.cpe.rpc.InformResponse;\r
57 import org.commscope.tr069adapter.acs.cpe.rpc.TransferComplete;\r
58 import org.commscope.tr069adapter.acs.cpe.rpc.TransferCompleteResponse;\r
59 import org.slf4j.Logger;\r
60 import org.slf4j.LoggerFactory;\r
61 import org.springframework.beans.factory.annotation.Autowired;\r
62 import org.springframework.web.bind.annotation.PostMapping;\r
63 import org.springframework.web.bind.annotation.RequestMapping;\r
64 import org.springframework.web.bind.annotation.RestController;\r
65 \r
66 @RestController\r
67 @RequestMapping("/CPEMgmt")\r
68 public class CPEManagementService {\r
69 \r
70   private static final String UTF_8 = "UTF-8";\r
71 \r
72   private static final Logger logger = LoggerFactory.getLogger(CPEManagementService.class);\r
73 \r
74   private static final int MY_MAX_ENVELOPES = 1;\r
75 \r
76   @Autowired\r
77   DeviceEventHandler deviceEventHandler;\r
78 \r
79   /**\r
80    * @param request\r
81    * @param response\r
82    */\r
83   @SuppressWarnings("static-access")\r
84   @PostMapping("/acs")\r
85   public void processDeviceEvent(@Context HttpServletRequest request,\r
86       @Context HttpServletResponse response) {\r
87 \r
88     logger.debug("A device event occurred");\r
89     logHeaderElements(request.getHeaderNames());\r
90 \r
91     try {\r
92       Boolean isEmptyCPERequest = true;\r
93       SOAPMessage soapMsg = null;\r
94       ByteArrayOutputStream out = new ByteArrayOutputStream();\r
95 \r
96       String ct = request.getContentType();\r
97       int csix = -1;\r
98       String csFrom = "ISO-8859-1";\r
99       if (ct != null) {\r
100         csix = ct.indexOf("charset=");\r
101         response.setContentType(ct);\r
102       } else {\r
103         response.setContentType("text/xml;charset=UTF-8");\r
104       }\r
105 \r
106       if (csix != -1)\r
107         csFrom = ct.substring(csix + 8).replaceAll("\"", "");\r
108 \r
109       Cookie[] cookies = request.getCookies();\r
110       String acsSessionID = getACSSessionCookieData(cookies);\r
111       String cwmpVersion = getCWMPVersionCookieData(cookies);\r
112 \r
113       XmlFilterInputStream f =\r
114           new XmlFilterInputStream(request.getInputStream(), request.getContentLength());\r
115       MessageFactory mf = getSOAPMessageFactory();\r
116       while (f.next()) {\r
117         isEmptyCPERequest = false;\r
118         MimeHeaders hdrs = new MimeHeaders();\r
119         hdrs.setHeader("Content-Type", "text/xml; charset=UTF-8");\r
120         InputStream in = getInputStream(csFrom, f);\r
121         soapMsg = mf.createMessage(hdrs, in);\r
122 \r
123         logSoapMsg(soapMsg);\r
124 \r
125         TR069RPC msg = null;\r
126         msg = TR069RPC.parse(soapMsg);\r
127 \r
128         String reqType = getRequestType(msg);\r
129         logger.info("Event notified by the device is of type: {}", reqType);\r
130 \r
131         if (reqType != null) {\r
132           if (reqType.equals(INFORM)) {\r
133             processDeviceInform(msg, request, response, out);\r
134           } else if (reqType.equals(TRANSFER_COMPLETE)) {\r
135             processTransferComplete(msg, response, out);\r
136           } else {\r
137             processOperationResult(msg, response, reqType, acsSessionID, out);\r
138           }\r
139         }\r
140       }\r
141 \r
142       if (isEmptyCPERequest.booleanValue()) {\r
143         processEmptyCPERequest(response, cwmpVersion, acsSessionID, out);\r
144       }\r
145 \r
146       if (out.size() < 1) {// To delete dm_sessionId cookie\r
147         clearCookies(cookies, response);\r
148       }\r
149 \r
150       response.setContentLength(out.size());\r
151       response.setHeader("SOAPAction", "");\r
152       String sout = out.toString().trim();\r
153       logger.info(sout);\r
154       response.getOutputStream().print(sout);\r
155       response.getOutputStream().flush();\r
156       logger.debug("End of processing");\r
157 \r
158     } catch (Exception e) {\r
159       logger.error("An error occurred while processing device event, Exception: {}",\r
160           e.getMessage());\r
161       response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
162     }\r
163 \r
164     logger.debug("End of processing the HTTP post request");\r
165   }\r
166 \r
167   private InputStream getInputStream(String csFrom, XmlFilterInputStream f) throws IOException {\r
168     InputStream in = null;\r
169     if (csFrom.equalsIgnoreCase(UTF_8)) {\r
170       in = new XmlFilterNS(f);\r
171     } else {\r
172       in = new CharsetConverterInputStream(csFrom, UTF_8, new XmlFilterNS(f));\r
173     }\r
174     return in;\r
175   }\r
176 \r
177   private void processDeviceInform(TR069RPC msg, HttpServletRequest request,\r
178       HttpServletResponse response, ByteArrayOutputStream out) throws IOException {\r
179     Inform inform = (Inform) msg;\r
180     DeviceInformResponse deviceInformResponse = null;\r
181     try {\r
182       deviceInformResponse =\r
183           deviceEventHandler.processDeviceInform(inform, request.getHeader("Authorization"));\r
184       Cookie cookie = new Cookie(ACS_SESSIONID, deviceInformResponse.getSessionId());\r
185       Cookie cwmpVerCookie = new Cookie(CWMP_VERSION, msg.getCWMPVersion());\r
186       response.addCookie(cookie);\r
187       response.addCookie(cwmpVerCookie);\r
188     } catch (TR069EventProcessingException tr069ex) {\r
189       ErrorCode errorCode = tr069ex.getErrorCode();\r
190       if (ErrorCode.OUI_OR_PC_MISMATCH.equals(errorCode)) {\r
191         sendFault(response, out, Fault.FCODE_ACS_REQUEST_DENIED, "OUIandProductClassNotValid",\r
192             inform.getId());\r
193       } else {\r
194         int httpStatusCode = deviceEventHandler.handleException(tr069ex);\r
195         response.setStatus(httpStatusCode);\r
196       }\r
197       int httpStatusCode = deviceEventHandler.handleException(tr069ex);\r
198       response.setStatus(httpStatusCode);\r
199     }\r
200 \r
201     InformResponse resp = new InformResponse(inform.getId(), MY_MAX_ENVELOPES);\r
202     resp.setCWMPVersion(msg.getCWMPVersion());\r
203     resp.writeTo(out);\r
204   }\r
205 \r
206   private void processTransferComplete(TR069RPC msg, HttpServletResponse response,\r
207       ByteArrayOutputStream out) {\r
208     TransferComplete tc = (TransferComplete) msg;\r
209     try {\r
210       DeviceInformResponse deviceInformResponse = deviceEventHandler.processTransferComplete(tc);\r
211       Cookie cookie = new Cookie(ACS_SESSIONID, deviceInformResponse.getSessionId());\r
212       Cookie cwmpVerCookie = new Cookie(CWMP_VERSION, msg.getCWMPVersion() + ";");\r
213       response.addCookie(cookie);\r
214       response.addCookie(cwmpVerCookie);\r
215     } catch (Exception e) {\r
216       response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
217       return;\r
218     }\r
219     TransferCompleteResponse tr = new TransferCompleteResponse(tc.getId());\r
220     tr.setCWMPVersion(msg.getCWMPVersion());\r
221     tr.writeTo(out);\r
222   }\r
223 \r
224   private void processOperationResult(TR069RPC msg, HttpServletResponse response, String reqType,\r
225       String acsSessionID, ByteArrayOutputStream out) {\r
226     logger.debug("Received Operation Result response {}", msg);\r
227     if (null == acsSessionID) {\r
228       logger.error("Received response without session ID, response: {}", reqType);\r
229     } else {\r
230       try {\r
231         TR069RPC message = deviceEventHandler.processRPCResponse(msg, acsSessionID);\r
232         if (null != message) {\r
233           message.setCWMPVersion(msg.getCWMPVersion());\r
234           message.writeTo(out);\r
235         } else {\r
236           response.setStatus(HttpServletResponse.SC_NO_CONTENT);\r
237         }\r
238       } catch (TR069EventProcessingException tr069ex) {\r
239         response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
240       }\r
241     }\r
242   }\r
243 \r
244   private void processEmptyCPERequest(HttpServletResponse response, String cwmpVersion,\r
245       String acsSessionID, ByteArrayOutputStream out) {\r
246     if (null == acsSessionID) {\r
247       logger.error("Received empty response without session ID");\r
248       response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
249       return;\r
250     }\r
251 \r
252     try {\r
253       logger.info("Received Empty Device response");\r
254       TR069RPC message = deviceEventHandler.processEmptyRequest(acsSessionID);\r
255       if (null != message) {\r
256         message.setCWMPVersion(cwmpVersion);\r
257         message.writeTo(out);\r
258       } else {\r
259         response.setStatus(HttpServletResponse.SC_NO_CONTENT);\r
260       }\r
261     } catch (TR069EventProcessingException tr069ex) {\r
262       response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);\r
263     }\r
264   }\r
265 \r
266   private void clearCookies(Cookie[] cookies, HttpServletResponse response) {\r
267     Cookie cookieToDelete = null;\r
268     if (null != cookies) {\r
269       logger.debug("Clearing the cookies");\r
270       for (int i = 0; i < cookies.length; i++) {\r
271         if (cookies[i].getName().equals(ACS_SESSIONID)\r
272             || cookies[i].getName().equals(CWMP_VERSION)) {\r
273           cookieToDelete = cookies[i];\r
274           cookieToDelete.setMaxAge(0);\r
275           response.addCookie(cookieToDelete);\r
276         }\r
277       }\r
278     }\r
279   }\r
280 \r
281   private static class XmlFilterInputStream extends InputStream {\r
282 \r
283     private InputStream istream;\r
284     private int lvl;\r
285     private int lastchar;\r
286     @SuppressWarnings("unused")\r
287     private int len;\r
288     private int nextchar;\r
289     private boolean intag = false;\r
290     private StringBuilder buff = new StringBuilder(16);\r
291 \r
292     /** Creates a new instance of xmlFilterInputStream */\r
293     public XmlFilterInputStream(InputStream is, int l) {\r
294       len = l;\r
295       istream = is;\r
296     }\r
297 \r
298     @Override\r
299     public int read() throws IOException {\r
300       if (lastchar == '>' && lvl == 0) {\r
301         return -1;\r
302       }\r
303       int l = lastchar;\r
304       if (!readLastChar())\r
305         return lastchar;\r
306 \r
307       if (!intag && lastchar == '&') {\r
308         int amppos = buff.length();\r
309         updateBuffer();\r
310         String s = buff.substring(amppos);\r
311         replaceSpecialChars(s, amppos);\r
312         return read();\r
313       }\r
314 \r
315       if (l == '<') {\r
316         intag = true;\r
317         if (lastchar == '/') {\r
318           lvl--;\r
319         } else {\r
320           lvl++;\r
321         }\r
322       }\r
323 \r
324       len--;\r
325       return lastchar;\r
326     }\r
327 \r
328     public boolean next() throws IOException {\r
329       if ((nextchar = istream.read()) == -1) {\r
330         logger.debug("Next char is {}", nextchar);\r
331         lvl = 0;\r
332         lastchar = 0;\r
333       }\r
334       return (nextchar != -1);\r
335     }\r
336 \r
337     private boolean readLastChar() throws IOException {\r
338       if (nextchar != -1) {\r
339         lastchar = nextchar;\r
340         nextchar = -1;\r
341       } else {\r
342         if (buff.length() > 0) {\r
343           lastchar = buff.charAt(0);\r
344           buff.deleteCharAt(0);\r
345           return false;\r
346         } else {\r
347           lastchar = istream.read();\r
348         }\r
349       }\r
350 \r
351       if (lastchar == '<') {\r
352         intag = true;\r
353       } else if (lastchar == '>') {\r
354         intag = false;\r
355       }\r
356 \r
357       return true;\r
358     }\r
359 \r
360     private void updateBuffer() throws IOException {\r
361       // fix up broken xml not encoding &\r
362       buff.append((char) lastchar);\r
363       for (int c = 0; c < 10; c++) {\r
364         int ch = istream.read();\r
365         boolean breakLoop = false;\r
366         if (ch == -1) {\r
367           breakLoop = true;\r
368         }\r
369         if (ch == '&') {\r
370           nextchar = ch;\r
371           breakLoop = true;\r
372         }\r
373         if (breakLoop)\r
374           break;\r
375 \r
376         buff.append((char) ch);\r
377       }\r
378     }\r
379 \r
380     private void replaceSpecialChars(String s, int amppos) {\r
381       if (!s.startsWith("&amp;") && !s.startsWith("&lt;") && !s.startsWith("&gt;")\r
382           && !s.startsWith("&apos;") && !s.startsWith("&quot;") && !s.startsWith("&#")) {\r
383         buff.replace(amppos, amppos + 1, "&amp;");\r
384       }\r
385     }\r
386   }\r
387 \r
388   private static class XmlFilterNS extends InputStream {\r
389     // Dumb class to filter out declaration of default xmlns\r
390 \r
391     private String pat = "xmlns=\"urn:dslforum-org:cwmp-1-0\"";\r
392     private String pat2 = "xmlns=\"urn:dslforum-org:cwmp-1-1\"";\r
393     private int length = 0;\r
394     private int pos = 0;\r
395     private boolean f = false;\r
396     private byte[] buff = new byte[1024];\r
397     private InputStream is;\r
398 \r
399     @Override\r
400     public int read() throws IOException {\r
401       if (!f) {\r
402         length = is.read(buff);\r
403         if (length < buff.length) {\r
404           byte[] b2 = new byte[length];\r
405           System.arraycopy(buff, 0, b2, 0, length);\r
406           buff = b2;\r
407         }\r
408 \r
409         String b = new String(buff, StandardCharsets.UTF_8);\r
410         b = b.replace(pat, "");\r
411         b = b.replace(pat2, "");\r
412         buff = b.getBytes(StandardCharsets.UTF_8);\r
413         length = buff.length;\r
414         f = true;\r
415       }\r
416 \r
417       if (pos < length) {\r
418         return buff[pos++] & 0xFF;\r
419       }\r
420       return is.read();\r
421     }\r
422 \r
423     public XmlFilterNS(InputStream is) {\r
424       this.is = is;\r
425     }\r
426   }\r
427 \r
428   private static class CharsetConverterInputStream extends InputStream {\r
429 \r
430     @SuppressWarnings("unused")\r
431     private InputStream in;\r
432     private PipedInputStream pipein;\r
433     private OutputStream pipeout;\r
434     private Reader r;\r
435     private Writer w;\r
436 \r
437     public CharsetConverterInputStream(String csFrom, String csTo, InputStream in)\r
438         throws IOException {\r
439       this.in = in;\r
440       r = new InputStreamReader(in, csFrom);\r
441       pipein = new PipedInputStream();\r
442       pipeout = new PipedOutputStream(pipein);\r
443       w = new OutputStreamWriter(pipeout, csTo);\r
444     }\r
445 \r
446     @Override\r
447     public int read() throws IOException {\r
448       if (pipein.available() > 0) {\r
449         return pipein.read();\r
450       }\r
451       int c = r.read();\r
452       if (c == -1) {\r
453         return -1;\r
454       }\r
455       w.write(c);\r
456       w.flush();\r
457       return pipein.read();\r
458     }\r
459   }\r
460 \r
461   /**\r
462    * @return\r
463    * @throws Exception\r
464    */\r
465   private MessageFactory getSOAPMessageFactory() throws SOAPException {\r
466     MessageFactory mf = null;\r
467     mf = MessageFactory.newInstance();\r
468     return mf;\r
469   }\r
470 \r
471   /**\r
472    * @param cookies\r
473    * @return\r
474    */\r
475   private String getACSSessionCookieData(Cookie[] cookies) {\r
476     String acsSessionID = null;\r
477     if (null != cookies) {\r
478       for (Cookie cookie : cookies) {\r
479         if (cookie.getName().equals(ACS_SESSIONID)) {\r
480           acsSessionID = cookie.getValue();\r
481           logger.debug("The session id is {}", acsSessionID);\r
482         }\r
483       }\r
484     }\r
485     return acsSessionID;\r
486   }\r
487 \r
488   /**\r
489    * @param cookies\r
490    * @return\r
491    * @throws TR069EventProcessingException\r
492    */\r
493   private String getCWMPVersionCookieData(Cookie[] cookies) throws TR069EventProcessingException {\r
494     String cwmpVersion = null;\r
495     try {\r
496       if (null != cookies) {\r
497         for (Cookie cookie : cookies) {\r
498           if (cookie.getName().equals(CWMP_VERSION)) {\r
499             cwmpVersion = cookie.getValue();\r
500             if (cwmpVersion != null) {\r
501               cwmpVersion = URLDecoder.decode(cwmpVersion, StandardCharsets.UTF_8.name());\r
502             }\r
503             logger.debug("The CWMP version supported by the device is: {}", cwmpVersion);\r
504           }\r
505         }\r
506       }\r
507     } catch (UnsupportedEncodingException e) {\r
508       logger.error(e.getMessage());\r
509       TR069EventProcessingException ex = new TR069EventProcessingException(\r
510           ErrorCode.UNSUPPORTED_CHARACTER_ENCODING, StandardCharsets.UTF_8.name());\r
511       logger.error(ex.getMessage());\r
512       throw ex;\r
513     }\r
514     return cwmpVersion;\r
515   }\r
516 \r
517   /**\r
518    * @param soapMsg\r
519    */\r
520   private void logSoapMsg(SOAPMessage soapMsg) {\r
521     StringBuilder buffer = new StringBuilder();\r
522     buffer.append(soapMsg.toString());\r
523     ByteArrayOutputStream baos = new ByteArrayOutputStream();\r
524     try {\r
525       soapMsg.writeTo(baos);\r
526     } catch (SOAPException | IOException e) {\r
527       logger.error("Error while writting soap message");\r
528     }\r
529     buffer.append(baos);\r
530     String soapMessage = buffer.toString();\r
531     logger.debug(soapMessage);\r
532   }\r
533 \r
534   /**\r
535    * @param response\r
536    * @param out\r
537    * @param fcodeAcsRequestDenied\r
538    * @param faultString\r
539    * @param id\r
540    * @throws IOException\r
541    */\r
542   private void sendFault(HttpServletResponse response, ByteArrayOutputStream out,\r
543       String fcodeAcsRequestDenied, String faultString, String id) throws IOException {\r
544     Fault fault = new Fault(fcodeAcsRequestDenied, faultString, id);\r
545     fault.writeTo(out);\r
546     response.setContentLength(out.size());\r
547     String sout = out.toString(UTF_8);\r
548     sout = sout.replace('\'', '"');\r
549     response.getOutputStream().print(sout);\r
550   }\r
551 \r
552   private void logHeaderElements(Enumeration<String> headerName) {\r
553     while (headerName.hasMoreElements()) {\r
554       String requestHeader = headerName.nextElement();\r
555       logger.debug("Request Headers {}", requestHeader);\r
556     }\r
557   }\r
558 \r
559   private String getRequestType(TR069RPC msg) {\r
560     String requestType = msg.getName();\r
561     if (requestType == null)\r
562       requestType = "Empty Request";\r
563 \r
564     return requestType;\r
565   }\r
566 \r
567   /******************************************************************************************************************/\r
568 \r
569   public DeviceEventHandler getDeviceEventHandler() {\r
570     return deviceEventHandler;\r
571   }\r
572 \r
573   public void setDeviceEventHandler(DeviceEventHandler deviceEventHandler) {\r
574     this.deviceEventHandler = deviceEventHandler;\r
575   }\r
576 \r
577 }\r