package com.tyndalehouse.step.core.service.impl; import com.google.inject.Inject; import com.google.inject.Singleton; import com.tyndalehouse.step.core.exceptions.StepInternalException; import com.tyndalehouse.step.core.models.ClientSession; import com.tyndalehouse.step.core.service.AppManagerService; import com.tyndalehouse.step.core.service.SupportRequestService; import com.tyndalehouse.step.core.utils.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpRequestInterceptor; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScheme; import org.apache.http.auth.AuthScope; import org.apache.http.auth.AuthState; import org.apache.http.auth.Credentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.ClientContext; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.ByteArrayBody; import org.apache.http.entity.mime.content.InputStreamBody; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicHeader; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import org.crosswire.common.util.IOUtil; import javax.inject.Named; import javax.inject.Provider; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import static com.tyndalehouse.step.core.utils.StringUtils.getNonNullString; /** * Accesses JIRA to raise a support request. * * @author chrisburrell */ @Singleton public class SupportRequestServiceImpl implements SupportRequestService { public static final int ERROR_START = 400; public static final String JIRA_USER = "jira.user"; public static final String JIRA_PASSWORD = "jira.password"; private static final String ISSUE_API = "/issue/"; private static final String ATTACH_API = ISSUE_API + "%s/attachments"; private final String createTemplate; private String jiraEndpoint; private final javax.inject.Provider<ClientSession> clientSessionProvider; private final AppManagerService appManager; @Inject public SupportRequestServiceImpl(@Named("app.jira.create.issue") final String createTemplate, @Named("app.jira.create.endpoint") final String jiraEndpoint, final Provider<ClientSession> clientSessionProvider, final AppManagerService appManager) { this.createTemplate = createTemplate; this.jiraEndpoint = jiraEndpoint; this.clientSessionProvider = clientSessionProvider; this.appManager = appManager; } @Override public void createRequest(final String summary, final String description, final String url, final String issueType, final String email) { final String id = createJiraRequest(summary, description, url, issueType, email); attachImage(id); } /** * Attaches some data to the issue * * @param id the FST-number */ private void attachImage(final String id) { InputStream imageData = null; HttpPost attachmentRequest = null; MultipartEntity entity = null; HttpResponse response = null; try { imageData = clientSessionProvider.get().getAttachment("screenshot-part"); byte[] imageAsBytes = readImage(imageData); if (imageAsBytes == null || imageAsBytes.length == 0) { return; } attachmentRequest = getJiraHttpPost(String.format(ATTACH_API, id), null); entity = new MultipartEntity(); entity.addPart("file", new ByteArrayBody(imageAsBytes, "screenshot.png")); attachmentRequest.setEntity(entity); DefaultHttpClient httpClient = getDefaultHttpClient(attachmentRequest); response = httpClient.execute(attachmentRequest, getHttpContext(httpClient)); if (response.getStatusLine().getStatusCode() >= ERROR_START) { handleHttpResponseFailure(response, null); } } catch (IOException e) { handleHttpResponseFailure(response, null); } finally { IOUtils.closeQuietly(imageData); EntityUtils.consumeQuietly(entity); if (attachmentRequest != null) { attachmentRequest.releaseConnection(); } } } private byte[] readImage(final InputStream imageData) { ByteArrayOutputStream outputStream = null; BufferedOutputStream bos = null; BufferedInputStream bis = null; try { outputStream = new ByteArrayOutputStream(); bos = new BufferedOutputStream(outputStream); bis = new BufferedInputStream(imageData); int b; while ((b = bis.read()) != -1) { bos.write(b); } bos.flush(); return outputStream.toByteArray(); } catch (IOException ex) { throw new StepInternalException("Unable to read image data"); } finally { IOUtils.closeQuietly(bos); IOUtils.closeQuietly(bis); IOUtils.closeQuietly(outputStream); IOUtils.closeQuietly(imageData); } } /** * Creates an issue on JIRA * * @param summary the summary of the ticket * @param description the description of the ticket * @param issueType the user attached to the issue * @param email the email * @return the id of the issue that was created */ private String createJiraRequest(final String summary, final String description, final String url, final String issueType, final String email) { final String escapedEmail = escapeQuotes(getNonNullString(email, "")); final String escapedSummary = escapeQuotes(getNonNullString(summary, "")); final String escapedDescription = escapeQuotes(getNonNullString(description, "")); final String escapedUrl = escapeQuotes(getNonNullString(url, "")); final String escapedType = escapeQuotes(getNonNullString(issueType, "")); ByteArrayInputStream createRequest = null; BasicHttpEntity entity = null; HttpPost post = null; HttpResponse response = null; try { post = getJiraHttpPost(ISSUE_API, "application/json"); entity = new BasicHttpEntity(); //app.jira.create.issue={ "fields": { "project": { "key": "FST" }, "summary": "%s", "description": "%s", "customfield_10923":"%s", "customfield_10922":"%s", "customfield_10921": "%s", "issuetype": { "name": "%s" }}} final byte[] body = String.format(createTemplate, escapedType + " - " + escapedSummary, escapedDescription, this.appManager.getAppVersion(), escapedUrl, escapedEmail, escapedType).getBytes(); createRequest = new ByteArrayInputStream(body); entity.setContent(createRequest); entity.setContentLength(body.length); post.setEntity(entity); final DefaultHttpClient defaultHttpClient = getDefaultHttpClient(post); response = defaultHttpClient.execute(post, getHttpContext(defaultHttpClient)); if (response.getStatusLine().getStatusCode() >= ERROR_START) { return handleHttpResponseFailure(response, null); } return extractIssueKey(readResponse(response.getEntity())); } catch (IOException ex) { return handleHttpResponseFailure(response, ex); } finally { IOUtils.closeQuietly(createRequest); EntityUtils.consumeQuietly(entity); if (post != null) { post.releaseConnection(); } } } /** * Set pre-emptive authentication on * * @param httpClient the http client * @return the context */ private HttpContext getHttpContext(final DefaultHttpClient httpClient) { BasicHttpContext localContext = new BasicHttpContext(); BasicScheme basicAuth = new BasicScheme(); localContext.setAttribute("preemptive-auth", basicAuth); httpClient.addRequestInterceptor(new PreemptiveAuthInterceptor(), 0); return localContext; } private DefaultHttpClient getDefaultHttpClient(HttpPost post) { final DefaultHttpClient defaultHttpClient = new DefaultHttpClient(); final Credentials credentials = new UsernamePasswordCredentials(System.getProperty(JIRA_USER), System.getProperty(JIRA_PASSWORD)); defaultHttpClient.getCredentialsProvider().setCredentials(AuthScope.ANY, credentials); return defaultHttpClient; } /** * Handles the http response by reading the entity if not null * * @param response the HTTP response * @param ex the exception that caused the issue (or null) * @return no string - always returns null */ private String handleHttpResponseFailure(final HttpResponse response, final IOException ex) { String explanation = response != null ? readResponse(response.getEntity()) : "<no response>"; throw new StepInternalException("Unable to create issue with JIRA: " + explanation, ex); } /** * Escapes all double quotes * * @param nonNullString the string - must be non null * @return the escaped string */ private String escapeQuotes(final String nonNullString) { return nonNullString.replaceAll("\"", "\\\\\"").replaceAll("\n", "\\\\n").replaceAll("\t", "\\\\t").replaceAll("\r", "\\\\r"); } private HttpPost getJiraHttpPost(final String operation, final String contentType) { final HttpPost post = new HttpPost(this.jiraEndpoint + operation); if(contentType != null) { post.addHeader(new BasicHeader("Content-Type", contentType)); } post.addHeader(new BasicHeader("X-Atlassian-Token", "no-check")); return post; } private String extractIssueKey(final String response) { int startKey = response.indexOf("FST-"); int quoteMarker = response.indexOf('"', startKey); return response.substring(startKey, quoteMarker); } private String readResponse(final HttpEntity entity) { if (entity == null) { return ""; } InputStream content = null; BufferedReader reader = null; InputStreamReader inputStreamReader = null; try { content = entity.getContent(); if (content == null) { return ""; } inputStreamReader = new InputStreamReader(content); reader = new BufferedReader(inputStreamReader); final StringBuilder response = new StringBuilder(256); String line; while ((line = reader.readLine()) != null) { response.append(line); } return response.toString().replaceAll("[\\n\\r]", ""); } catch (IOException e) { throw new StepInternalException("Unable to parse response", e); } finally { EntityUtils.consumeQuietly(entity); IOUtils.closeQuietly(reader); IOUtils.closeQuietly(inputStreamReader); IOUtils.closeQuietly(content); } } static class PreemptiveAuthInterceptor implements HttpRequestInterceptor { public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException { AuthState authState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE); // If no auth scheme avaialble yet, try to initialize it // preemptively if (authState.getAuthScheme() == null) { AuthScheme authScheme = (AuthScheme) context.getAttribute("preemptive-auth"); CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER); HttpHost targetHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); if (authScheme != null) { Credentials creds = credsProvider.getCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort())); if (creds == null) { throw new HttpException("No credentials for preemptive authentication"); } authState.update(authScheme, creds); } } } } }