diff --git a/app/logbook/olog/client-es/pom.xml b/app/logbook/olog/client-es/pom.xml index 6276c06ffe..0f1a6b60fa 100644 --- a/app/logbook/olog/client-es/pom.xml +++ b/app/logbook/olog/client-es/pom.xml @@ -11,19 +11,9 @@ - com.sun.jersey - jersey-core - 1.19 - - - com.sun.jersey - jersey-client - 1.19 - - - com.sun.jersey.contribs - jersey-multipart - 1.19 + jakarta.ws.rs + jakarta.ws.rs-api + 4.0.0 com.fasterxml.jackson.datatype diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java index 256a034c9a..eee6dc43ad 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java @@ -2,7 +2,7 @@ import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogFactory; -import org.phoebus.olog.es.api.OlogClient.OlogClientBuilder; +import org.phoebus.olog.es.api.OlogHttpClient; import org.phoebus.security.tokens.SimpleAuthenticationToken; import java.util.logging.Level; @@ -33,7 +33,7 @@ public String getId() { @Override public LogClient getLogClient() { try { - return OlogClientBuilder.serviceURL().create(); + return OlogHttpClient.builder().build(); } catch (Exception e) { logger.log(Level.SEVERE, "Failed to create olog es client", e); } @@ -50,8 +50,8 @@ public LogClient getLogClient(Object authToken) { try { if (authToken instanceof SimpleAuthenticationToken) { SimpleAuthenticationToken token = (SimpleAuthenticationToken) authToken; - return OlogClientBuilder.serviceURL().withHTTPAuthentication(true).username(token.getUsername()).password(token.getPassword()) - .create(); + return OlogHttpClient.builder().username(token.getUsername()).password(token.getPassword()) + .build(); } else { return getLogClient(); } diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java index 5ea44a17a1..4332b5ebbd 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java @@ -18,8 +18,7 @@ package org.phoebus.applications.logbook.authentication; -import org.phoebus.olog.es.api.OlogClient; -import org.phoebus.olog.es.api.OlogClient.OlogClientBuilder; +import org.phoebus.olog.es.api.OlogHttpClient; import org.phoebus.security.authorization.ServiceAuthenticationProvider; import org.phoebus.security.tokens.AuthenticationScope; @@ -30,12 +29,11 @@ public class OlogServiceAuthenticationProvider implements ServiceAuthenticationP @Override public void authenticate(String username, String password){ - OlogClient ologClient = OlogClientBuilder.serviceURL().create(); try { - ologClient.authenticate(username, password); + OlogHttpClient.builder().build().authenticate(username, password); } catch (Exception e) { Logger.getLogger(OlogServiceAuthenticationProvider.class.getName()) - .log(Level.WARNING, "Failed to authenticate user " + username + " against Olog service", e); + .log(Level.WARNING, "Failed to authenticate user " + username + " with logbook service", e); throw new RuntimeException(e); } } diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java deleted file mode 100644 index 52a2ed6657..0000000000 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java +++ /dev/null @@ -1,559 +0,0 @@ -package org.phoebus.olog.es.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientHandlerException; -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.ClientResponse.Status; -import com.sun.jersey.api.client.UniformInterfaceException; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.api.client.config.ClientConfig; -import com.sun.jersey.api.client.config.DefaultClientConfig; -import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; -import com.sun.jersey.core.util.MultivaluedMapImpl; -import com.sun.jersey.multipart.FormDataBodyPart; -import com.sun.jersey.multipart.FormDataMultiPart; -import com.sun.jersey.multipart.file.FileDataBodyPart; -import com.sun.jersey.multipart.impl.MultiPartWriter; -import org.phoebus.logbook.Attachment; -import org.phoebus.logbook.LogClient; -import org.phoebus.logbook.LogEntry; -import org.phoebus.logbook.LogEntryChangeHandler; -import org.phoebus.logbook.LogTemplate; -import org.phoebus.logbook.Logbook; -import org.phoebus.logbook.LogbookException; -import org.phoebus.logbook.Messages; -import org.phoebus.logbook.Property; -import org.phoebus.logbook.SearchResult; -import org.phoebus.logbook.Tag; -import org.phoebus.olog.es.api.model.OlogLog; -import org.phoebus.olog.es.api.model.OlogObjectMappers; -import org.phoebus.olog.es.api.model.OlogSearchResult; -import org.phoebus.olog.es.authentication.LoginCredentials; -import org.phoebus.security.store.SecureStore; -import org.phoebus.security.tokens.AuthenticationScope; -import org.phoebus.security.tokens.ScopedAuthenticationToken; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.UriBuilder; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A client to the Phoebus-Olog webservice - * - * @author Kunal Shroff - */ -public class OlogClient implements LogClient { - private static final Logger logger = Logger.getLogger(OlogClient.class.getName()); - private final WebResource service; - - private static final String OLOG_CLIENT_INFO_HEADER = "X-Olog-Client-Info"; - private static final String CLIENT_INFO = - "CS Studio " + Messages.AppVersion + " on " + System.getProperty("os.name"); - - private List changeHandlers = new ArrayList<>(); - - /** - * Builder Class to help create an Olog client. - * - * @author shroffk - */ - public static class OlogClientBuilder { - // required - private final URI ologURI; - - // optional - private boolean withHTTPAuthentication = true; - - private ClientConfig clientConfig = null; - @SuppressWarnings("unused") - private SSLContext sslContext = null; - private final String protocol; - private String username = null; - private String password = null; - - private OlogClientBuilder() { - this.ologURI = URI.create(Preferences.olog_url); - this.protocol = this.ologURI.getScheme(); - } - - /** - * Creates a {@link OlogClientBuilder} for a CF client to Default URL in the - * channelfinder.properties. - * - * @return The builder - */ - public static OlogClientBuilder serviceURL() { - return new OlogClientBuilder(); - } - - /** - * Enable of Disable the HTTP authentication on the client connection. - * - * @param withHTTPAuthentication Whether to use authentication or not - * @return {@link OlogClientBuilder} - */ - public OlogClientBuilder withHTTPAuthentication(boolean withHTTPAuthentication) { - this.withHTTPAuthentication = withHTTPAuthentication; - return this; - } - - /** - * Set the username to be used for HTTP Authentication. - * - * @param username User's identity - * @return {@link OlogClientBuilder} - */ - public OlogClientBuilder username(String username) { - this.username = username; - return this; - } - - /** - * Set the password to be used for the HTTP Authentication. - * - * @param password User's password - * @return {@link OlogClientBuilder} - */ - public OlogClientBuilder password(String password) { - this.password = password; - return this; - } - - @SuppressWarnings("unused") - private OlogClientBuilder withSSLContext(SSLContext sslContext) { - this.sslContext = sslContext; - return this; - } - - public OlogClient create() { - if (this.protocol.equalsIgnoreCase("http")) { //$NON-NLS-1$ - this.clientConfig = new DefaultClientConfig(); - } else if (this.protocol.equalsIgnoreCase("https")) { //$NON-NLS-1$ - OlogTrustManager.setupSSLTrust(this.ologURI.getHost(), this.ologURI.getPort()); - if (this.clientConfig == null) { - this.clientConfig = new DefaultClientConfig(); - } - } - if (this.username == null || this.password == null) { - ScopedAuthenticationToken scopedAuthenticationToken = getCredentialsFromSecureStore(); - if (scopedAuthenticationToken != null) { - this.username = scopedAuthenticationToken.getUsername(); - this.password = scopedAuthenticationToken.getPassword(); - } else { - this.username = Preferences.username != null ? Preferences.username : this.username; - this.password = Preferences.password != null ? Preferences.password : this.password; - } - } - - int connectTimeout = Preferences.connectTimeout; - this.clientConfig.getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, connectTimeout); - - if (Preferences.permissive_hostname_verifier) { - HostnameVerifier allHostsValid = (hostname, session) -> true; - HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid); - } - - return new OlogClient(this.ologURI, this.clientConfig, this.withHTTPAuthentication, this.username, this.password); - } - - private ScopedAuthenticationToken getCredentialsFromSecureStore() { - try { - SecureStore secureStore = new SecureStore(); - return secureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); - } catch (Exception e) { - Logger.getLogger(OlogClientBuilder.class.getName()).log(Level.WARNING, "Unable to instantiate SecureStore", e); - return null; - } - } - } - - private OlogClient(URI ologURI, ClientConfig config, boolean withHTTPBasicAuthFilter, String username, String password) { - config.getClasses().add(MultiPartWriter.class); - - Client client = Client.create(config); - if (withHTTPBasicAuthFilter) { - client.addFilter(new HTTPBasicAuthFilter(username, password)); - } - if (Logger.getLogger(OlogClient.class.getName()).isLoggable(Level.ALL)) { - client.addFilter(new RawLoggingFilter(Logger.getLogger(OlogClient.class.getName()))); - } - client.setFollowRedirects(true); - client.setConnectTimeout(3000); - this.service = client.resource(UriBuilder.fromUri(ologURI).build()); - - ServiceLoader serviceLoader = ServiceLoader.load(LogEntryChangeHandler.class); - serviceLoader.stream().forEach(p -> changeHandlers.add(p.get())); - } - - @Override - public LogEntry set(LogEntry log) throws LogbookException { - LogEntry newEntry = save(log, null); - changeHandlers.forEach(h -> h.logEntryChanged(newEntry)); - return newEntry; - } - - /** - * Calls the back-end service to persist the log entry. - * - * @param log The log entry to save. - * @param inReplyTo If non-null, this save operation will treat the log parameter as a reply to - * the log entry represented by inReplyTo. - * @return The saved log entry. - * @throws LogbookException E.g. due to invalid log entry data. - */ - private LogEntry save(LogEntry log, LogEntry inReplyTo) throws LogbookException { - - try { - MultivaluedMap queryParams = new MultivaluedMapImpl(); - queryParams.putSingle("markup", "commonmark"); - if (inReplyTo != null) { - queryParams.putSingle("inReplyTo", Long.toString(inReplyTo.getId())); - } - - FormDataMultiPart form = new FormDataMultiPart(); - try { - form.bodyPart(new FormDataBodyPart("logEntry", OlogObjectMappers.logEntrySerializer.writeValueAsString(log), MediaType.APPLICATION_JSON_TYPE)); - } catch (JsonProcessingException e) { - logger.log(Level.SEVERE, "Got unexpected exception", e); - throw e; - } - log.getAttachments().forEach(attachment -> { - // Add all files as represented in the attachment objects. Note that each gets - // the "multipart name" files, but that is OK. - form.bodyPart(new FileDataBodyPart("files", attachment.getFile())); - }); - - ClientResponse clientResponse = service.path("logs") - .queryParams(queryParams) - .path("multipart") - .type(MediaType.MULTIPART_FORM_DATA) - .accept(MediaType.APPLICATION_JSON) - .put(ClientResponse.class, form); - - if (clientResponse.getStatus() > 300) { - logger.log(Level.SEVERE, "Failed to create log entry: " + clientResponse); - throw new LogbookException(clientResponse.toString()); - } - - return OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), OlogLog.class); - - } catch (UniformInterfaceException | ClientHandlerException | IOException e) { - logger.log(Level.SEVERE, "Failed to submit log entry, got client exception", e); - throw new LogbookException(e); - } - } - - @Override - public LogEntry reply(LogEntry log, LogEntry inReplyTo) throws LogbookException { - return save(log, inReplyTo); - } - - /** - * Returns a LogEntry that exactly matches the logId logId - * - * @param logId LogEntry id - * @return LogEntry object - */ - @Override - public LogEntry getLog(Long logId) { - return findLogById(logId); - } - - - @Override - public LogEntry findLogById(Long logId) { - try { - return OlogObjectMappers.logEntryDeserializer.readValue( - service - .path("logs") - .path(logId.toString()) - .accept(MediaType.APPLICATION_JSON).get(String.class), OlogLog.class); - } catch (JsonProcessingException e) { - return null; - } - } - - @Override - public List findLogs(Map map) throws RuntimeException { - throw new RuntimeException(new UnsupportedOperationException()); - } - - /** - * Retrieves {@link LogEntry}s matching the search criteria. Note that even if the matching {@link LogEntry}s - * may have a non-empty list of {@link Attachment}s, the {@link Attachment}s will NOT contains the actual - * data/content. This is essentially a lazy loading strategy to avoid fetching attachment data at this point. - * - * @param searchParams Map of search parameters/expressions - * @return A list of matching {@link LogEntry}s - * @throws RuntimeException If search fails, e.g. due to invalid search parameters - */ - private SearchResult findLogs(MultivaluedMap searchParams) throws RuntimeException { - try { - // Convert List into List - final OlogSearchResult ologSearchResult = OlogObjectMappers.logEntryDeserializer.readValue( - service.path("logs/search").queryParams(searchParams) - .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) - .accept(MediaType.APPLICATION_JSON) - .get(String.class), - OlogSearchResult.class); - return SearchResult.of(new ArrayList<>(ologSearchResult.getLogs()), - ologSearchResult.getHitCount()); - } catch (UniformInterfaceException | ClientHandlerException | IOException e) { - logger.log(Level.WARNING, "failed to retrieve log entries", e); - if (e instanceof UniformInterfaceException) { - if (((UniformInterfaceException) e).getResponse().getStatus() == Status.BAD_REQUEST.getStatusCode()) { - throw new RuntimeException(Messages.BadRequestFailure); - } - } - throw new RuntimeException(e); - } - } - - @Override - public List findLogsByLogbook(String logbookName) { - throw new RuntimeException(new UnsupportedOperationException()); - } - - @Override - public List findLogsByProperty(String propertyName) { - throw new RuntimeException(new UnsupportedOperationException()); - } - - @Override - public List findLogsByProperty(String propertyName, String attributeName, String attributeValue) { - HashMap map = new HashMap<>(); - map.put(propertyName + "." + attributeName, attributeValue); - return findLogs(map); - } - - @Override - public List findLogsBySearch(String pattern) { - throw new RuntimeException(new UnsupportedOperationException()); - } - - @Override - public List findLogsByTag(String tagName) { - throw new RuntimeException(new UnsupportedOperationException()); - } - - @Override - public InputStream getAttachment(Long logId, String attachmentName) { - ClientResponse response = service - .path("logs") - .path("attachments") - .path(logId.toString()) - .path(attachmentName).get(ClientResponse.class); - return response.getEntity(InputStream.class); - } - - @Override - public Collection listAttachments(Long logId) { - return getLog(logId).getAttachments(); - } - - @Override - public Collection listAttributes(String propertyName) { - try { - return (Collection) getProperty(propertyName).getAttributes(); - } catch (LogbookException e) { - logger.log(Level.WARNING, "Unable to get property attribute list from service", e); - return Collections.emptyList(); - } - } - - @Override - public List listLogs() { - return new ArrayList<>(); - } - - @Override - public Collection listLevels() { - return Arrays.stream(Preferences.levels).toList(); - } - - @Override - public Collection listLogbooks() { - try { - return OlogObjectMappers.logEntryDeserializer.readValue( - service.path("logbooks").accept(MediaType.APPLICATION_JSON).get(String.class), - new TypeReference>() { - }); - } catch (UniformInterfaceException | ClientHandlerException | IOException e) { - logger.log(Level.WARNING, "Unable to get logbooks from service", e); - return Collections.emptySet(); - } - } - - @Override - public Collection listProperties() { - try { - return OlogObjectMappers.logEntryDeserializer.readValue( - service.path("properties").accept(MediaType.APPLICATION_JSON).get(String.class), - new TypeReference>() { - }); - } catch (UniformInterfaceException | ClientHandlerException | IOException e) { - logger.log(Level.WARNING, "failed to list olog properties", e); - return Collections.emptySet(); - } - } - - @Override - public Collection listTags() { - try { - return OlogObjectMappers.logEntryDeserializer.readValue( - service.path("tags").accept(MediaType.APPLICATION_JSON).get(String.class), - new TypeReference>() { - }); - } catch (UniformInterfaceException | ClientHandlerException | IOException e) { - logger.log(Level.WARNING, "failed to retrieve olog tags", e); - return Collections.emptySet(); - } - } - - @Override - public String getServiceUrl() { - return Preferences.olog_url; - } - - @Override - public LogEntry update(LogEntry logEntry) { - ClientResponse clientResponse; - - try { - clientResponse = service.path("logs/" + logEntry.getId()) - .queryParam("markup", "commonmark") - .type(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_XML) - .accept(MediaType.APPLICATION_JSON) - .post(ClientResponse.class, OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntry)); - LogEntry updated = OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), OlogLog.class); - changeHandlers.forEach(h -> h.logEntryChanged(updated)); - return updated; - } catch (Exception e) { - logger.log(Level.SEVERE, "Unable to update log entry id=" + logEntry.getId(), e); - return null; - } - } - - @Override - public SearchResult search(Map map) { - MultivaluedMap mMap = new MultivaluedMapImpl(); - map.forEach(mMap::putSingle); - return findLogs(mMap); - } - - @Override - public void groupLogEntries(List logEntryIds) throws LogbookException { - try { - ClientResponse clientResponse = service.path("logs/group") - .type(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_XML) - .accept(MediaType.APPLICATION_JSON) - .post(ClientResponse.class, OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntryIds)); - if (clientResponse.getStatus() == Status.UNAUTHORIZED.getStatusCode()) { - throw new LogbookException("Failed to group log entries: user unauthorized"); - } else if (clientResponse.getStatus() != Status.OK.getStatusCode()) { - throw new LogbookException("Failed to group log entries: " + clientResponse.getStatus()); - } - } catch (JsonProcessingException e) { - logger.log(Level.SEVERE, "Failed to group log entries", e); - throw new LogbookException(e); - } - } - - /** - * Logs in to the Olog service. - * - * @param username Username, must not be null. - * @param password Password, must not be null. - * @throws Exception if the login fails, e.g. bad credentials or service off-line. - */ - public void authenticate(String username, String password) throws Exception { - try { - ClientResponse clientResponse = service.path("login") - .type(MediaType.APPLICATION_JSON) - .post(ClientResponse.class, OlogObjectMappers.logEntrySerializer.writeValueAsString(new LoginCredentials(username, password))); - if (clientResponse.getStatus() == Status.UNAUTHORIZED.getStatusCode()) { - throw new Exception("Failed to login: user unauthorized"); - } else if (clientResponse.getStatus() != Status.OK.getStatusCode()) { - throw new Exception("Failed to login, got HTTP status " + clientResponse.getStatus()); - } - } catch (Exception e) { - logger.log(Level.SEVERE, "Failed to log in to Olog service", e); - throw e; - } - } - - @Override - public String serviceInfo() { - ClientResponse clientResponse = service.path("").get(ClientResponse.class); - return clientResponse.getEntity(String.class); - } - - @Override - public SearchResult getArchivedEntries(long id) { - try { - final OlogSearchResult ologSearchResult = OlogObjectMappers.logEntryDeserializer.readValue( - service.path("logs/archived/" + id) - .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) - .accept(MediaType.APPLICATION_JSON) - .get(String.class), - OlogSearchResult.class); - return SearchResult.of(new ArrayList<>(ologSearchResult.getLogs()), - ologSearchResult.getHitCount()); - } catch (UniformInterfaceException | ClientHandlerException | IOException e) { - logger.log(Level.WARNING, "failed to retrieve archived log entries", e); - throw new RuntimeException(e); - } - } - - @Override - public Collection getTemplates() { - try { - return OlogObjectMappers.logEntryDeserializer.readValue( - service.path("templates").accept(MediaType.APPLICATION_JSON).get(String.class), - new TypeReference>() { - }); - } catch (UniformInterfaceException | ClientHandlerException | IOException e) { - logger.log(Level.WARNING, "Unable to get templates from service", e); - return Collections.emptySet(); - } - } - - @Override - public LogTemplate saveTemplate(LogTemplate template) throws LogbookException { - ClientResponse clientResponse = service.path("templates").accept(MediaType.APPLICATION_JSON_TYPE) - .header("Content-Type", MediaType.APPLICATION_JSON_TYPE) - .put(ClientResponse.class, template); - if (clientResponse.getStatus() > 300) { - logger.log(Level.SEVERE, "Failed to create template: " + clientResponse); - throw new LogbookException(clientResponse.toString()); - } - - try { - return OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), LogTemplate.class); - } catch (IOException e) { - logger.log(Level.SEVERE, "Failed to submit template, got client exception", e); - throw new LogbookException(e); - } - - } -} diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java new file mode 100644 index 0000000000..e8e871620b --- /dev/null +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java @@ -0,0 +1,558 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.olog.es.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import org.phoebus.logbook.Attachment; +import org.phoebus.logbook.LogClient; +import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.LogEntryChangeHandler; +import org.phoebus.logbook.LogTemplate; +import org.phoebus.logbook.Logbook; +import org.phoebus.logbook.LogbookException; +import org.phoebus.logbook.Messages; +import org.phoebus.logbook.Property; +import org.phoebus.logbook.SearchResult; +import org.phoebus.logbook.Tag; +import org.phoebus.olog.es.api.model.OlogLog; +import org.phoebus.olog.es.api.model.OlogObjectMappers; +import org.phoebus.olog.es.api.model.OlogSearchResult; +import org.phoebus.olog.es.authentication.LoginCredentials; +import org.phoebus.security.store.SecureStore; +import org.phoebus.security.tokens.AuthenticationScope; +import org.phoebus.security.tokens.ScopedAuthenticationToken; +import org.phoebus.util.http.HttpRequestMultipartBody; + +import java.io.InputStream; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Logbook client implementation using Java native APIs only. Implemented as singleton. + */ +public class OlogHttpClient implements LogClient { + + private final HttpClient httpClient; + private static final ObjectMapper OBJECT_MAPPER; + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String OLOG_CLIENT_INFO_HEADER = "X-Olog-Client-Info"; + private static final String CLIENT_INFO = + "CS Studio " + Messages.AppVersion + " on " + System.getProperty("os.name"); + + private static final Logger LOGGER = Logger.getLogger(OlogHttpClient.class.getName()); + private final List changeHandlers = new ArrayList<>(); + + private String basicAuthenticationHeader; + + static { + System.getProperties().setProperty("jdk.internal.httpclient.disableHostnameVerification", + Boolean.toString(Preferences.permissive_hostname_verifier)); + OBJECT_MAPPER = new ObjectMapper(); + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + public static class Builder { + private String username = null; + private String password = null; + + private Builder() { + + } + + public Builder username(String userName) { + this.username = userName; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + public OlogHttpClient build() { + if (this.username == null || this.password == null) { + ScopedAuthenticationToken scopedAuthenticationToken = getCredentialsFromSecureStore(); + if (scopedAuthenticationToken != null) { + this.username = scopedAuthenticationToken.getUsername(); + this.password = scopedAuthenticationToken.getPassword(); + } + } + return new OlogHttpClient(this.username, this.password); + } + + private ScopedAuthenticationToken getCredentialsFromSecureStore() { + try { + SecureStore secureStore = new SecureStore(); + return secureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); + } catch (Exception e) { + Logger.getLogger(OlogHttpClient.class.getName()).log(Level.WARNING, "Unable to instantiate SecureStore", e); + return null; + } + } + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Disallow instantiation. + */ + private OlogHttpClient(String userName, String password) { + httpClient = HttpClient.newBuilder() + .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL)) + .followRedirects(HttpClient.Redirect.ALWAYS) + // HttpClient rejects Duration.ZERO for the connect timeout value. + // To support infinite timeout (preference value == 0), use Long.MAX_VALUE + .connectTimeout(Duration.ofMillis(Preferences.connectTimeout <= 0 ? Long.MAX_VALUE : Preferences.connectTimeout)) + .build(); + + if (userName != null && password != null) { + this.basicAuthenticationHeader = "Basic " + Base64.getEncoder().encodeToString((userName + ":" + password).getBytes()); + } + + ServiceLoader serviceLoader = ServiceLoader.load(LogEntryChangeHandler.class); + serviceLoader.stream().forEach(p -> changeHandlers.add(p.get())); + } + + @Override + public LogEntry set(LogEntry log) throws LogbookException { + return save(log, null); + } + + /** + * Calls the back-end service to persist the log entry. + * + * @param log The log entry to save. + * @param inReplyTo If non-null, this save operation will treat the log parameter as a reply to + * the log entry represented by inReplyTo. + * @return The saved log entry. + * @throws LogbookException E.g. due to invalid log entry data, or if attachment content type + * cannot be determined. + */ + private LogEntry save(LogEntry log, LogEntry inReplyTo) throws LogbookException { + try { + MultivaluedMap queryParams = new MultivaluedHashMap<>(); + queryParams.putSingle("markup", "commonmark"); + if (inReplyTo != null) { + queryParams.putSingle("inReplyTo", Long.toString(inReplyTo.getId())); + } + + HttpRequestMultipartBody httpRequestMultipartBody = new HttpRequestMultipartBody(); + httpRequestMultipartBody.addTextPart("logEntry", OlogObjectMappers.logEntrySerializer.writeValueAsString(log), "application/json"); + + for (Attachment attachment : log.getAttachments()) { + httpRequestMultipartBody.addFilePart(attachment.getFile()); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/logs/multipart?" + mapToQueryParams(queryParams))) + .header("Content-Type", httpRequestMultipartBody.getContentType()) + .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) + .header("Authorization", basicAuthenticationHeader) + .PUT(HttpRequest.BodyPublishers.ofByteArray(httpRequestMultipartBody.getBytes())) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() >= 300) { + LOGGER.log(Level.SEVERE, "Failed to create log entry: " + response.body()); + throw new LogbookException(response.body()); + } else { + return OlogObjectMappers.logEntryDeserializer.readValue(response.body(), OlogLog.class); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to submit log entry, got client exception", e); + throw new LogbookException(e); + } + } + + @Override + public LogEntry reply(LogEntry log, LogEntry inReplyTo) throws LogbookException { + return save(log, inReplyTo); + } + + /** + * Returns a LogEntry that exactly matches the logId logId + * + * @param logId LogEntry id + * @return LogEntry object + */ + @Override + public LogEntry getLog(Long logId) { + return findLogById(logId); + } + + @Override + public LogEntry findLogById(Long logId) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/logs/" + logId)) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return OlogObjectMappers.logEntryDeserializer.readValue(response.body(), OlogLog.class); + } catch (Exception e) { + return null; + } + } + + @Override + public List findLogs(Map map) { + throw new RuntimeException(new UnsupportedOperationException()); + } + + /** + * Calls service to retrieve log entries based on the search parameters + * + * @param searchParams Potentially empty map of multi-valued search parameters. + * @return A {@link SearchResult} containing log entries matching search parameters. + * @throws RuntimeException If error occurs, e.g. bad request due to unsupported or malformed search parameter(s). + */ + private SearchResult findLogs(MultivaluedMap searchParams) throws RuntimeException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + + "/logs/search?" + mapToQueryParams(searchParams))) + .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + OlogSearchResult searchResult = OlogObjectMappers.logEntryDeserializer.readValue(response.body(), OlogSearchResult.class); + return SearchResult.of(new ArrayList<>(searchResult.getLogs()), + searchResult.getHitCount()); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "failed to retrieve log entries", e); + throw new RuntimeException(e); + } + } + + @Override + public List findLogsByLogbook(String logbookName) { + throw new RuntimeException(new UnsupportedOperationException()); + } + + @Override + public List findLogsByProperty(String propertyName) { + throw new RuntimeException(new UnsupportedOperationException()); + } + + @Override + public List findLogsBySearch(String pattern) { + throw new RuntimeException(new UnsupportedOperationException()); + } + + @Override + public List findLogsByTag(String tagName) { + throw new RuntimeException(new UnsupportedOperationException()); + } + + @Override + public String getServiceUrl() { + return Preferences.olog_url; + } + + /** + * Updates an existing {@link LogEntry}. Note that unlike the {@link #save(LogEntry, LogEntry)} API, + * this does not support attachments, i.e. it does not set up a multipart request to the service. + * + * @param logEntry - the updated log entry + * @return The updated {@link LogEntry} + */ + @Override + public LogEntry update(LogEntry logEntry) { + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/logs/" + logEntry.getId() + "?markup=commonmark")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", basicAuthenticationHeader) + .POST(HttpRequest.BodyPublishers.ofString(OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntry))) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + LogEntry updated = OlogObjectMappers.logEntryDeserializer.readValue(response.body(), OlogLog.class); + changeHandlers.forEach(h -> h.logEntryChanged(updated)); + return updated; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Unable to update log entry id=" + logEntry.getId(), e); + return null; + } + } + + @Override + public SearchResult search(Map map) { + MultivaluedMap mMap = new MultivaluedHashMap<>(); + map.forEach(mMap::putSingle); + return findLogs(mMap); + } + + /** + * Puts user selected log entries into a group. + * + * @param logEntryIds List of log entry ids + * @throws LogbookException If operation fails, e.g. user unauthorized + */ + @Override + public void groupLogEntries(List logEntryIds) throws LogbookException { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/logs/group")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", basicAuthenticationHeader) + .POST(HttpRequest.BodyPublishers.ofString(OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntryIds))) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 401) { + throw new LogbookException("Failed to group log entries: user unauthorized"); + } else if (response.statusCode() != 200) { + throw new LogbookException("Failed to group log entries: " + response.statusCode()); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to group log entries", e); + throw new LogbookException(e); + } + } + + /** + * Logs in to the Olog service. + * + * @param userName Username, must not be null. + * @param password Password, must not be null. + * @throws Exception if the login fails, e.g. bad credentials or service off-line. + */ + public void authenticate(String userName, String password) throws Exception { + + String stringBuilder = Preferences.olog_url + + "/login"; + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(stringBuilder)) + .header("Content-Type", CONTENT_TYPE_JSON) + .POST(HttpRequest.BodyPublishers.ofString(OBJECT_MAPPER.writeValueAsString(new LoginCredentials(userName, password)))) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 401) { + throw new Exception("Failed to login: user unauthorized"); + } else if (response.statusCode() != 200) { + throw new Exception("Failed to login, got HTTP status " + response.statusCode()); + } + } + + /** + * @return A JSON string containing server side information, e.g. version, Elasticsearch status etc. + */ + @Override + public String serviceInfo() { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url)) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return response.body(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "failed to obtain service info", e); + return ""; + } + } + + @Override + public Collection listAttachments(Long logId) { + return getLog(logId).getAttachments(); + } + + + @Override + public Collection listLogs() { + return List.of(); + } + + + @Override + public Collection listLogbooks() { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/logbooks")) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return OlogObjectMappers.logEntryDeserializer.readValue(response.body(), new TypeReference>() { + }); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to get logbooks from service", e); + return Collections.emptySet(); + } + } + + @Override + public Collection listTags() { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/tags")) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return OlogObjectMappers.logEntryDeserializer.readValue(response.body(), new TypeReference>() { + }); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "failed to retrieve logbook tags", e); + return Collections.emptySet(); + } + } + + @Override + public Collection listProperties() { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/properties")) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return OlogObjectMappers.logEntryDeserializer.readValue(response.body(), new TypeReference>() { + }); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "failed to retrieve logbook properties", e); + return Collections.emptySet(); + } + } + + + @Override + public InputStream getAttachment(Long logId, String attachmentName) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/logs/attachments/" + logId + "/" + URLEncoder.encode(attachmentName, StandardCharsets.UTF_8))) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + return response.body(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "failed to obtain attachment", e); + return null; + } + } + + + private String mapToQueryParams(MultivaluedMap map) { + StringBuilder stringBuilder = new StringBuilder(); + map.keySet().forEach(k -> { + List value = map.get(k); + if (value != null && !value.isEmpty()) { + stringBuilder.append(k).append("="); + stringBuilder.append(String.join(",", + value.stream().map(v -> URLEncoder.encode(v, StandardCharsets.UTF_8)).toList())); + stringBuilder.append("&"); + } + }); + return stringBuilder.toString(); + } + + /** + * @param id Unique log entry id + * @return A {@link SearchResult} containing a list of {@link LogEntry} objects representing the + * history, i.e. previous edits, if any. + */ + @Override + public SearchResult getArchivedEntries(long id) { + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + + "/logs/archived/" + id)) + .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + OlogSearchResult ologSearchResult = OlogObjectMappers.logEntryDeserializer.readValue(response.body(), OlogSearchResult.class); + return SearchResult.of(new ArrayList<>(ologSearchResult.getLogs()), + ologSearchResult.getHitCount()); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "failed to retrieve archived log entries", e); + throw new RuntimeException(e); + } + } + + @Override + public Collection getTemplates() { + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + + "/templates")) + .header("Content-Type", CONTENT_TYPE_JSON) + .GET() + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return OlogObjectMappers.logEntryDeserializer.readValue( + response.body(), new TypeReference>() { + }); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to get templates from service", e); + return Collections.emptySet(); + } + } + + /** + * @param template A new {@link LogTemplate} + * @return The persisted {@link LogTemplate} + * @throws LogbookException if operation fails + */ + @Override + public LogTemplate saveTemplate(LogTemplate template) throws LogbookException { + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + "/templates")) + .header("Content-Type", CONTENT_TYPE_JSON) + .header("Authorization", basicAuthenticationHeader) + .PUT(HttpRequest.BodyPublishers.ofString(OlogObjectMappers.logEntrySerializer.writeValueAsString(template))) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() > 300) { + LOGGER.log(Level.SEVERE, "Failed to create template: " + response.body()); + throw new LogbookException(response.body()); + } + return OlogObjectMappers.logEntryDeserializer.readValue(response.body(), LogTemplate.class); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to submit template, got client exception", e); + throw new LogbookException(e); + } + } +} diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogTrustManager.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogTrustManager.java deleted file mode 100644 index a29d0a26b5..0000000000 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogTrustManager.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2020 European Spallation Source ERIC. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -package org.phoebus.olog.es.api; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Ideas stolen from https://github.com/escline/InstallCert. - */ -public class OlogTrustManager { - - private static final Logger LOGGER = Logger.getLogger(OlogTrustManager.class.getName()); - - /** - * This is the default password for the JVM certificate store - */ - private static final String CACERTS_PASSWORD = "changeit"; - - /** - * Attempts to download certificates as presented by the remote service. If the remote service does not - * resent any certificates (unlikely?), or if the presented certificates are trusted, this method does nothing. - * If on the other hand the certificates are not trusted, they are imported into the JVM trust store. Note - * however that the augmented trust store is in-memory only, a key/certificate file is NOT created. - * @param ologHost Host name of the remote service, no protocol prefix, no trailing path. - * @param ologPort The port number. A value of -1 will be interpreted as default HTTPS port 443. - */ - public static void setupSSLTrust(String ologHost, int ologPort) { - - File cacertsDir = new File(System.getProperty("java.home") + File.separatorChar + "lib" + File.separatorChar + "security"); - LOGGER.log(Level.INFO, String.format("Loading default JVM certificates from %s", cacertsDir.getAbsolutePath())); - - // Certificates *may* be found in a file named jssecacerts. If not, assume cacerts. - File cacertsFile = new File(cacertsDir, "jssecacerts"); - if(!cacertsFile.isFile()){ - cacertsFile = new File(cacertsDir, "cacerts"); - } - if(!cacertsFile.isFile()){ - LOGGER.log(Level.INFO, "No certificate store found, skipping certificate installation."); - return; - } - - try(InputStream in = new FileInputStream(cacertsFile)){ - - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(in, CACERTS_PASSWORD.toCharArray()); - SSLContext context = SSLContext.getInstance("TLS"); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(keyStore); - - X509TrustManager defaultTrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0]; - SavingTrustManager trustManager = new SavingTrustManager(defaultTrustManager); - context.init(null, new TrustManager[] { trustManager}, null); - SSLSocketFactory sslSocketFactory = context.getSocketFactory(); - - try(SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(ologHost, ologPort == -1 ? 443 : ologPort)){ - socket.setSoTimeout(5000); - LOGGER.log(Level.INFO, String.format("Attempting SSL handshake against %s", ologHost)); - socket.startHandshake(); - LOGGER.log(Level.INFO, "SSL Handshake succeeded, server certificate already trusted."); - return; - } - catch(SSLException sslException){ - LOGGER.log(Level.INFO, "SSL Handshake failed, certificate is not trusted, will be imported."); - } - - // The custom TrustManager holds a reference to the certificates as "downloaded" from - // the remote service URL. - X509Certificate[] chain = trustManager.certificateChain; - if (chain == null) { - LOGGER.log(Level.SEVERE, "Could not obtain server certificate chain."); - return; - } - LOGGER.log(Level.INFO,String.format("Server sent %d certificate(s)", chain.length)); - for (int i = 0; i < chain.length; i++) { - X509Certificate cert = chain[i]; - LOGGER.log(Level.INFO, String.format("%d Subject %s", i + 1, cert.getSubjectDN())); - LOGGER.log(Level.INFO,String.format("%d Issuer %s", i + 1, cert.getIssuerDN())); - String alias = ologHost + i; - keyStore.setCertificateEntry(alias, cert); - } - - // Now the key store should contain additional certificates, if successfully downloaded. - // Initialize the trust factory and SSL context again with the augmented key store. - trustManagerFactory.init(keyStore); - defaultTrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0]; - context.init(null, new TrustManager[] { defaultTrustManager }, null); - SSLContext.setDefault(context); - } - catch (Exception e) { - LOGGER.log(Level.SEVERE, "unexpected error occured during certificate import", e); - } - } - - /** - * A {@link TrustManager} used to obtain the certificate chain from the remote service that is - * using an untrusted certificate. - */ - private static class SavingTrustManager implements X509TrustManager { - - private final X509TrustManager trustManager; - private X509Certificate[] certificateChain; - - /** - * @param trustManager The default JVM {@link TrustManager}. - */ - SavingTrustManager(X509TrustManager trustManager) { - this.trustManager = trustManager; - } - - public X509Certificate[] getAcceptedIssuers() { - return this.trustManager.getAcceptedIssuers(); - } - - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException{ - this.trustManager.checkClientTrusted(chain, authType); - } - - public void checkServerTrusted(X509Certificate[] certificateChain, String authType) throws CertificateException{ - this.certificateChain = certificateChain; - trustManager.checkServerTrusted(certificateChain, authType); - } - } -} diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/Preferences.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/Preferences.java index 761e59c199..db1c2d2750 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/Preferences.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/Preferences.java @@ -18,15 +18,6 @@ public class Preferences { @Preference public static boolean permissive_hostname_verifier; - @Preference - public static String username; - - @Preference - public static String password; - - @Preference - public static boolean debug; - @Preference public static String[] levels; static diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/RawLoggingFilter.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/RawLoggingFilter.java deleted file mode 100644 index d614beddef..0000000000 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/RawLoggingFilter.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common Development - * and Distribution License("CDDL") (collectively, the "License"). You - * may not use this file except in compliance with the License. You can obtain - * a copy of the License at https://jersey.dev.java.net/CDDL+GPL.html - * or jersey/legal/LICENSE.txt. See the License for the specific - * language governing permissions and limitations under the License. - * - * When distributing the software, include this License Header Notice in each - * file and include the License file at jersey/legal/LICENSE.txt. - * Sun designates this particular file as subject to the "Classpath" exception - * as provided by Sun in the GPL Version 2 section of the License file that - * accompanied this code. If applicable, add the following below the License - * Header, with the fields enclosed by brackets [] replaced by your own - * identifying information: "Portions Copyrighted [year] - * [name of copyright owner]" - * - * Contributor(s): - * - * If you wish your version of this file to be governed by only the CDDL or - * only the GPL Version 2, indicate your decision by adding "[Contributor] - * elects to include this software in this distribution under the [CDDL or GPL - * Version 2] license." If you don't indicate a single choice of license, a - * recipient has the option to distribute your version of this file under - * either the CDDL, the GPL Version 2 or to extend the choice of license to - * its licensees as provided above. However, if you add GPL Version 2 code - * and therefore, elected the GPL Version 2 license, then the option applies - * only if the new code is made subject to such option by the copyright - * holder. - */ -/* - * Copyright 2010 Brookhaven National Laboratory - * All rights reserved. Use is subject to license terms. - */ - - -package org.phoebus.olog.es.api; - -import com.sun.jersey.api.client.AbstractClientRequestAdapter; -import com.sun.jersey.api.client.ClientHandlerException; -import com.sun.jersey.api.client.ClientRequest; -import com.sun.jersey.api.client.ClientRequestAdapter; -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.filter.ClientFilter; -import com.sun.jersey.core.util.ReaderWriter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.ws.rs.core.MultivaluedMap; - -/** - * A Raw HTML request/response logging filter. - * added level check to the handle. - * - * @author Paul.Sandoz@Sun.Com, shroffk - */ -public class RawLoggingFilter extends ClientFilter { - - private static final Logger LOGGER = Logger - .getLogger(RawLoggingFilter.class.getName()); - - private static final String NOTIFICATION_PREFIX = "* "; - - private static final String REQUEST_PREFIX = "> "; - - private static final String RESPONSE_PREFIX = "< "; - - private final class Adapter extends AbstractClientRequestAdapter { - private final StringBuilder b; - - Adapter(ClientRequestAdapter cra, StringBuilder b) { - super(cra); - this.b = b; - } - - public OutputStream adapt(ClientRequest request, OutputStream out) - throws IOException { - return new LoggingOutputStream(getAdapter().adapt(request, out), b); - } - - } - - private final class LoggingOutputStream extends OutputStream { - private final OutputStream out; - - private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - private final StringBuilder b; - - LoggingOutputStream(OutputStream out, StringBuilder b) { - this.out = out; - this.b = b; - } - - @Override - public void write(byte[] b) throws IOException { - baos.write(b); - out.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - baos.write(b, off, len); - out.write(b, off, len); - } - - @Override - public void write(int b) throws IOException { - baos.write(b); - out.write(b); - } - - @Override - public void close() throws IOException { - printEntity(b, baos.toByteArray()); - log(b); - out.close(); - } - } - - private final PrintStream loggingStream; - - private final Logger logger; - - private long _id = 0; - - /** - * Create a logging filter logging the request and response to a default JDK - * logger, named as the fully qualified class name of this class. - */ - public RawLoggingFilter() { - this(LOGGER); - } - - /** - * Create a logging filter logging the request and response to a JDK logger. - * - * @param logger - * the logger to log requests and responses. - */ - public RawLoggingFilter(Logger logger) { - this.loggingStream = null; - this.logger = logger; - } - - /** - * Create a logging filter logging the request and response to print stream. - * - * @param loggingStream - * the print stream to log requests and responses. - */ - public RawLoggingFilter(PrintStream loggingStream) { - this.loggingStream = loggingStream; - this.logger = null; - } - - private void log(StringBuilder b) { - if (logger != null) { - logger.fine(b.toString()); - } else { - loggingStream.print(b); - } - } - - private StringBuilder prefixId(StringBuilder b, long id) { - b.append(Long.toString(id)).append(" "); - return b; - } - - @Override - public ClientResponse handle(ClientRequest request) - throws ClientHandlerException { - if (this.logger.isLoggable(Level.FINE)) { - long id = ++this._id; - logRequest(id, request); - - ClientResponse response = getNext().handle(request); - - logResponse(id, response); - - return response; - } else{ - return getNext().handle(request); - } - - } - - private void logRequest(long id, ClientRequest request) { - StringBuilder b = new StringBuilder(); - - printRequestLine(b, id, request); - printRequestHeaders(b, id, request.getHeaders()); - - if (request.getEntity() != null) { - request.setAdapter(new Adapter(request.getAdapter(), b)); - } else { - log(b); - } - } - - private void printRequestLine(StringBuilder b, long id, - ClientRequest request) { - prefixId(b, id).append(NOTIFICATION_PREFIX) - .append("Client out-bound request").append("\n"); - prefixId(b, id).append(REQUEST_PREFIX).append(request.getMethod()) - .append(" ").append(request.getURI().toASCIIString()) - .append("\n"); - } - - private void printRequestHeaders(StringBuilder b, long id, - MultivaluedMap headers) { - for (Map.Entry> e : headers.entrySet()) { - String header = e.getKey(); - for (Object value : e.getValue()) { - prefixId(b, id).append(REQUEST_PREFIX).append(header) - .append(": ") - .append(ClientRequest.getHeaderValue(value)) - .append("\n"); - } - } - prefixId(b, id).append(REQUEST_PREFIX).append("\n"); - } - - private void logResponse(long id, ClientResponse response) { - StringBuilder b = new StringBuilder(); - - printResponseLine(b, id, response); - printResponseHeaders(b, id, response.getHeaders()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - InputStream in = response.getEntityInputStream(); - try { - ReaderWriter.writeTo(in, out); - - byte[] requestEntity = out.toByteArray(); - printEntity(b, requestEntity); - response.setEntityInputStream(new ByteArrayInputStream( - requestEntity)); - } catch (IOException ex) { - throw new ClientHandlerException(ex); - } - log(b); - } - - private void printResponseLine(StringBuilder b, long id, - ClientResponse response) { - prefixId(b, id).append(NOTIFICATION_PREFIX) - .append("Client in-bound response").append("\n"); - prefixId(b, id).append(RESPONSE_PREFIX) - .append(Integer.toString(response.getStatus())).append("\n"); - } - - private void printResponseHeaders(StringBuilder b, long id, - MultivaluedMap headers) { - for (Map.Entry> e : headers.entrySet()) { - String header = e.getKey(); - for (String value : e.getValue()) { - prefixId(b, id).append(RESPONSE_PREFIX).append(header) - .append(": ").append(value).append("\n"); - } - } - prefixId(b, id).append(RESPONSE_PREFIX).append("\n"); - } - - private void printEntity(StringBuilder b, byte[] entity) throws IOException { - if (entity.length == 0) - return; - b.append(new String(entity)).append("\n"); - } -} \ No newline at end of file diff --git a/app/logbook/olog/client-es/src/main/resources/olog_es_preferences.properties b/app/logbook/olog/client-es/src/main/resources/olog_es_preferences.properties index 27f1562467..1153ca956b 100644 --- a/app/logbook/olog/client-es/src/main/resources/olog_es_preferences.properties +++ b/app/logbook/olog/client-es/src/main/resources/olog_es_preferences.properties @@ -5,14 +5,7 @@ # The olog url olog_url=http://localhost:8080/Olog -# User credentials for olog -username=admin -password=1234 - -# Enable debugging of http request and responses -debug=false - -# The connection timeout for the Jersey client, in ms. 0 = infinite. +# The connection timeout for HttpClient, in ms. 0 = infinite. connectTimeout=0 # Enable a permissive hostname verifier diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java index 5a38fe0ab1..6599501865 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java @@ -54,7 +54,6 @@ import org.phoebus.ui.javafx.ImageCache; import javax.imageio.ImageIO; -import javax.ws.rs.core.UriBuilder; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -214,7 +213,7 @@ else if(attachments.isEmpty()){ */ private void showImageAttachment() { URI uri = selectedAttachment.get().getFile().toURI(); - URI withWatermark = UriBuilder.fromUri(uri).queryParam("watermark", "true").build(); + URI withWatermark = URI.create(uri + "?watermark=true"); ApplicationLauncherService.openResource(withWatermark, false, null); } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 4ba91940d5..38aefb6231 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -48,8 +48,8 @@ import org.phoebus.logbook.SearchResult; import org.phoebus.logbook.olog.ui.query.OlogQuery; import org.phoebus.logbook.olog.ui.query.OlogQueryManager; -import org.phoebus.logbook.olog.ui.write.EditMode; import org.phoebus.logbook.olog.ui.spi.Decoration; +import org.phoebus.logbook.olog.ui.write.EditMode; import org.phoebus.logbook.olog.ui.write.LogEntryEditorStage; import org.phoebus.olog.es.api.model.LogGroupProperty; import org.phoebus.olog.es.api.model.OlogLog; @@ -406,6 +406,7 @@ public void setLogs(List logs) { } private List decorations; + protected void setDecorations(List decorations) { this.decorations = decorations; for (Decoration decoration : decorations) { diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchJob.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchJob.java index f6f78a2bd1..f65a7d4cd4 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchJob.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchJob.java @@ -1,16 +1,11 @@ package org.phoebus.logbook.olog.ui; -import javafx.beans.property.SimpleBooleanProperty; import org.phoebus.framework.jobs.Job; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.jobs.JobRunnableWithCancel; import org.phoebus.logbook.LogClient; -import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.SearchResult; -import org.phoebus.olog.es.api.OlogClient; -import org.phoebus.olog.es.api.model.OlogSearchResult; -import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -19,7 +14,7 @@ /** * Background job for searching log entries - * + * * @author Kunal Shroff, Kay Kasemir */ public class LogbookSearchJob extends JobRunnableWithCancel { @@ -30,10 +25,11 @@ public class LogbookSearchJob extends JobRunnableWithCancel { /** * Submit a logbook search query - * @param client the logbook client - * @param searchMap the search parameters + * + * @param client the logbook client + * @param searchMap the search parameters * @param logEntryHandler consumer for the {@link SearchResult} from the search - * @param errorHandler error handler + * @param errorHandler error handler * @return a logbook search job */ public static Job submit(LogClient client, final Map searchMap, @@ -43,7 +39,7 @@ public static Job submit(LogClient client, final Map searchMap, } private LogbookSearchJob(LogClient client, Map searchMap, Consumer logEntryHandler, - BiConsumer errorHandler) { + BiConsumer errorHandler) { super(); this.client = client; this.searchMap = searchMap; diff --git a/core/logbook/src/main/java/org/phoebus/logbook/LogService.java b/core/logbook/src/main/java/org/phoebus/logbook/LogService.java index 8a7c65b757..5c8ab421a5 100644 --- a/core/logbook/src/main/java/org/phoebus/logbook/LogService.java +++ b/core/logbook/src/main/java/org/phoebus/logbook/LogService.java @@ -2,39 +2,30 @@ import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.ServiceLoader; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Level; import java.util.logging.Logger; /** * A service for creating log entries into the registered log clients. - * + *

* TODO the service might be less confusing if it simply returned the registered * {@link LogFactory}'s and then has the calling code submit logging request to * the clients. - * - * @author Kunal Shroff * + * @author Kunal Shroff */ public class LogService { - /** Suggested logger for all log book related code. */ + /** + * Suggested logger for all log book related code. + */ public static final Logger logger = Logger.getLogger(LogService.class.getPackageName()); - static final java.lang.String SERVICE_NAME = "LoggingService"; - - public static final String AUTHENTICATION_SCOPE = "Logbook"; - private static LogService logService; private ServiceLoader loader; private Map logFactories; - private static ExecutorService executor = Executors.newCachedThreadPool(); - private LogService() { // Load available adapter factories logFactories = new HashMap(); @@ -47,7 +38,7 @@ private LogService() { /** * Returns the instance logbook service instance - * + * * @return */ public static LogService getInstance() { @@ -59,6 +50,7 @@ public static LogService getInstance() { /** * Get a registered log factory for creating logbook clients to the specified type of logbook service + * * @param logbookServiceId A string identifying the logbook service type * @return logbookFactory for creating clients to logbookServiceId */ @@ -68,29 +60,11 @@ public LogFactory getLogFactories(String logbookServiceId) { /** * Get a list of all the registered logbook factories + * * @return A Map of all the logbook factories */ public Map getLogFactories() { return Collections.unmodifiableMap(logFactories); } - /** - * Create a log entry in all registered LogFactory TODO change to Log type - * - * @param logEntries - * @param authToken - */ - public void createLogEntry(List logEntries, Object authToken) { - executor.submit(() -> { - logFactories.values().stream().forEach(logFactory -> { - logEntries.forEach(logEntry -> { - try { - logFactory.getLogClient(authToken).set(logEntry); - } catch (Exception e) { - logger.log(Level.WARNING, "failed to create log entry ", e); - } - }); - }); - }); - } } diff --git a/core/util/src/main/java/org/phoebus/util/http/HttpRequestMultipartBody.java b/core/util/src/main/java/org/phoebus/util/http/HttpRequestMultipartBody.java new file mode 100644 index 0000000000..2eab012066 --- /dev/null +++ b/core/util/src/main/java/org/phoebus/util/http/HttpRequestMultipartBody.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.util.http; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.SecureRandom; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + *

+ * Utility class for the purpose of creating a multipart HTTP request body. Supports text (String) and + * binary (files), or a mix. + * Inspired by this example. + *

+ *

+ * Each added part is appended as a byte array representation to a {@link ByteArrayOutputStream}. + * When all parts have been added, client code must call {@link #getBytes()} to acquire the body used in the call to a server, + * and must call {@link #getContentType()} to be able to set the correct Content-Type header of the request. + *

+ */ +public class HttpRequestMultipartBody { + + private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + private final String boundary; + + public HttpRequestMultipartBody() { + this.boundary = new BigInteger(256, new SecureRandom()).toString(); + } + + /** + * @return Content type string including the boundary string. + */ + public String getContentType() { + return "multipart/form-data; boundary=" + boundary; + } + + /** + * Adds text part of a multipart body + * + * @param fieldName A field name for the sake of identification on the service, e.g. to identify the + * text part of a log entry. + * @param value Value, e.g. JSON representation of a log entry. + * @param contentType Self-explanatory, e.g. application/json. + */ + public void addTextPart(String fieldName, String value, String contentType) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("\r\n--").append(boundary).append("\r\nContent-Disposition: form-data; name=\"").append(fieldName).append("\""); + stringBuilder.append("\r\nContent-Type: ").append(contentType).append("\r\n\r\n"); + stringBuilder.append(value); + byteArrayOutputStream.writeBytes(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); + } + + /** + * Adds contents of a {@link File} to the multipart request body. The content type is probed, but may fall back to + * application/octet-stream if probe fails. + * + * @param file A non-null file. + * @throws RuntimeException if the file does not exist or cannot be read. + */ + public void addFilePart(File file) { + if (file == null){ + throw new RuntimeException("File part must not be null"); + } + else if(!file.exists() || !file.canRead()){ + throw new RuntimeException("File " + file.getAbsolutePath() + " does not exist or cannot be read"); + } + StringBuilder stringBuilder = new StringBuilder(); + // Default generic content type... + String contentType = "application/octet-stream"; + try { + // ... but try to determine something more specific + String probedType = Files.probeContentType(file.toPath()); + if(probedType != null){ + contentType = probedType; + } + } catch (IOException e) { + Logger.getLogger(HttpRequestMultipartBody.class.getName()).log(Level.WARNING, "Unable to determine content type of file " + file.getAbsolutePath(), e); + } + stringBuilder.append("\r\n--").append(boundary).append("\r\nContent-Disposition: form-data; name=\"").append("files").append("\"; filename=\"").append(file.getName()).append("\""); + stringBuilder.append("\r\nContent-Type: ").append(contentType).append("\r\n\r\n"); + + byteArrayOutputStream.writeBytes(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); + try { + Files.copy(file.toPath(), byteArrayOutputStream); + } catch (IOException e) { + Logger.getLogger(HttpRequestMultipartBody.class.getName()).log(Level.WARNING, "Failed to copy content of file part", e); + } + } + + /** + * @return The body of the multipart request. + */ + public byte[] getBytes() { + // Add last boundary + byteArrayOutputStream.writeBytes(("\r\n--" + boundary + "--").getBytes(StandardCharsets.UTF_8)); + return byteArrayOutputStream.toByteArray(); + } +} diff --git a/core/util/src/test/java/org/phoebus/util/http/HttpRequestMultipartBodyTest.java b/core/util/src/test/java/org/phoebus/util/http/HttpRequestMultipartBodyTest.java new file mode 100644 index 0000000000..2c68990f45 --- /dev/null +++ b/core/util/src/test/java/org/phoebus/util/http/HttpRequestMultipartBodyTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.util.http; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HttpRequestMultipartBodyTest { + + @Test + public void testContentType() { + String contentType = new HttpRequestMultipartBody().getContentType(); + // Assume generated random boundary string is at least 50 chars + assertTrue(("multipart/form-data; boundary=".length() + 50) < contentType.length()); + } + + @Test + public void testBody() throws IOException { + HttpRequestMultipartBody httpRequestMultipartBody = new HttpRequestMultipartBody(); + String boundary = httpRequestMultipartBody.getContentType().substring(httpRequestMultipartBody.getContentType().indexOf("=") + 1); + + httpRequestMultipartBody.addTextPart("fieldName", "{\"json\":\"content\"}", "application/json"); + + File file = File.createTempFile("prefix", "tmp"); + file.deleteOnExit(); + FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write("fileContent".getBytes(StandardCharsets.UTF_8)); + fileOutputStream.flush(); + fileOutputStream.close(); + + httpRequestMultipartBody.addFilePart(file); + + String body = new String(httpRequestMultipartBody.getBytes()); + + String expected = "\r\n--" + boundary + + "\r\nContent-Disposition: form-data; name=\"fieldName\"" + + "\r\nContent-Type: application/json" + + "\r\n\r\n" + + "{\"json\":\"content\"}" + + "\r\n--" + boundary + + "\r\nContent-Disposition: form-data; name=\"files\"; filename=\"" + file.getName() + "\"" + + "\r\nContent-Type: application/octet-stream" + + "\r\n\r\n" + + "fileContent" + + "\r\n--" + boundary + "--"; + + assertEquals(expected, body); + } +}