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