001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.monitoring;
019
020import com.mashape.unirest.http.HttpResponse;
021import com.mashape.unirest.http.Unirest;
022import com.mashape.unirest.http.exceptions.UnirestException;
023import io.stallion.asyncTasks.AsyncTaskController;
024import io.stallion.exceptions.ClientException;
025import io.stallion.exceptions.WebException;
026import io.stallion.jobs.JobCoordinator;
027import io.stallion.plugins.StallionJavaPlugin;
028import io.stallion.plugins.PluginRegistry;
029import io.stallion.restfulEndpoints.EndpointResource;
030import io.stallion.restfulEndpoints.MinRole;
031import io.stallion.restfulEndpoints.XSRF;
032import io.stallion.services.Log;
033import io.stallion.settings.Settings;
034import io.stallion.templating.TemplateRenderer;
035import io.stallion.users.Role;
036
037import javax.ws.rs.*;
038
039import java.io.IOException;
040import java.net.URL;
041import java.util.*;
042import java.util.jar.Manifest;
043
044import static io.stallion.utils.Literals.*;
045import static io.stallion.Context.*;
046
047@Path("/st-internal")
048public class InternalEndpoints implements EndpointResource {
049
050
051    @GET
052    @Path("/health")
053    public HealthInfo checkHealth(@QueryParam("secret") String secret, @QueryParam("failOnWarnings") Boolean failOnWarnings, @QueryParam("sections") String sections) {
054        failOnWarnings = or(failOnWarnings, false);
055        sections = or(sections, "all");
056        checkSecret(secret);
057        HealthInfo info = buildHealthInfo(sections);
058        if (info.getErrors().size() > 0) {
059            response().setStatus(515);
060        }
061        if (failOnWarnings && info.getWarnings().size() > 0) {
062            response().setStatus(515);
063        }
064        info.setHttpStatusCode(response().getStatus());
065        return info;
066    }
067
068    @POST
069    @Path("/run-job")
070    @XSRF(false)
071    public Object runJob(@QueryParam("secret") String secret, @QueryParam("job") String job) {
072        checkSecret(secret);
073        JobCoordinator.instance().forceRunJob(job, true);
074        return true;
075    }
076
077
078    private HealthInfo buildHealthInfo(String sectionsString) {
079        List<String> sections = Arrays.asList(sectionsString.split(","));
080        if ("all".equals(sectionsString)) {
081            sections = list("http", "jobs", "tasks", "endpoints", "system");
082        }
083        HealthInfo info = new HealthInfo();
084
085        if (sections.contains("http")) {
086            info.setHttp(HealthTracker.instance().getHttpHealthInfo());
087        }
088        if (sections.contains("jobs")) {
089            info.setJobs(JobCoordinator.instance().buildJobHealthInfos());
090        }
091        if (sections.contains("tasks")) {
092            info.setTasks(AsyncTaskController.instance().buildHealthInfo());
093        }
094        if (sections.contains("endpoints")) {
095            info.setEndpoints(checkEndpointHealth());
096        }
097        if (sections.contains("system")) {
098            info.setSystem(new SystemHealth().hydrateSystemHealth());
099        }
100
101        info.hydrateErrors();
102
103        return info;
104    }
105
106    private void checkSecret(String secret) {
107        if (empty(settings().getHealthCheckSecret())) {
108            throw new ClientException("You must define a setting value for 'healthCheckSecret' in your stallion.toml. This should be a random string that only you know. Then you should pass this in via the query string parameter 'secret' or the header 'X-Healthcheck-Secret", 500);
109        }
110        if (empty(secret)) {
111            secret = request().getHeader("X-Healthcheck-Secret");
112        }
113        if (empty(secret)) {
114            throw new ClientException("You must pass in a either a query param 'secret' or a header 'X-Healthcheck-Secret'. The passed in secret should match the value defined in stallion.toml for the setting name 'healthCheckSecret'.  ", 403);
115        }
116        if (!secret.equals(Settings.instance().getHealthCheckSecret())) {
117            throw new ClientException("Invalid healthcheck secret");
118        }
119    }
120
121    private List<EndpointHealthInfo> checkEndpointHealth() {
122        List<EndpointHealthInfo> infos = new ArrayList<>();
123        for (String[] endPointArray: settings().getHealthCheckEndpoints()) {
124            EndpointHealthInfo endPointHealth = new EndpointHealthInfo();
125            infos.add(endPointHealth);
126            endPointHealth.setUrl(endPointArray[0]);
127            HttpResponse<String> httpResponse = null;
128            try {
129                httpResponse = Unirest.get("http://localhost:" + settings().getPort() + "" + endPointArray[0]).asString();
130            } catch (UnirestException e) {
131                endPointHealth.setStatusCode(999);
132                continue;
133            }
134            endPointHealth.setStatusCode(httpResponse.getStatus());
135            if (endPointArray.length > 1) {
136                endPointHealth.setFoundString(httpResponse.getBody().contains(endPointArray[1]));
137            } else {
138                endPointHealth.setFoundString(true);
139            }
140        }
141        return infos;
142    }
143
144    @GET
145    @Path("/info")
146    public SystemInformation getInfo(@QueryParam("secret") String secret) {
147        checkSecret(secret);
148        SystemInformation info = new SystemInformation();
149        info.setTargetPath(settings().getTargetFolder());
150        info.setxRealIp(request().getHeader("X-Real-Ip"));
151        info.setxForwardedFor(request().getHeader("x-Fowarded-For"));
152        info.setGuessedHost(request().getHost());
153        info.setGuessedScheme(request().getScheme());
154        info.setxForwardedProto(request().getHeader("X-Forwarded-Proto"));
155        info.setRemoteAddr(request().getRemoteAddr());
156        info.setDeployDate(System.getenv("STALLION_DEPLOY_TIME"));
157        info.setGuessedIp(request().getActualIp());
158        info.setInstanceHostName(System.getenv("STALLION_HOST"));
159        info.setInstanceDomain(System.getenv("STALLION_DOMAIN"));
160        info.setxForwardedHost(request().getHeader("x-forwarded-host"));
161        info.setxUpstreamForwardedProto(request().getHeader("x-upstream-forwarded-proto"));
162        info.setEnv(Settings.instance().getEnv());
163        info.setPort(settings().getPort());
164        List<ClassLoader> loaders = new ArrayList<>();
165        loaders.add(getClass().getClassLoader());
166        for (StallionJavaPlugin booter: PluginRegistry.instance().getJavaPluginByName().values()) {
167            loaders.add(booter.getClass().getClassLoader());
168        }
169        for (ClassLoader loader: loaders) {
170            try {
171                Enumeration<URL> resources = loader.getResources("META-INF/MANIFEST.MF");
172                for (Integer i : safeLoop(1000)) {
173                    if (!resources.hasMoreElements()) {
174                        break;
175                    }
176                    URL url = resources.nextElement();
177                    Manifest manifest = new Manifest(url.openStream());
178                    String buildTime = manifest.getMainAttributes().getValue("Build-Time");
179                    String key = url.toString();
180                    if (!empty(buildTime)) {
181                        info.getJarBuildDates().put(key, buildTime);
182                    }
183                }
184            } catch (IOException ex) {
185                Log.exception(ex, "Error loading MANIFEST.MF");
186            }
187        }
188        return info;
189    }
190
191    @GET
192    @Path("/exceptions")
193    @MinRole(value = Role.ADMIN, redirect = true)
194    @Produces("text/html")
195    public String viewExceptions() {
196        Map<String, Object> context = map();
197        ArrayList<ExceptionInfo> exceptions = new ArrayList<>();
198        for(ExceptionInfo info: HealthTracker.instance().getExceptionQueue()) {
199            exceptions.add(info);
200        }
201        Collections.reverse(exceptions);
202        context.put("exceptions", exceptions);
203        return TemplateRenderer.instance().renderTemplate(getClass().getResource("/templates/exceptions.jinja"), context);
204    }
205
206    @POST
207    @Produces("text/html")
208    @Path("/force-exception")
209    public String forceException(@QueryParam("secret") String secret) {
210        checkSecret(secret);
211        throw new WebException("A forced exception!", 500);
212    }
213
214    @GET
215    @Produces("text/html")
216    @Path("/force-exception-get")
217    public String forceExceptionGet(@QueryParam("secret") String secret) {
218        checkSecret(secret);
219        throw new WebException("A forced exception!", 500);
220    }
221}