commit cb4f28b1bf0704c251dcd1931942389036481f07 Author: Yan Date: Thu Apr 10 22:46:51 2025 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f7c743 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.classpath +/.project +target +conf +logs +.settings \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7eeacfa --- /dev/null +++ b/pom.xml @@ -0,0 +1,119 @@ + + 4.0.0 + com.example + guerrilla + 1.0.0 + guerrilla + Hit and Run + + UTF-8 + 17 + 17 + 3.4.4 + 3.14.0 + 1.19.1 + 1.6.2 + 33.1.0-jre + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot-dependencies.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + com.google.guava + guava + ${guava.version} + + + org.htmlunit + htmlunit + + + org.apache.commons + commons-lang3 + + + org.jsoup + jsoup + ${jsoup.version} + + + javax.mail + javax.mail-api + ${java.mail.version} + + + com.sun.mail + javax.mail + ${java.mail.version} + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot-dependencies.version} + + com.example.guerrilla.Boot + + + + build-info + + build-info + + + + + repackage + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/example/guerrilla/Boot.java b/src/main/java/com/example/guerrilla/Boot.java new file mode 100644 index 0000000..7383dd3 --- /dev/null +++ b/src/main/java/com/example/guerrilla/Boot.java @@ -0,0 +1,40 @@ +package com.example.guerrilla; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +import com.example.guerrilla.service.CaptureService; + +@SpringBootApplication +public class Boot { + + private static final Logger logger = LoggerFactory.getLogger(Boot.class); + + @Autowired + CaptureService captureService; + + public static void main(String[] args) throws IOException { + String configDirectory = "conf"; + if (args.length > 0) { + configDirectory = args[0]; + } + logger.info("config directory: {}", configDirectory); + + if (new File(configDirectory).exists() && new File(configDirectory).isDirectory()) { + System.setProperty("spring.config.location", configDirectory + "/springboot.yml"); + System.setProperty("logging.config", configDirectory + "/logback.xml"); + } + CaptureService captureService = new SpringApplicationBuilder(Boot.class).web(WebApplicationType.NONE).run(args) + .getBean(CaptureService.class); + captureService.run(); + System.exit(0); + } + +} diff --git a/src/main/java/com/example/guerrilla/config/AppConfig.java b/src/main/java/com/example/guerrilla/config/AppConfig.java new file mode 100644 index 0000000..bd42fd6 --- /dev/null +++ b/src/main/java/com/example/guerrilla/config/AppConfig.java @@ -0,0 +1,64 @@ +package com.example.guerrilla.config; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +@Configuration +public class AppConfig { + + @Value("${app.main.javamail-config}") + private String javamailCfg; + + @Bean + public Properties javaMailProperties() throws IOException { + Properties javaMailProperties = new Properties(); + FileInputStream fis = new FileInputStream(javamailCfg); + javaMailProperties.load(fis); + fis.close(); + return javaMailProperties; + } + + @Bean("objectMapper") + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.registerModule(new Jdk8Module()); + return mapper; + } + + @Component + @ConfigurationProperties(prefix = "app.htmlunit") + public class WebPageList { + private List vipPages = new ArrayList<>(); + + public List getVipPages() { + return vipPages; + } + + @ConfigurationProperties(prefix = "vip-pages") + public void setVipPages(List vipPages) { + this.vipPages = vipPages; + } + + public String toString() { + return ReflectionToStringBuilder.toString(this); + } + } + + public record WebPage(String name, String url) { + } +} diff --git a/src/main/java/com/example/guerrilla/service/CaptureService.java b/src/main/java/com/example/guerrilla/service/CaptureService.java new file mode 100644 index 0000000..61a09da --- /dev/null +++ b/src/main/java/com/example/guerrilla/service/CaptureService.java @@ -0,0 +1,193 @@ +package com.example.guerrilla.service; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlButton; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.html.HtmlPasswordInput; +import org.htmlunit.html.HtmlTextInput; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.example.guerrilla.config.AppConfig.WebPage; +import com.example.guerrilla.config.AppConfig.WebPageList; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.MapDifference; +import com.google.common.collect.MapDifference.ValueDifference; +import com.google.common.collect.Maps; + +@Service +public class CaptureService { + + private static final Logger logger = LoggerFactory.getLogger(CaptureService.class); + + @Value("${app.htmlunit.login-url}") + private String loginUrl; + + @Value("${app.htmlunit.login-btn-selector}") + private String loginBtnSelector; + + @Value("${app.htmlunit.login-wait-millis:3000}") + private Long loginWaitMillis; + + @Value("${app.htmlunit.logout-url}") + private String logoutUrl; + + @Value("${app.htmlunit.selector1}") + private String selector1; + + @Value("${app.htmlunit.selector2}") + private String selector2; + + @Value("${app.htmlunit.selector3}") + private String selector3; + + @Value("${app.htmlunit.username}") + private String username; + + @Value("${app.htmlunit.password}") + private String password; + + @Value("${app.htmlunit.user-page-url}") + private String userPageUrl; + + @Autowired + private WebPageList webPageList; + + @Value("${app.main.expected-result-file-path}") + private String expectedResultFilePath; + + @Value("${app.main.email-password:}") + private String smtpPassword; + + @Value("${app.main.email-sender:}") + private String fromAddress; + + @Value("${app.main.email-recipient:}") + private String[] toAddresses; + + @Value("${app.main.alert-subject}") + private String alertSubject; + + @Autowired + private EmailService emailService; + + @Autowired + private ObjectMapper objectMapper; + + public HashMap convertStringToMap(String jsonString) throws IOException { + TypeReference> ref = new TypeReference<>() { + }; + return new HashMap<>(objectMapper.readValue(jsonString, ref)); + } + + public void run() throws IOException { + logger.info("webPageList: {}", webPageList); + File jsonFile = new File(expectedResultFilePath); + if (!jsonFile.exists()) { + logger.info("file does not exist: {}", expectedResultFilePath); + return; + } + String jsonString = FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8); + HashMap expectedMap = convertStringToMap(jsonString); + logger.info("expected result: {}", expectedMap); + + Map actualMap = getData(); + logger.info("actual result: {}", actualMap); + MapDifference diff = Maps.difference(expectedMap, actualMap); + logger.info("diff.areEqual: {}", diff.areEqual()); + + if (!diff.areEqual()) { + StringBuilder contentBuilder = new StringBuilder(); + Map> entriesDiffering = diff.entriesDiffering(); + logger.info("entriesDiffering.size(): {}", entriesDiffering.size()); + entriesDiffering.entrySet().stream().forEach(entry -> { + logger.info("entriesDiffering key: {}", entry.getKey()); + logger.info("entriesDiffering expected: {}", entry.getValue().leftValue()); + logger.info("entriesDiffering actual: {}", entry.getValue().rightValue()); + contentBuilder.append(entry.getKey() + "
\n"); + contentBuilder.append("EXPECTED: " + entry.getValue().leftValue() + "
\n"); + contentBuilder.append("ACTUAL: " + entry.getValue().rightValue() + "
\n"); + contentBuilder.append("
\n"); + }); + Pair sendEmailResult = Pair.of(Boolean.TRUE, emailService.sendEmail(fromAddress, + smtpPassword, toAddresses, alertSubject, contentBuilder.toString())); + logger.info("sendEmailResult: {}", sendEmailResult); + } + } + + public Map getData() { + Map result = new HashMap<>(); + + try (final WebClient webClient = new WebClient()) { + + final HtmlPage loginPage = webClient.getPage(loginUrl); + logger.info("loginPage: {}", loginPage.asXml()); + + final HtmlForm form = loginPage.getForms().get(0); + final HtmlTextInput usernameField = form.getInputByName("username"); + usernameField.type(username); + logger.info("entered username: {}", username); + + final HtmlPasswordInput passwordField = form.getInputByName("password"); + passwordField.type(password); + logger.info("entered password: {}", password); + + HtmlButton loginBtn = loginPage.querySelector(loginBtnSelector); + logger.info("loginBtn type: {}", loginBtn.getType()); + loginBtn.click(); + + Thread.sleep(loginWaitMillis); + + final HtmlPage userPage = webClient.getPage(userPageUrl); + logger.info("userPage: {}", userPage.asXml()); + + webPageList.getVipPages().forEach(webPage -> { + try { + fillResponseMap(webClient, webPage, result); + } catch (Exception e) { + logger.error("getData error!", e); + } + }); + webClient.getPage(logoutUrl); + } catch (Exception e) { + logger.error("getData error!", e); + } + return result; + } + + private void fillResponseMap(WebClient webClient, WebPage webPage, Map result) throws Exception { + Map> reply = new HashMap<>(); + HtmlPage vipPage = webClient.getPage(webPage.url()); + logger.info("{}: {}", webPage.name(), vipPage.asXml()); + Document doc = Jsoup.parse(vipPage.asXml()); + for (String selector : List.of(selector1, selector2, selector3)) { + Elements elements = doc.select(selector); + List selectedList = new ArrayList<>(); + for (Element element : elements) { + logger.info("[fillResponseMap] {} -> {}", selector, element.html()); + selectedList.add(element.html()); + } + reply.put(selector, selectedList); + } + result.put(webPage.name(), reply); + } +} diff --git a/src/main/java/com/example/guerrilla/service/EmailService.java b/src/main/java/com/example/guerrilla/service/EmailService.java new file mode 100644 index 0000000..23aaf10 --- /dev/null +++ b/src/main/java/com/example/guerrilla/service/EmailService.java @@ -0,0 +1,70 @@ +package com.example.guerrilla.service; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class EmailService { + + private static final Logger logger = LoggerFactory.getLogger(EmailService.class); + + @Autowired + private Properties javaMailProperties; + + public boolean sendEmail(String fromAddress, String smtpPassword, String[] toAddresses, String subject, + String content) { + try { + logger.debug("[sendEmail] fromAddress: {}, toAddresses: {}", fromAddress, toAddresses); + if (StringUtils.isEmpty(subject) || StringUtils.isEmpty(smtpPassword) || StringUtils.isEmpty(fromAddress) + || toAddresses.length == 0) { + logger.warn("[sendEmail] subject, smtp-password, source-address or target-address is not set"); + return false; + } + String emailHost = javaMailProperties.getProperty("mail.smtp.host"); + logger.debug("[sendEmail] {} -> {}, mail.smtp.host: {}", fromAddress, List.of(toAddresses), emailHost); + if (StringUtils.isNotBlank(emailHost) && StringUtils.isNotBlank(fromAddress) && toAddresses.length > 0) { + logger.debug("[sendEmail] starting to send message from: {}", fromAddress); + Session session = Session.getInstance(javaMailProperties); + + MimeMessage message = new MimeMessage(session); + message.setContent(content, "text/html; charset=utf-8"); + message.setFrom(new InternetAddress(fromAddress)); + + Arrays.asList(toAddresses).forEach(toAddress -> { + try { + message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddress)); + } catch (Exception e) { + logger.error("sendEmail error!", e); + } + }); + message.setSubject(subject); + + Transport transport = session.getTransport("smtp"); + transport.connect(emailHost, fromAddress, smtpPassword); + transport.sendMessage(message, message.getAllRecipients()); + transport.close(); + logger.debug("sendEmail to: {}", Arrays.asList(toAddresses)); + + return true; + } else { + logger.warn("[sendEmail] missing either from-field, to-field or host address!"); + } + } catch (Exception e) { + logger.error("sendEmail error!", e); + } + return false; + } +}