package games.strategy.triplea.pbem; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.http.NameValuePair; import org.apache.http.client.CookieStore; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import games.strategy.engine.framework.system.HttpProxy; import games.strategy.engine.pbem.AbstractForumPoster; import games.strategy.engine.pbem.IForumPoster; import games.strategy.net.OpenFileUtility; import games.strategy.triplea.help.HelpSupport; import games.strategy.util.ThreadUtil; import games.strategy.util.Util; /** * Post turn summary to www.axisandallies.org to the thread identified by the forumId * URL format: http://www.axisandallies.org/forums/index.php?topic=[forumId], * like http://www.axisandallies.org/forums/index.php?topic=25878 * The poster logs in, and out every time it posts, this way we don't nee to manage any state between posts */ public class AxisAndAlliesForumPoster extends AbstractForumPoster { private static final long serialVersionUID = 8896923978584346664L; // the patterns used to extract values from hidden form fields posted to the server public static final Pattern NUM_REPLIES_PATTERN = Pattern.compile(".*name=\"num_replies\" value=\"(\\d+)\".*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); public static final Pattern SEQ_NUM_PATTERN = Pattern.compile(".*name=\"seqnum\"\\svalue=\"(\\d+)\".*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); public static final Pattern SC_PATTERN = Pattern.compile(".*name=\"sc\"\\svalue=\"(\\w+)\".*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); // Pattern that matches if the "Notify me of replies" checkbox is checked public static final Pattern NOTIFY_PATTERN = Pattern.compile(".*id=\"check_notify\"\\schecked=\"checked\".*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); // 3 patterns used for error handling public static final Pattern AN_ERROR_OCCURRED_PATTERN = Pattern.compile(".*An Error Has Occurred.*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); public static final Pattern ERROR_TEXT_PATTERN = Pattern .compile(".*<tr\\s+class=\"windowbg\">\\s*<td[^>]*>([^<]*)</td>.*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); public static final Pattern ERROR_LIST_PATTERN = Pattern.compile(".*id=\"error_list[^>]*>\\s+([^<]*)\\s+<.*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); /** * Logs into axisandallies.org * nb: Username and password are posted in clear text * * @throws Exception * if login fails */ private HttpContext login(CloseableHttpClient client) throws Exception { HttpPost httpPost = new HttpPost("http://www.axisandallies.org/forums/index.php?action=login2"); CookieStore cookieStore = new BasicCookieStore(); HttpContext httpContext = new BasicHttpContext(); httpContext.setAttribute(HttpClientContext.COOKIE_STORE, cookieStore); HttpProxy.addProxy(httpPost); httpPost.addHeader("Accept", "*/*"); httpPost.addHeader("Accept-Language", "en-us"); httpPost.addHeader("Cache-Control", "no-cache"); httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); final List<NameValuePair> parameters = new ArrayList<>(2); parameters.add(new BasicNameValuePair("user", getUsername())); parameters.add(new BasicNameValuePair("passwrd", getPassword())); httpPost.setEntity(new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8)); try (CloseableHttpResponse response = client.execute(httpPost, httpContext)) { int status = response.getStatusLine().getStatusCode(); if (status == HttpURLConnection.HTTP_OK) { final String body = Util.getStringFromInputStream(response.getEntity().getContent()); if (body.toLowerCase().contains("password incorrect")) { throw new Exception("Incorrect Password"); } // site responds with 200, and a refresh header final Header refreshHeader = response.getFirstHeader("Refresh"); if (refreshHeader == null) { throw new Exception("Missing refresh header after login"); } // refresh: 0; URL=http://... final String value = refreshHeader.getValue(); final Pattern p = Pattern.compile("[^;]*;\\s*url=(.*)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); final Matcher m = p.matcher(value); if (m.matches()) { final String url = m.group(1); HttpGet httpGet = new HttpGet(url); HttpProxy.addProxy(httpGet); try (CloseableHttpResponse response2 = client.execute(httpGet, httpContext)) { status = response2.getStatusLine().getStatusCode(); if (status != 200) { // something is probably wrong, but there is not much we can do about it, we handle errors when we post } } } else { throw new Exception("The refresh header didn't contain a URL"); } } else { throw new Exception("Failed to login to forum, server responded with status code: " + status); } } return httpContext; } @Override public boolean postTurnSummary(final String message, final String subject) { try (CloseableHttpClient client = HttpClients.createDefault()) { HttpContext httpContext = login(client); // Now we load the post page, and find the hidden fields needed to post HttpGet httpGet = new HttpGet("http://www.axisandallies.org/forums/index.php?action=post;topic=" + m_topicId + ".0"); HttpProxy.addProxy(httpGet); try (CloseableHttpResponse response = client.execute(httpGet, httpContext)) { int status = response.getStatusLine().getStatusCode(); String body = Util.getStringFromInputStream(response.getEntity().getContent()); if (status == 200) { String numReplies; String seqNum; String sc; Matcher m = NUM_REPLIES_PATTERN.matcher(body); if (m.matches()) { numReplies = m.group(1); } else { throw new Exception("Hidden field 'num_replies' not found on page"); } m = SEQ_NUM_PATTERN.matcher(body); if (m.matches()) { seqNum = m.group(1); } else { throw new Exception("Hidden field 'seqnum' not found on page"); } m = SC_PATTERN.matcher(body); if (m.matches()) { sc = m.group(1); } else { throw new Exception("Hidden field 'sc' not found on page"); } // now we have the required hidden fields to reply to HttpPost httpPost = new HttpPost("http://www.axisandallies.org/forums/index.php?action=post2;start=0;board=40"); // Construct the multi part post MultipartEntityBuilder builder = MultipartEntityBuilder.create() .addTextBody("topic", m_topicId) .addTextBody("subject", subject) .addTextBody("icon", "xx") .addTextBody("message", message) // If the user has chosen to receive notifications, ensure this setting is passed on .addTextBody("notify", NOTIFY_PATTERN.matcher(body).matches() ? "1" : "0"); if (m_includeSaveGame && m_saveGameFile != null) { builder.addBinaryBody("attachment[]", m_saveGameFile, ContentType.APPLICATION_OCTET_STREAM, m_saveGameFileName); } builder .addTextBody("post", "Post") .addTextBody("num_replies", numReplies) .addTextBody("additional_options", "1") .addTextBody("sc", sc) .addTextBody("seqnum", seqNum); httpPost.setEntity(builder.build()); // add headers httpPost.addHeader("Referer", "http://www.axisandallies.org/forums/index.php?action=post;topic=" + m_topicId + ".0;num_replies=" + numReplies); httpPost.addHeader("Accept", "*/*"); // the site has spam prevention which means you can't post until 15 seconds after login if (!ThreadUtil.sleep(15 * 1000)) { return false; } httpPost.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); HttpProxy.addProxy(httpPost); try (CloseableHttpResponse response2 = client.execute(httpPost, httpContext)) { status = response2.getStatusLine().getStatusCode(); body = Util.getStringFromInputStream(response2.getEntity().getContent()); if (status == HttpURLConnection.HTTP_MOVED_TEMP) { // site responds with a 302 redirect back to the forum index (board=40) // The syntax for post is ".....topic=xx.yy" where xx is the thread id, and yy is the post number in the // given thread // since the site is lenient we can just give a high post_number to go to the last post in the thread m_turnSummaryRef = "http://www.axisandallies.org/forums/index.php?topic=" + m_topicId + ".10000"; } else { // these two patterns find general errors, where the first pattern checks if the error text appears, // the second pattern extracts the error message. This could be the "The last posting from your IP was // less // than 15 seconds // ago.Please try again later" // this patter finds errors that are marked in red (for instance "You are not allowed to post URLs", or // "Some one else has posted while you vere reading" Matcher matcher = ERROR_LIST_PATTERN.matcher(body); if (matcher.matches()) { throw new Exception("The site gave an error: '" + matcher.group(1) + "'"); } matcher = AN_ERROR_OCCURRED_PATTERN.matcher(body); if (matcher.matches()) { matcher = ERROR_TEXT_PATTERN.matcher(body); if (matcher.matches()) { throw new Exception("The site gave an error: '" + matcher.group(1) + "'"); } } final Header refreshHeader = response2.getFirstHeader("Refresh"); if (refreshHeader != null) { // sometimes the message will be flagged as spam, and a refresh url is given // refresh: 0; URL=http://...topic=26114.new%3bspam=true#new final String value = refreshHeader.getValue(); final Pattern p = Pattern.compile("[^;]*;\\s*url=.*spam=true.*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); m = p.matcher(value); if (m.matches()) { throw new Exception("The summary was posted but was flagged as spam"); } } throw new Exception( "Unknown error, please contact the forum owner and also post a bug to the tripleA development team"); } } } else { throw new Exception("Unable to load forum post " + m_topicId); } } } catch (final Exception e) { m_turnSummaryRef = e.getMessage(); return false; } return true; } @Override public String getDisplayName() { return "AxisAndAllies.org"; } /** * Create a clone of the poster. * * @return a copy */ @Override public IForumPoster doClone() { final AxisAndAlliesForumPoster clone = new AxisAndAlliesForumPoster(); clone.setTopicId(getTopicId()); clone.setIncludeSaveGame(getIncludeSaveGame()); clone.setAlsoPostAfterCombatMove(getAlsoPostAfterCombatMove()); clone.setPassword(getPassword()); clone.setUsername(getUsername()); return clone; } @Override public boolean supportsSaveGame() { return true; } @Override public void viewPosted() { final String url = "http://www.axisandallies.org/forums/index.php?topic=" + m_topicId + ".10000"; OpenFileUtility.openURL(url); } @Override public String getTestMessage() { return "Testing, this will take about 20 seconds..."; } @Override public String getHelpText() { return HelpSupport.loadHelp("axisAndAlliesForum.html"); } }