1 /*******************************************************************************
\r
2 * ============LICENSE_START==================================================
\r
4 * * ===========================================================================
\r
5 * * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
\r
6 * * ===========================================================================
\r
7 * * Licensed under the Apache License, Version 2.0 (the "License");
\r
8 * * you may not use this file except in compliance with the License.
\r
9 * * You may obtain a copy of the License at
\r
11 * * http://www.apache.org/licenses/LICENSE-2.0
\r
13 * * Unless required by applicable law or agreed to in writing, software
\r
14 * * distributed under the License is distributed on an "AS IS" BASIS,
\r
15 * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
16 * * See the License for the specific language governing permissions and
\r
17 * * limitations under the License.
\r
18 * * ============LICENSE_END====================================================
\r
20 * * ECOMP is a trademark and service mark of AT&T Intellectual Property.
\r
22 ******************************************************************************/
\r
25 package com.att.research.datarouter.provisioning;
\r
27 import java.io.ByteArrayOutputStream;
\r
28 import java.io.File;
\r
29 import java.io.IOException;
\r
30 import java.io.InputStream;
\r
31 import java.nio.file.FileStore;
\r
32 import java.nio.file.FileSystem;
\r
33 import java.nio.file.Files;
\r
34 import java.nio.file.Path;
\r
35 import java.nio.file.Paths;
\r
36 import java.nio.file.StandardCopyOption;
\r
37 import java.util.Properties;
\r
39 import javax.servlet.http.HttpServletRequest;
\r
40 import javax.servlet.http.HttpServletResponse;
\r
42 import org.json.JSONArray;
\r
44 import com.att.eelf.configuration.EELFLogger;
\r
45 import com.att.eelf.configuration.EELFManager;
\r
46 import com.att.research.datarouter.provisioning.beans.EventLogRecord;
\r
47 import com.att.research.datarouter.provisioning.beans.LogRecord;
\r
48 import com.att.research.datarouter.provisioning.beans.Parameters;
\r
49 import com.att.research.datarouter.provisioning.eelf.EelfMsgs;
\r
50 import com.att.research.datarouter.provisioning.utils.DB;
\r
51 import com.att.research.datarouter.provisioning.utils.RLEBitSet;
\r
52 import com.att.research.datarouter.provisioning.utils.LogfileLoader;
\r
56 * This servlet handles requests to URLs under /internal on the provisioning server.
\r
59 * <div class="contentContainer">
\r
60 * <table class="packageSummary" border="0" cellpadding="3" cellspacing="0">
\r
61 * <caption><span>URL Path Summary</span><span class="tabEnd"> </span></caption>
\r
63 * <th class="colFirst" width="15%">URL Path</th>
\r
64 * <th class="colOne">Method</th>
\r
65 * <th class="colLast">Purpose</th>
\r
67 * <tr class="altColor">
\r
68 * <td class="colFirst">/internal/prov</td>
\r
69 * <td class="colOne">GET</td>
\r
70 * <td class="colLast">used to GET a full JSON copy of the provisioning data.</td>
\r
72 * <tr class="rowColor">
\r
73 * <td class="colFirst">/internal/fetchProv</td>
\r
74 * <td class="colOne">GET</td>
\r
75 * <td class="colLast">used to signal to a standby POD that the provisioning data should be fetched from the active POD.</td>
\r
77 * <tr class="altColor">
\r
78 * <td class="colFirst" rowspan="2">/internal/logs</td>
\r
79 * <td class="colOne">GET</td>
\r
80 * <td class="colLast">used to GET an index of log files and individual logs for this provisioning server.</td>
\r
82 * <tr class="altColor">
\r
83 * <td class="colOne">POST</td>
\r
84 * <td class="colLast">used to POST log files from the individual nodes to this provisioning server.</td>
\r
86 * <tr class="rowColor">
\r
87 * <td class="colFirst" rowspan="4">/internal/api</td>
\r
88 * <td class="colOne">GET</td>
\r
89 * <td class="colLast">used to GET an individual parameter value. The parameter name is specified by the path after /api/.</td>
\r
91 * <tr class="rowColor">
\r
92 * <td class="colOne">PUT</td>
\r
93 * <td class="colLast">used to set an individual parameter value. The parameter name is specified by the path after /api/.</td>
\r
95 * <tr class="rowColor">
\r
96 * <td class="colOne">DELETE</td>
\r
97 * <td class="colLast">used to remove an individual parameter value. The parameter name is specified by the path after /api/.</td>
\r
99 * <tr class="rowColor">
\r
100 * <td class="colOne">POST</td>
\r
101 * <td class="colLast">used to create a new individual parameter value. The parameter name is specified by the path after /api/.</td>
\r
103 * <tr class="altColor">
\r
104 * <td class="colFirst">/internal/halt</td>
\r
105 * <td class="colOne">GET</td>
\r
106 * <td class="colLast">used to halt the server (must be accessed from 127.0.0.1).</td>
\r
108 * <tr class="rowColor">
\r
109 * <td class="colFirst" rowspan="2">/internal/drlogs</td>
\r
110 * <td class="colOne">GET</td>
\r
111 * <td class="colLast">used to get a list of DR log entries available for retrieval.
\r
112 * Note: these are the actual data router log entries sent to the provisioning server
\r
113 * by the nodes, not the provisioning server's internal logs (access via /internal/logs above).
\r
114 * The range is returned as a list of record sequence numbers.</td>
\r
116 * <tr class="rowColor">
\r
117 * <td class="colOne">POST</td>
\r
118 * <td class="colLast">used to retrieve specific log entries.
\r
119 * The sequence numbers of the records to fetch are POST-ed; the records matching the sequence numbers are returned.</td>
\r
121 * <tr class="altColor">
\r
122 * <td class="colFirst">/internal/route/*</td>
\r
123 * <td class="colOne">*</td>
\r
124 * <td class="colLast">URLs under this path are handled via the {@link com.att.research.datarouter.provisioning.RouteServlet}</td>
\r
129 * Authorization to use these URLs is a little different than for other URLs on the provisioning server.
\r
130 * For the most part, the IP address that the request comes from should be either:
\r
133 * <li>an IP address of a provisioning server, or</li>
\r
134 * <li>the IP address of a node (to allow access to /internal/prov), or</li>
\r
135 * <li>an IP address from the "<i>special subnet</i>" which is configured with
\r
136 * the PROV_SPECIAL_SUBNET parameter.
\r
139 * In addition, requests to /internal/halt can ONLY come from localhost (127.0.0.1) on the HTTP port.
\r
142 * All DELETE/GET/PUT/POST requests made to /internal/api on this servlet on the standby server are
\r
143 * proxied to the active server (using the {@link ProxyServlet}) if it is up and reachable.
\r
146 * @author Robert Eby
\r
147 * @version $Id: InternalServlet.java,v 1.23 2014/03/24 18:47:10 eby Exp $
\r
149 @SuppressWarnings("serial")
\r
150 public class InternalServlet extends ProxyServlet {
\r
151 private static Integer logseq = new Integer(0); // another piece of info to make log spool file names unique
\r
152 //Adding EELF Logger Rally:US664892
\r
153 private static EELFLogger eelflogger = EELFManager.getInstance().getLogger("com.att.research.datarouter.provisioning.InternalServlet");
\r
156 * Delete a parameter at the address /internal/api/<parameter>.
\r
157 * See the <b>Internal API</b> document for details on how this method should be invoked.
\r
160 public void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
\r
161 setIpAndFqdnForEelf("doDelete");
\r
162 eelflogger.info(EelfMsgs.MESSAGE_WITH_BEHALF_AND_FEEDID, req.getHeader(BEHALF_HEADER),getIdFromPath(req)+"");
\r
163 EventLogRecord elr = new EventLogRecord(req);
\r
164 if (!isAuthorizedForInternal(req)) {
\r
165 elr.setMessage("Unauthorized.");
\r
166 elr.setResult(HttpServletResponse.SC_FORBIDDEN);
\r
167 eventlogger.info(elr);
\r
168 resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized.");
\r
172 String path = req.getPathInfo();
\r
173 if (path.startsWith("/api/")) {
\r
174 if (isProxyOK(req) && isProxyServer()) {
\r
175 super.doDelete(req, resp);
\r
178 String key = path.substring(5);
\r
179 if (key.length() > 0) {
\r
180 Parameters param = Parameters.getParameter(key);
\r
181 if (param != null) {
\r
182 if (doDelete(param)) {
\r
183 elr.setResult(HttpServletResponse.SC_OK);
\r
184 eventlogger.info(elr);
\r
185 resp.setStatus(HttpServletResponse.SC_OK);
\r
186 provisioningDataChanged();
\r
187 provisioningParametersChanged();
\r
189 // Something went wrong with the DELETE
\r
190 elr.setResult(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
\r
191 eventlogger.info(elr);
\r
192 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, DB_PROBLEM_MSG);
\r
198 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Bad URL.");
\r
201 * Get some information (such as a parameter) underneath the /internal/ namespace.
\r
202 * See the <b>Internal API</b> document for details on how this method should be invoked.
\r
205 public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
\r
206 setIpAndFqdnForEelf("doGet");
\r
207 eelflogger.info(EelfMsgs.MESSAGE_WITH_BEHALF_AND_FEEDID, req.getHeader(BEHALF_HEADER),getIdFromPath(req)+"");
\r
208 String path = req.getPathInfo();
\r
209 if (path.equals("/halt") && !req.isSecure()) {
\r
210 // request to halt the server - can ONLY come from localhost
\r
211 String remote = req.getRemoteAddr();
\r
212 if (remote.equals("127.0.0.1")) {
\r
213 intlogger.info("PROV0009 Request to HALT received.");
\r
214 resp.setStatus(HttpServletResponse.SC_OK);
\r
217 intlogger.info("PROV0010 Disallowed request to HALT received from "+remote);
\r
218 resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
\r
223 EventLogRecord elr = new EventLogRecord(req);
\r
224 if (!isAuthorizedForInternal(req)) {
\r
225 elr.setMessage("Unauthorized.");
\r
226 elr.setResult(HttpServletResponse.SC_FORBIDDEN);
\r
227 eventlogger.info(elr);
\r
228 resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized.");
\r
231 if (path.equals("/fetchProv") && !req.isSecure()) {
\r
232 // if request came from active_pod or standby_pod and it is not us, reload prov data
\r
233 SynchronizerTask s = SynchronizerTask.getSynchronizer();
\r
235 resp.setStatus(HttpServletResponse.SC_OK);
\r
238 if (path.equals("/prov")) {
\r
239 if (isProxyOK(req) && isProxyServer()) {
\r
240 if (super.doGetWithFallback(req, resp))
\r
242 // fall back to returning the local data if the remote is unreachable
\r
243 intlogger.info("Active server unavailable; falling back to local copy.");
\r
245 Poker p = Poker.getPoker();
\r
246 resp.setStatus(HttpServletResponse.SC_OK);
\r
247 resp.setContentType(PROVFULL_CONTENT_TYPE2);
\r
248 resp.getOutputStream().print(p.getProvisioningString());
\r
251 if (path.equals("/logs") || path.equals("/logs/")) {
\r
252 resp.setStatus(HttpServletResponse.SC_OK);
\r
253 resp.setContentType("application/json");
\r
254 resp.getOutputStream().print(generateLogfileList().toString());
\r
257 if (path.startsWith("/logs/")) {
\r
258 Properties p = (new DB()).getProperties();
\r
259 String logdir = p.getProperty("com.att.research.datarouter.provserver.accesslog.dir");
\r
260 String logfile = path.substring(6);
\r
261 if (logdir != null && logfile != null && logfile.indexOf('/') < 0) {
\r
262 File log = new File(logdir + "/" + logfile);
\r
263 if (log.exists() && log.isFile()) {
\r
264 resp.setStatus(HttpServletResponse.SC_OK);
\r
265 resp.setContentType("text/plain");
\r
266 Path logpath = Paths.get(log.getAbsolutePath());
\r
267 Files.copy(logpath, resp.getOutputStream());
\r
271 resp.sendError(HttpServletResponse.SC_NO_CONTENT, "No file.");
\r
274 if (path.startsWith("/api/")) {
\r
275 if (isProxyOK(req) && isProxyServer()) {
\r
276 super.doGet(req, resp);
\r
279 String key = path.substring(5);
\r
280 if (key.length() > 0) {
\r
281 Parameters param = Parameters.getParameter(key);
\r
282 if (param != null) {
\r
283 resp.setStatus(HttpServletResponse.SC_OK);
\r
284 resp.setContentType("text/plain");
\r
285 resp.getOutputStream().print(param.getValue() + "\n");
\r
290 if (path.equals("/drlogs") || path.equals("/drlogs/")) {
\r
291 // Special POD <=> POD API to determine what log file records are loaded here
\r
292 LogfileLoader lfl = LogfileLoader.getLoader();
\r
293 resp.setStatus(HttpServletResponse.SC_OK);
\r
294 resp.setContentType("text/plain");
\r
295 resp.getOutputStream().print(lfl.getBitSet().toString());
\r
298 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Bad URL.");
\r
301 * Modify a parameter at the address /internal/api/<parameter>.
\r
302 * See the <b>Internal API</b> document for details on how this method should be invoked.
\r
305 public void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
\r
306 setIpAndFqdnForEelf("doPut");
\r
307 eelflogger.info(EelfMsgs.MESSAGE_WITH_BEHALF_AND_FEEDID, req.getHeader(BEHALF_HEADER),getIdFromPath(req)+"");
\r
308 EventLogRecord elr = new EventLogRecord(req);
\r
309 if (!isAuthorizedForInternal(req)) {
\r
310 elr.setMessage("Unauthorized.");
\r
311 elr.setResult(HttpServletResponse.SC_FORBIDDEN);
\r
312 eventlogger.info(elr);
\r
313 resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized.");
\r
316 String path = req.getPathInfo();
\r
317 if (path.startsWith("/api/")) {
\r
318 if (isProxyOK(req) && isProxyServer()) {
\r
319 super.doPut(req, resp);
\r
322 String key = path.substring(5);
\r
323 if (key.length() > 0) {
\r
324 Parameters param = Parameters.getParameter(key);
\r
325 if (param != null) {
\r
326 String t = catValues(req.getParameterValues("val"));
\r
328 if (doUpdate(param)) {
\r
329 elr.setResult(HttpServletResponse.SC_OK);
\r
330 eventlogger.info(elr);
\r
331 resp.setStatus(HttpServletResponse.SC_OK);
\r
332 provisioningDataChanged();
\r
333 provisioningParametersChanged();
\r
335 // Something went wrong with the UPDATE
\r
336 elr.setResult(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
\r
337 eventlogger.info(elr);
\r
338 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, DB_PROBLEM_MSG);
\r
344 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Bad URL.");
\r
347 * Create some new information (such as a parameter or log entries) underneath the /internal/ namespace.
\r
348 * See the <b>Internal API</b> document for details on how this method should be invoked.
\r
350 @SuppressWarnings("resource")
\r
352 public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
\r
353 setIpAndFqdnForEelf("doPost");
\r
354 eelflogger.info(EelfMsgs.MESSAGE_WITH_BEHALF, req.getHeader(BEHALF_HEADER));
\r
355 EventLogRecord elr = new EventLogRecord(req);
\r
356 if (!isAuthorizedForInternal(req)) {
\r
357 elr.setMessage("Unauthorized.");
\r
358 elr.setResult(HttpServletResponse.SC_FORBIDDEN);
\r
359 eventlogger.info(elr);
\r
360 resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized.");
\r
364 String path = req.getPathInfo();
\r
365 if (path.startsWith("/api/")) {
\r
366 if (isProxyOK(req) && isProxyServer()) {
\r
367 super.doPost(req, resp);
\r
370 String key = path.substring(5);
\r
371 if (key.length() > 0) {
\r
372 Parameters param = Parameters.getParameter(key);
\r
373 if (param == null) {
\r
374 String t = catValues(req.getParameterValues("val"));
\r
375 param = new Parameters(key, t);
\r
376 if (doInsert(param)) {
\r
377 elr.setResult(HttpServletResponse.SC_OK);
\r
378 eventlogger.info(elr);
\r
379 resp.setStatus(HttpServletResponse.SC_OK);
\r
380 provisioningDataChanged();
\r
381 provisioningParametersChanged();
\r
383 // Something went wrong with the INSERT
\r
384 elr.setResult(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
\r
385 eventlogger.info(elr);
\r
386 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, DB_PROBLEM_MSG);
\r
393 if (path.equals("/logs") || path.equals("/logs/")) {
\r
394 String ctype = req.getHeader("Content-Type");
\r
395 if (ctype == null || !ctype.equals("text/plain")) {
\r
396 elr.setResult(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
\r
397 elr.setMessage("Bad media type: "+ctype);
\r
398 resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
\r
399 eventlogger.info(elr);
\r
402 String spooldir = (new DB()).getProperties().getProperty("com.att.research.datarouter.provserver.spooldir");
\r
403 String spoolname = String.format("%d-%d-", System.currentTimeMillis(), Thread.currentThread().getId());
\r
404 synchronized (logseq) {
\r
405 // perhaps unnecessary, but it helps make the name unique
\r
406 spoolname += logseq.toString();
\r
409 String encoding = req.getHeader("Content-Encoding");
\r
410 if (encoding != null) {
\r
411 if (encoding.trim().equals("gzip")) {
\r
412 spoolname += ".gz";
\r
414 elr.setResult(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
\r
415 resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
\r
416 eventlogger.info(elr);
\r
420 // Determine space available -- available space must be at least 5%
\r
421 FileSystem fs = (Paths.get(spooldir)).getFileSystem();
\r
424 for (FileStore store: fs.getFileStores()) {
\r
425 total += store.getTotalSpace();
\r
426 avail += store.getUsableSpace();
\r
428 try { fs.close(); } catch (Exception e) { }
\r
429 if (((avail * 100) / total) < 5) {
\r
430 elr.setResult(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
\r
431 resp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
\r
432 eventlogger.info(elr);
\r
435 Path tmppath = Paths.get(spooldir, spoolname);
\r
436 Path donepath = Paths.get(spooldir, "IN."+spoolname);
\r
437 Files.copy(req.getInputStream(), Paths.get(spooldir, spoolname), StandardCopyOption.REPLACE_EXISTING);
\r
438 Files.move(tmppath, donepath, StandardCopyOption.REPLACE_EXISTING);
\r
439 elr.setResult(HttpServletResponse.SC_CREATED);
\r
440 resp.setStatus(HttpServletResponse.SC_CREATED);
\r
441 eventlogger.info(elr);
\r
442 LogfileLoader.getLoader(); // This starts the logfile loader "task"
\r
446 if (path.equals("/drlogs") || path.equals("/drlogs/")) {
\r
447 // Receive post request and generate log entries
\r
448 String ctype = req.getHeader("Content-Type");
\r
449 if (ctype == null || !ctype.equals("text/plain")) {
\r
450 elr.setResult(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
\r
451 elr.setMessage("Bad media type: "+ctype);
\r
452 resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
\r
453 eventlogger.info(elr);
\r
456 InputStream is = req.getInputStream();
\r
457 ByteArrayOutputStream bos = new ByteArrayOutputStream();
\r
459 while ((ch = is.read()) >= 0)
\r
461 RLEBitSet bs = new RLEBitSet(bos.toString()); // The set of records to retrieve
\r
462 elr.setResult(HttpServletResponse.SC_OK);
\r
463 resp.setStatus(HttpServletResponse.SC_OK);
\r
464 resp.setContentType("text/plain");
\r
465 LogRecord.printLogRecords(resp.getOutputStream(), bs);
\r
466 eventlogger.info(elr);
\r
470 elr.setResult(HttpServletResponse.SC_NOT_FOUND);
\r
471 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Bad URL.");
\r
472 eventlogger.info(elr);
\r
475 private String catValues(String[] v) {
\r
476 StringBuilder sb = new StringBuilder();
\r
479 for (String s : v) {
\r
485 return sb.toString();
\r
487 private JSONArray generateLogfileList() {
\r
488 JSONArray ja = new JSONArray();
\r
489 Properties p = (new DB()).getProperties();
\r
490 String s = p.getProperty("com.att.research.datarouter.provserver.accesslog.dir");
\r
492 String[] dirs = s.split(",");
\r
493 for (String dir : dirs) {
\r
494 File f = new File(dir);
\r
495 String[] list = f.list();
\r
496 if (list != null) {
\r
497 for (String s2 : list) {
\r
498 if (!s2.startsWith("."))
\r