Add TokenRefresher

This commit is contained in:
Robert Carosi
2021-11-05 13:20:19 +01:00
parent dd984ff944
commit 69569d3543
7 changed files with 104 additions and 14 deletions

View File

@@ -77,6 +77,7 @@ Synchronizes articles from Pocket to the Remarkable tablet.
## Limitations
- Articles behind a paywall cannot be downloaded.
- Articles on websites with sophisticated DDOS protection cannot be downloaded.
- Articles that use javascript to load the the content cannot be downloaded.
## Thanks

View File

@@ -28,6 +28,7 @@ final class DownloadService {
@PostConstruct
void createStorageDir() throws IOException {
storageDir = Files.createTempDirectory(null);
LOG.debug("Created temporary storage directory: {}.", storageDir);
}
@SuppressWarnings("UnstableApiUsage")

View File

@@ -11,6 +11,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.PostConstruct;
import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
@@ -18,14 +19,19 @@ import net.lingala.zip4j.io.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import nl.carosi.remarkablepocket.model.DocumentMetadata;
import nl.siegmann.epublib.epub.EpubReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class MetadataProvider {
private final Jrmapi rmapi;
private static final Logger LOG = LoggerFactory.getLogger(MetadataProvider.class);
private final AtomicReference<Jrmapi> rmapi;
private final EpubReader epubReader;
private final ObjectMapper objectMapper;
private Path workDir;
public MetadataProvider(Jrmapi rmapi, EpubReader epubReader, ObjectMapper objectMapper) {
public MetadataProvider(
AtomicReference<Jrmapi> rmapi, EpubReader epubReader, ObjectMapper objectMapper) {
this.rmapi = rmapi;
this.epubReader = epubReader;
this.objectMapper = objectMapper;
@@ -34,13 +40,15 @@ final class MetadataProvider {
@PostConstruct
void createWorkDir() throws IOException {
workDir = Files.createTempDirectory(null);
LOG.debug("Created temporary working directory: {}.", workDir);
}
DocumentMetadata getMetadata(Document doc) {
LOG.debug("Getting metadata for document: {}.", doc.getVissibleName());
ZipFile zip;
String fileHash;
try {
zip = new ZipFile(rmapi.fetchZip(doc, workDir.toString() + File.separator));
zip = new ZipFile(rmapi.get().fetchZip(doc, workDir.toString() + File.separator));
fileHash = getFileHash(zip);
} catch (ZipException e) {
throw new RuntimeException(e);

View File

@@ -6,6 +6,7 @@ import es.jlarriba.jrmapi.Jrmapi;
import es.jlarriba.jrmapi.model.Document;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import javax.annotation.PostConstruct;
import nl.carosi.remarkablepocket.model.DocumentMetadata;
@@ -15,13 +16,13 @@ import org.springframework.beans.factory.annotation.Value;
final class RemarkableService {
private static final Logger LOG = LoggerFactory.getLogger(RemarkableService.class);
private final Jrmapi rmapi;
private final AtomicReference<Jrmapi> rmapi;
private final MetadataProvider metadataProvider;
private final String rmStorageDir;
private String rmStorageDirId = "";
public RemarkableService(
Jrmapi rmapi,
AtomicReference<Jrmapi> rmapi,
MetadataProvider metadataProvider,
@Value("${rm.storage-dir}") String rmStorageDir) {
if (!rmStorageDir.endsWith("/")) {
@@ -40,12 +41,12 @@ final class RemarkableService {
String[] dirs = rmStorageDir.substring(1, rmStorageDir.length() - 1).split("/");
for (final String dir : dirs) {
rmStorageDirId =
rmapi.listDocs().stream()
rmapi.get().listDocs().stream()
.filter(e -> e.getParent().equals(rmStorageDirId))
.filter(e -> e.getVissibleName().equals(dir))
.findFirst()
.map(Document::getID)
.orElseGet(() -> rmapi.createDir(dir, rmStorageDirId));
.orElseGet(() -> rmapi.get().createDir(dir, rmStorageDirId));
}
}
@@ -73,22 +74,22 @@ final class RemarkableService {
}
void delete(Document doc) {
rmapi.deleteEntry(doc);
rmapi.get().deleteEntry(doc);
}
private void logPages(DocumentMetadata meta) {
LOG.debug(
"{}: Current page: {}, page count: {}",
"{}: Current page: {}, page count: {}.",
meta.doc().getVissibleName(),
meta.doc().getCurrentPage() + 1,
meta.pageCount());
}
private Stream<Document> docsStream() {
return rmapi.listDocs().stream().filter(e -> e.getParent().equals(rmStorageDirId));
return rmapi.get().listDocs().stream().filter(e -> e.getParent().equals(rmStorageDirId));
}
private void upload(Path path) {
rmapi.uploadDoc(path.toFile(), rmStorageDirId);
rmapi.get().uploadDoc(path.toFile(), rmStorageDirId);
}
}

View File

@@ -1,6 +1,7 @@
package nl.carosi.remarkablepocket;
import es.jlarriba.jrmapi.Jrmapi;
import java.util.concurrent.atomic.AtomicReference;
import nl.siegmann.epublib.epub.EpubReader;
import nl.siegmann.epublib.epub.EpubWriter;
import org.springframework.beans.factory.annotation.Value;
@@ -22,10 +23,11 @@ import org.springframework.scheduling.annotation.EnableScheduling;
PocketService.class,
RemarkableService.class,
SyncService.class,
TokenRefresher.class
})
public class SyncApplication {
@Bean
Jrmapi jrmapi(@Value("${rm.device-token}") String deviceToken) {
return new Jrmapi(deviceToken);
AtomicReference<Jrmapi> jrmapi(@Value("${rm.device-token}") String deviceToken) {
return new AtomicReference<>(new Jrmapi(deviceToken));
}
}

View File

@@ -0,0 +1,73 @@
package nl.carosi.remarkablepocket;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import es.jlarriba.jrmapi.Jrmapi;
import java.lang.reflect.Field;
import java.time.Instant;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.TaskScheduler;
// Hacky, but unfortunately jrmapi does not refresh bearer tokens.
final class TokenRefresher {
private static final Logger LOG = LoggerFactory.getLogger(TokenRefresher.class);
private final ObjectMapper objectMapper;
private final TaskScheduler scheduler;
private final AtomicReference<Jrmapi> rmapi;
private final String deviceToken;
private final Base64.Decoder decoder = Base64.getDecoder();
public TokenRefresher(
ObjectMapper objectMapper,
TaskScheduler scheduler,
AtomicReference<Jrmapi> rmapi,
@Value("${rm.device-token}") String deviceToken) {
this.objectMapper = objectMapper;
this.scheduler = scheduler;
this.rmapi = rmapi;
this.deviceToken = deviceToken;
}
@PostConstruct
void scheduleRefresh() {
try {
scheduleRefreshImpl();
} catch (NoSuchFieldException | IllegalAccessException | JsonProcessingException e) {
throw new RuntimeException("Could not refresh jrmapi token.");
}
}
private void scheduleRefreshImpl()
throws NoSuchFieldException, IllegalAccessException, JsonProcessingException {
Field tokenField = Jrmapi.class.getDeclaredField("userToken");
tokenField.setAccessible(true);
String token = (String) tokenField.get(rmapi.get());
int exp = getExpiration(token);
Instant nextRefresh = Instant.ofEpochSecond(exp - 60 * 10); // 10 min margin.
scheduler.schedule(this::refresh, nextRefresh);
LOG.debug("Next token refresh scheduled at: {}.", nextRefresh);
}
private void refresh() {
rmapi.set(new Jrmapi(deviceToken));
LOG.debug("Refreshed jrmapi token.");
scheduleRefresh();
}
private int getExpiration(String jwt) throws JsonProcessingException {
String[] chunks = jwt.split("\\.");
String payload = new String(decoder.decode(chunks[1]));
JWT exp = objectMapper.readValue(payload, JWT.class);
return exp.exp();
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record JWT(int exp) {}
}

View File

@@ -7,6 +7,10 @@ public final record Article(String id, String url, String title) {
}
private static String sanitize(String title) {
return title.replaceAll("/", " ").replaceAll(" +", " ").strip();
return title
.replaceAll("[\"]", "'")
.replaceAll(":", " -")
.replaceAll("[/?<>*.|\\\\]", " ")
.replaceAll(" +", " ").strip();
}
}