/* * Copyright 2000-2018 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.buildServer.server.rest.request; import com.sun.jersey.api.core.InjectParam; import io.swagger.annotations.Api; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.*; import javax.ws.rs.core.Context; import jetbrains.buildServer.ServiceLocator; import jetbrains.buildServer.controllers.FileSecurityUtil; import jetbrains.buildServer.log.Loggers; import jetbrains.buildServer.server.rest.ApiUrlBuilder; import jetbrains.buildServer.server.rest.data.BuildArtifactsFinder; import jetbrains.buildServer.server.rest.data.DataProvider; import jetbrains.buildServer.server.rest.data.PermissionChecker; import jetbrains.buildServer.server.rest.errors.BadRequestException; import jetbrains.buildServer.server.rest.errors.InvalidStateException; import jetbrains.buildServer.server.rest.errors.NotFoundException; import jetbrains.buildServer.server.rest.model.Fields; import jetbrains.buildServer.server.rest.model.Util; import jetbrains.buildServer.server.rest.model.plugin.PluginInfos; import jetbrains.buildServer.server.rest.model.server.LicenseKeyEntities; import jetbrains.buildServer.server.rest.model.server.LicenseKeyEntity; import jetbrains.buildServer.server.rest.model.server.LicensingData; import jetbrains.buildServer.server.rest.model.server.Server; import jetbrains.buildServer.server.rest.util.BeanContext; import jetbrains.buildServer.server.rest.util.BeanFactory; import jetbrains.buildServer.serverSide.*; import jetbrains.buildServer.serverSide.auth.Permission; import jetbrains.buildServer.serverSide.maintenance.BackupConfig; import jetbrains.buildServer.serverSide.maintenance.BackupProcess; import jetbrains.buildServer.serverSide.maintenance.BackupProcessManager; import jetbrains.buildServer.serverSide.maintenance.MaintenanceProcessAlreadyRunningException; import jetbrains.buildServer.util.FileUtil; import jetbrains.buildServer.util.StringUtil; import jetbrains.buildServer.util.browser.Element; import jetbrains.buildServer.web.util.WebUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /* * User: Yegor Yarko * Date: 11.04.2009 */ @Path(ServerRequest.API_SERVER_URL) @Api("Server") public class ServerRequest { public static final String SERVER_VERSION_RQUEST_PATH = "version"; public static final String SERVER_REQUEST_PATH = "/server"; public static final String API_SERVER_URL = Constants.API_URL + SERVER_REQUEST_PATH; protected static final String LICENSING_DATA = "/licensingData"; protected static final String LICENSING_KEYS = LICENSING_DATA + "/licenseKeys"; @Context private DataProvider myDataProvider; @Context private ServiceLocator myServiceLocator; @Context private ApiUrlBuilder myApiUrlBuilder; @Context private BeanFactory myFactory; @SuppressWarnings("NullableProblems") @Context @NotNull private BeanContext myBeanContext; @SuppressWarnings("NullableProblems") @Context @NotNull private PermissionChecker myPermissionChecker; public static String getLicenseKeysListHref() { return API_SERVER_URL + LICENSING_KEYS; } @GET @Produces({"application/xml", "application/json"}) public Server serveServerInfo(@QueryParam("fields") String fields) { return new Server(new Fields(fields), new BeanContext(myFactory, myServiceLocator, myApiUrlBuilder)); } @GET @Path("/{field}") @Produces({"text/plain"}) public String serveServerVersion(@PathParam("field") String fieldName) { return Server.getFieldValue(fieldName, myServiceLocator); } @GET @Path("/plugins") @Produces({"application/xml", "application/json"}) public PluginInfos servePlugins(@QueryParam("fields") String fields) { myDataProvider.checkGlobalPermission(Permission.VIEW_SERVER_SETTINGS); return new PluginInfos(myDataProvider.getPlugins(), new Fields(fields), myBeanContext); } /** * * @param fileName relative file name to save backup to (will be saved into * the default backup directory (.BuildServer/backup * if not overriden in main-config.xml) * @param addTimestamp whether to add timestamp to the file or not * @param includeConfigs whether to include configs into the backup or not * @param includeDatabase whether to include database into the backup or not * @param includeBuildLogs whether to include build logs into the backup or not * @param includePersonalChanges whether to include personal changes into the backup or not * @return the resulting file name that the backup will be saved to */ @POST @Path("/backup") @Produces({"text/plain"}) public String startBackup(@QueryParam("fileName") String fileName, @QueryParam("addTimestamp") Boolean addTimestamp, @QueryParam("includeConfigs") Boolean includeConfigs, @QueryParam("includeDatabase") Boolean includeDatabase, @QueryParam("includeBuildLogs") Boolean includeBuildLogs, @QueryParam("includePersonalChanges") Boolean includePersonalChanges, @QueryParam("includeRunningBuilds") Boolean includeRunningBuilds, @QueryParam("includeSupplimentaryData") Boolean includeSupplimentaryData, @InjectParam BackupProcessManager backupManager) { BackupConfig backupConfig = new BackupConfig(); if (StringUtil.isNotEmpty(fileName)) { if (!TeamCityProperties.getBoolean("rest.request.server.backup.allowAnyTargetPath")) { File backupDir = new File(myDataProvider.getBean(ServerPaths.class).getBackupDir()); try { FileSecurityUtil.checkInsideDirectory(FileUtil.resolvePath(backupDir, fileName), backupDir); } catch (Exception e) { //the message contains absolute paths if (myPermissionChecker.hasGlobalPermission(Permission.MANAGE_SERVER_INSTALLATION)) { throw e; } throw new BadRequestException("Target file name (" + fileName + ") should be relative path.", null); } } if (addTimestamp != null) { backupConfig.setFileName(fileName, addTimestamp); } else { backupConfig.setFileName(fileName); } }else{ throw new BadRequestException("No target file name specified.", null); } if (includeConfigs != null) backupConfig.setIncludeConfiguration(includeConfigs); if (includeDatabase != null) backupConfig.setIncludeDatabase(includeDatabase); if (includeBuildLogs != null) backupConfig.setIncludeBuildLogs(includeBuildLogs); if (includePersonalChanges != null) backupConfig.setIncludePersonalChanges(includePersonalChanges); if (includeRunningBuilds != null) backupConfig.setIncludeRunningBuilds(includeRunningBuilds); if (includeSupplimentaryData != null) backupConfig.setIncludeSupplementaryData(includeSupplimentaryData); try { backupManager.startBackup(backupConfig); } catch (MaintenanceProcessAlreadyRunningException e) { throw new InvalidStateException("Cannot start backup because another maintenance process is in progress", e); } return backupConfig.getResultFileName(); } /** * @return current backup status */ @GET @Path("/backup") @Produces({"text/plain"}) public String getBackupStatus(@InjectParam BackupProcessManager backupManager) { final BackupProcess backupProcess = backupManager.getCurrentBackupProcess(); if (backupProcess == null) { return "Idle"; } return backupProcess.getProgressStatus().name(); } /** * @return list server license keys */ @GET @Path(LICENSING_DATA) @Produces({"application/xml", "application/json"}) public LicensingData getLicensingData(@QueryParam("fields") String fields) { myDataProvider.checkGlobalPermission(Permission.VIEW_SERVER_SETTINGS); return new LicensingData(myBeanContext.getSingletonService(BuildServerEx.class).getLicenseKeysManager(), new Fields(fields), myBeanContext); } @GET @Path(LICENSING_KEYS) @Produces({"application/xml", "application/json"}) public LicenseKeyEntities getLicenseKeys(@QueryParam("fields") String fields) { myDataProvider.checkGlobalPermission(Permission.VIEW_SERVER_SETTINGS); LicenseList licenseList = myBeanContext.getSingletonService(BuildServerEx.class).getLicenseKeysManager().getLicenseList(); return new LicenseKeyEntities(licenseList.getAllLicenses(), licenseList.getActiveLicenses(), ServerRequest.getLicenseKeysListHref(), new Fields(fields), myBeanContext); } private static final Pattern DELIMITERS = Pattern.compile("[\\n\\r, ]"); /** * Adds newline-delimited list of license keys to the server or returns textual description is there are not valid keys */ @POST @Path(LICENSING_KEYS) @Consumes({"text/plain"}) @Produces({"application/xml", "application/json"}) public LicenseKeyEntities addLicenseKeys(final String licenseKeyCodes, @QueryParam("fields") String fields) { myDataProvider.checkGlobalPermission(Permission.CHANGE_SERVER_SETTINGS); LicenseKeysManager licenseKeysManager = myBeanContext.getSingletonService(BuildServerEx.class).getLicenseKeysManager(); List keysToAdd = Stream.of(DELIMITERS.split(licenseKeyCodes)).map(s -> s.trim()).filter(s -> !StringUtil.isEmpty(s)).collect(Collectors.toList()); List validatedKeys = licenseKeysManager.validateKeys(keysToAdd); //TeamCity API issue: why return good keys? if (!validatedKeys.isEmpty()) { // is there a way to return entity with not 200 result code??? StringBuilder resultMessage = new StringBuilder(); resultMessage.append("Invalid keys:\n"); boolean invalidKeysFound = false; for (LicenseKey validatedKey : validatedKeys) { invalidKeysFound = invalidKeysFound || !validatedKey.isValid(); String validateError = validatedKey.getValidateError(); resultMessage.append(validatedKey.getKey()).append(" - ").append(validateError).append("\n"); } if (invalidKeysFound) { throw new BadRequestException(resultMessage.toString()); } } return new LicenseKeyEntities(licenseKeysManager.addKeys(keysToAdd), null, null, new Fields(fields), myBeanContext); } @GET @Path(LICENSING_KEYS + "/{licenseKey}") @Produces({"application/xml", "application/json"}) public LicenseKeyEntity getLicenseKey(@PathParam("licenseKey") final String licenseKey, @QueryParam("fields") String fields) { myDataProvider.checkGlobalPermission(Permission.VIEW_SERVER_SETTINGS); LicenseKeysManager licenseKeysManager = myBeanContext.getSingletonService(BuildServerEx.class).getLicenseKeysManager(); LicenseKey key = getLicenseKey(licenseKey, licenseKeysManager); return new LicenseKeyEntity(key, licenseKeysManager.getLicenseList().getActiveLicenses().contains(key), new Fields(fields)); } @DELETE @Path(LICENSING_KEYS + "/{licenseKey}") public void deleteLicenseKey(@PathParam("licenseKey") final String licenseKey) { myDataProvider.checkGlobalPermission(Permission.CHANGE_SERVER_SETTINGS); LicenseKeysManager licenseKeysManager = myBeanContext.getSingletonService(BuildServerEx.class).getLicenseKeysManager(); getLicenseKey(licenseKey, licenseKeysManager); licenseKeysManager.removeKey(licenseKey); } @NotNull private static LicenseKey getLicenseKey(@NotNull final String licenseKey, @NotNull final LicenseKeysManager licenseKeysManager) { //todo: return actual license key data even if not added to the server LicenseList licenseList = licenseKeysManager.getLicenseList(); for (LicenseKey license : licenseList.getAllLicenses()) { if (licenseKey.equals(license.getKey())) { return license; } } throw new NotFoundException("No license with key '" + licenseKey + "' is found"); } @Path("/files/{areaId}") public FilesSubResource getFilesSubResource(@PathParam("areaId") final String areaId) { myPermissionChecker.checkGlobalPermission(getAreaPermission(areaId)); final String urlPrefix = getUrlPrefix(areaId); return new FilesSubResource(new FilesSubResource.Provider() { @Override @NotNull public Element getElement(@NotNull final String path) { return BuildArtifactsFinder.getItem(getAreaRoot(areaId), path, "server " + areaId, myBeanContext.getServiceLocator()); } @Override @NotNull public String getArchiveName(@NotNull final String path) { String nodeIdPart = ""; if (!CurrentNodeInfo.isMainNode()) { //assuming there is only single main server and it does not need node id in the file name nodeIdPart = "_" + CurrentNodeInfo.getNodeId().toLowerCase(); } return "server_" + nodeIdPart + areaId + (StringUtil.isEmpty(path) ? "" : "-" + path.replaceAll("[^a-zA-Z0-9-#.]+", "_")); } @NotNull @Override public String preprocess(@Nullable final String path) { String result = super.preprocess(path); result = StringUtil.replace(result, "%timestamp%", new SimpleDateFormat("yyyy-MM-dd_HHmm").format(new Date())); return result; } @Override public boolean fileContentServed(@Nullable final String path, @NotNull final HttpServletRequest request) { Loggers.AUTH.info("Served file \"" + path + "\" from server's \"" + areaId + "\" for request " + WebUtil.getRequestDump(request)); return super.fileContentServed(path, request); } }, urlPrefix, myBeanContext, false); } @NotNull private String getUrlPrefix(final String areaId) { return Util.concatenatePath(myBeanContext.getContextService(ApiUrlBuilder.class).transformRelativePath(API_SERVER_URL), "/files/", areaId); } @NotNull private File getAreaRoot(final @PathParam("areaId") String areaId) { File rootPath; if ("logs".equals(areaId)) { rootPath = myDataProvider.getBean(ServerPaths.class).getLogsPath(); } else if ("backups".equals(areaId)) { rootPath = new File(myDataProvider.getBean(ServerPaths.class).getBackupDir()); } else if ("dataDirectory".equals(areaId)) { rootPath = myDataProvider.getBean(ServerPaths.class).getDataDirectory(); }/*else if (!StringUtil.isEmpty(areaId) && areaId.startsWith("custom.")) { final String customAreaId = areaId.substring("custom.".length()); rootPath = new File(TeamCityProperties.getProperty("rest.request.server.files.customArea." + customAreaId)); }*/ else { throw new BadRequestException("Unknown area id '" + areaId + "'. Known are: " + "logs, backups, dataDirectory"); } return rootPath; } @NotNull private Permission getAreaPermission(final @PathParam("areaId") String areaId) { return "logs".equals(areaId) ? Permission.MANAGE_SERVER_INSTALLATION : Permission.VIEW_SERVER_SETTINGS; } }