From eba16d518ef43a21c8ba1935f1adc5099c82cc18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 15:06:06 +0100 Subject: [PATCH 1/9] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f21c15e..af57128 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ unnecessary features, or they didn't have all the features I wanted. # Screenshot ![Screenshot](./screenshot.png) -# Planned features -- An actual name +# Planned features for 1.0 (in order of importance - Some form of authentication - Input validation (on client and server) - Deleting links using API and frontend -- Code cleanup - Better deduplication +- Code cleanup +- An actual name - Official Docker Hub image # Usage From 59b6d43aea61863a18d87e4e6572917e2da0ec7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 15:46:29 +0100 Subject: [PATCH 2/9] Added Basic auth and GZIP compression --- build.gradle | 1 + src/main/java/tk/draganczuk/url/App.java | 18 ++++++++++++++++-- src/main/java/tk/draganczuk/url/Filters.java | 20 ++++++++++++++++++++ src/main/resources/public/js/main.js | 4 ++-- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 src/main/java/tk/draganczuk/url/Filters.java diff --git a/build.gradle b/build.gradle index 609d499..58e0dae 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ jar { dependencies { compile "com.sparkjava:spark-core:2.8.0" + implementation 'com.qmetric:spark-authentication:1.4' } application { diff --git a/src/main/java/tk/draganczuk/url/App.java b/src/main/java/tk/draganczuk/url/App.java index 24bff3e..93a220a 100644 --- a/src/main/java/tk/draganczuk/url/App.java +++ b/src/main/java/tk/draganczuk/url/App.java @@ -1,5 +1,7 @@ package tk.draganczuk.url; +import spark.Filter; + import static spark.Spark.*; public class App { @@ -17,13 +19,25 @@ public class App { port(Integer.parseInt(System.getProperty("port", "4567"))); + // Add GZIP compression + after(Filters::addGZIP); + + // Authenticate + Filter authFilter = Filters.createAuthFilter(); + before("/index.html", authFilter); + get("/", (req, res) -> { res.redirect("/index.html"); return "Redirect"; }); - get("/all", Routes::getAll); - post("/new", Routes::addUrl); + + path("/api", () -> { + before("/*", authFilter); + get("/all", Routes::getAll); + post("/new", Routes::addUrl); + }); + get("/:shortUrl", Routes::goToLongUrl); } } diff --git a/src/main/java/tk/draganczuk/url/Filters.java b/src/main/java/tk/draganczuk/url/Filters.java new file mode 100644 index 0000000..83ad996 --- /dev/null +++ b/src/main/java/tk/draganczuk/url/Filters.java @@ -0,0 +1,20 @@ +package tk.draganczuk.url; + +import com.qmetric.spark.authentication.AuthenticationDetails; +import com.qmetric.spark.authentication.BasicAuthenticationFilter; +import spark.Filter; +import spark.Request; +import spark.Response; + +public class Filters { + public static void addGZIP(Request request, Response response) { + response.header("Content-Encoding", "gzip"); + } + + public static Filter createAuthFilter() { + String username = System.getenv("username"); + String password = System.getenv("password"); + + return new BasicAuthenticationFilter(new AuthenticationDetails(username, password)); + } +} diff --git a/src/main/resources/public/js/main.js b/src/main/resources/public/js/main.js index 0a9acbf..850ff4e 100644 --- a/src/main/resources/public/js/main.js +++ b/src/main/resources/public/js/main.js @@ -1,5 +1,5 @@ const refreshData = async () => { - let data = await fetch("/all").then(res => res.text()); + let data = await fetch("/api/all").then(res => res.text()); data = data .split("\n") .filter(line => line !== "") @@ -44,7 +44,7 @@ const submitForm = () => { const longUrl = form.elements["longUrl"]; const shortUrl = form.elements["shortUrl"]; - const url = `/new?long=${longUrl.value}&short=${shortUrl.value}`; + const url = `/api/new?long=${longUrl.value}&short=${shortUrl.value}`; fetch(url, { method: "POST" From bde40b61e6aaa7eea99d92b3c7f37b84dc13577f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 15:51:00 +0100 Subject: [PATCH 3/9] Added documentation about authentication --- README.md | 15 ++++++++++++--- docker-compose.yml | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index af57128..f484896 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ git clone https://github.com/draganczukp/url ``` ## Building from source Gradle 6.x.x and JDK 11 are required. Other versions are not tested -1. Build the `.jar` file +### 1. Build the `.jar` file ``` gradle build --no-daemon ``` @@ -46,11 +46,18 @@ The `--no-daemon` option means that gradle should exit as soon as the build is finished. Without it, gradle would still be running in the background in order to speed up future builds. -2. Run it +### 2. Set environment variables +```bash +export username= +export password= +export file.location= # opitonal +``` + +### 3. Run it ``` java -jar build/libs/url.jar ``` -3. Navigate to `http://localhost:4567` in your browser, add links as you wish. +### 4. Navigate to `http://localhost:4567` in your browser, add links as you wish. ## Running with docker ### `docker run` method @@ -67,6 +74,8 @@ docker run -p 4567:4567 -d url:1.0 touch ./urls.csv docker run -p 4567:4567 \ -e file.location=/urls.csv \ + -e username="username" + -e password="password" -v ./urls.csv:/urls.csv \ -d url:1.0 ``` diff --git a/docker-compose.yml b/docker-compose.yml index 764631b..b375dde 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,5 +8,7 @@ services: - 4567:4567 environment: - file.location=/urls.csv + - username=admin + - password=admin volumes: - ./urls.csv:/urls.csv \ No newline at end of file From 8cd399d2e9ce0295de6cbc1a98490be93b4dff0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 15:58:37 +0100 Subject: [PATCH 4/9] Now only lowercase letters and numbers are generated --- src/main/java/tk/draganczuk/url/Utils.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/tk/draganczuk/url/Utils.java b/src/main/java/tk/draganczuk/url/Utils.java index 0c868c4..efa312a 100644 --- a/src/main/java/tk/draganczuk/url/Utils.java +++ b/src/main/java/tk/draganczuk/url/Utils.java @@ -10,14 +10,12 @@ public class Utils { int rightLimit = 122; // letter 'z' int targetStringLength = 10; - String generatedString = random.ints(leftLimit, rightLimit + 1) - .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + return random.ints(leftLimit, rightLimit + 1) + .filter(i -> (i <= 57 || i >= 97)) .limit(targetStringLength) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) .toString(); - - return generatedString; } } From 1322569cf6b7ab036659700bc2e7f7dcb8e79c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 16:05:09 +0100 Subject: [PATCH 5/9] Added a basic input validation for shortUrl --- src/main/java/tk/draganczuk/url/Routes.java | 15 +++++++++++---- src/main/java/tk/draganczuk/url/Utils.java | 9 +++++++++ src/main/resources/public/index.html | 5 +++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/tk/draganczuk/url/Routes.java b/src/main/java/tk/draganczuk/url/Routes.java index dad75b8..2ad5fd1 100644 --- a/src/main/java/tk/draganczuk/url/Routes.java +++ b/src/main/java/tk/draganczuk/url/Routes.java @@ -1,5 +1,6 @@ package tk.draganczuk.url; +import org.eclipse.jetty.http.HttpStatus; import spark.Request; import spark.Response; @@ -29,15 +30,21 @@ public class Routes { shortUrl = Utils.randomString(); } - return urlFile.addUrl(longUrl, shortUrl); + if (Utils.validate(shortUrl)) { + return urlFile.addUrl(longUrl, shortUrl); + } else { + res.status(HttpStatus.BAD_REQUEST_400); + return "shortUrl not valid ([a-z0-9]+)"; + } } - public static String goToLongUrl(Request req, Response res){ + + public static String goToLongUrl(Request req, Response res) { String shortUrl = req.params("shortUrl"); var longUrlOpt = urlFile - .findForShortUrl(shortUrl); + .findForShortUrl(shortUrl); - if(longUrlOpt.isEmpty()){ + if (longUrlOpt.isEmpty()) { res.status(404); return ""; } diff --git a/src/main/java/tk/draganczuk/url/Utils.java b/src/main/java/tk/draganczuk/url/Utils.java index efa312a..9292067 100644 --- a/src/main/java/tk/draganczuk/url/Utils.java +++ b/src/main/java/tk/draganczuk/url/Utils.java @@ -1,10 +1,14 @@ package tk.draganczuk.url; import java.util.Random; +import java.util.regex.Pattern; public class Utils { private static final Random random = new Random(System.currentTimeMillis()); + private static final String SHORT_URL_PATTERN = "[a-z0-9]+"; + private static final Pattern PATTERN = Pattern.compile(SHORT_URL_PATTERN); + public static String randomString() { int leftLimit = 48; // numeral '0' int rightLimit = 122; // letter 'z' @@ -18,4 +22,9 @@ public class Utils { StringBuilder::append) .toString(); } + + public static boolean validate(String shortUrl) { + return PATTERN.matcher(shortUrl) + .matches(); + } } diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html index 988384d..e53d4fe 100644 --- a/src/main/resources/public/index.html +++ b/src/main/resources/public/index.html @@ -30,11 +30,12 @@ Add new URL
- +
- +
From 89eb5526ce43a3e608b9f38fe35b297ee3780d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 16:08:06 +0100 Subject: [PATCH 6/9] Added dashes and underscores to permitted characters --- src/main/java/tk/draganczuk/url/Utils.java | 2 +- src/main/resources/public/index.html | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/tk/draganczuk/url/Utils.java b/src/main/java/tk/draganczuk/url/Utils.java index 9292067..174a7b5 100644 --- a/src/main/java/tk/draganczuk/url/Utils.java +++ b/src/main/java/tk/draganczuk/url/Utils.java @@ -6,7 +6,7 @@ import java.util.regex.Pattern; public class Utils { private static final Random random = new Random(System.currentTimeMillis()); - private static final String SHORT_URL_PATTERN = "[a-z0-9]+"; + private static final String SHORT_URL_PATTERN = "[a-z0-9_-]+"; private static final Pattern PATTERN = Pattern.compile(SHORT_URL_PATTERN); public static String randomString() { diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html index e53d4fe..897ebb3 100644 --- a/src/main/resources/public/index.html +++ b/src/main/resources/public/index.html @@ -33,9 +33,10 @@
- + + pattern="[a-z0-9_-]+"/>
From dc171b9973d36863909433fe7b61f7e182b33f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 16:10:04 +0100 Subject: [PATCH 7/9] Slightly modified default docker files --- README.md | 4 ++-- docker-compose.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f484896..9f0752b 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,11 @@ java -jar build/libs/url.jar ### `docker run` method 1. Build the image ``` -docker build . -t url:1.0 +docker build . -t url:latest ``` 2. Run the image ``` -docker run -p 4567:4567 -d url:1.0 +docker run -p 4567:4567 -d url:latest ``` 2.a Make the CSV file available to host ``` diff --git a/docker-compose.yml b/docker-compose.yml index b375dde..1442403 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ services: # TODO: Publish to docker hub build: context: . + container_name: url ports: - 4567:4567 environment: From f47afab80bab26c4d87bb13962c2f8a142508890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 16:11:14 +0100 Subject: [PATCH 8/9] Updated README.md Removed existing features from the planned features list --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 9f0752b..6cd7a72 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,6 @@ unnecessary features, or they didn't have all the features I wanted. ![Screenshot](./screenshot.png) # Planned features for 1.0 (in order of importance -- Some form of authentication -- Input validation (on client and server) - Deleting links using API and frontend - Better deduplication - Code cleanup From 6d7b065e9898e75069aa11035f9a090ffe97f32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Draga=C5=84czuk?= Date: Sun, 16 Feb 2020 16:52:54 +0100 Subject: [PATCH 9/9] Added option to delete a URL --- src/main/java/tk/draganczuk/url/App.java | 1 + src/main/java/tk/draganczuk/url/Routes.java | 13 +++++++ src/main/java/tk/draganczuk/url/UrlFile.java | 36 ++++++++++++++++---- src/main/resources/public/index.html | 1 + src/main/resources/public/js/main.js | 17 +++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/main/java/tk/draganczuk/url/App.java b/src/main/java/tk/draganczuk/url/App.java index 93a220a..603e690 100644 --- a/src/main/java/tk/draganczuk/url/App.java +++ b/src/main/java/tk/draganczuk/url/App.java @@ -36,6 +36,7 @@ public class App { before("/*", authFilter); get("/all", Routes::getAll); post("/new", Routes::addUrl); + delete("/:shortUrl", Routes::delete); }); get("/:shortUrl", Routes::goToLongUrl); diff --git a/src/main/java/tk/draganczuk/url/Routes.java b/src/main/java/tk/draganczuk/url/Routes.java index 2ad5fd1..2e9b590 100644 --- a/src/main/java/tk/draganczuk/url/Routes.java +++ b/src/main/java/tk/draganczuk/url/Routes.java @@ -54,4 +54,17 @@ public class Routes { return ""; } + public static String delete(Request req, Response res) { + String shortUrl = req.params("shortUrl"); + var longUrlOpt = urlFile + .findForShortUrl(shortUrl); + + if (longUrlOpt.isEmpty()) { + res.status(404); + return ""; + } + + urlFile.deleteEntry(String.format("%s,%s", shortUrl, longUrlOpt.get())); + return ""; + } } diff --git a/src/main/java/tk/draganczuk/url/UrlFile.java b/src/main/java/tk/draganczuk/url/UrlFile.java index 858a5e2..f666fb2 100644 --- a/src/main/java/tk/draganczuk/url/UrlFile.java +++ b/src/main/java/tk/draganczuk/url/UrlFile.java @@ -3,6 +3,7 @@ package tk.draganczuk.url; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.List; import java.util.Optional; @@ -41,18 +42,41 @@ public class UrlFile { public Optional findForShortUrl(String shortUrl){ try { - return Files.lines(file.toPath()) - .map(this::splitLine) - .filter(pair -> pair.getLeft().equals(shortUrl)) - .map(Pair::getRight) - .findAny(); + return Files.lines(file.toPath()) + .map(this::splitLine) + .filter(pair -> pair.getLeft().equals(shortUrl)) + .map(Pair::getRight) + .findAny(); } catch (IOException e) { return Optional.empty(); } } - public Pair splitLine(String line){ + public Pair splitLine(String line) { var split = line.split(","); return new Pair<>(split[0], split[1]); } + + public void deleteEntry(String entry) { + try { + File tmp = File.createTempFile(file.getName(), ".tmp"); + if (!tmp.exists()) { + tmp.createNewFile(); + } + + Files.lines(file.toPath()) + .filter(line -> !line.equals(entry)) + .forEach(line -> { + try { + Files.writeString(tmp.toPath(), line + "\n", StandardOpenOption.APPEND); + } catch (IOException e) { + e.printStackTrace(); + } + }); + + Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html index 897ebb3..15e29e4 100644 --- a/src/main/resources/public/index.html +++ b/src/main/resources/public/index.html @@ -50,6 +50,7 @@ Long URL Short url + diff --git a/src/main/resources/public/js/main.js b/src/main/resources/public/js/main.js index 850ff4e..a3c335a 100644 --- a/src/main/resources/public/js/main.js +++ b/src/main/resources/public/js/main.js @@ -23,9 +23,11 @@ const TR = (row) => { const tr = document.createElement("tr"); const longTD = TD(A(row.long)); const shortTD = TD(A_INT(row.short)); + const btn = deleteButton(row.short); tr.appendChild(longTD); tr.appendChild(shortTD); + tr.appendChild(btn); return tr; }; @@ -33,6 +35,21 @@ const TR = (row) => { const A = (s) => `${s}`; const A_INT = (s) => `${window.location.host}/${s}`; +const deleteButton = (shortUrl) => { + const btn = document.createElement("button"); + + btn.innerHTML = "×"; + + btn.onclick = e => { + e.preventDefault(); + fetch(`/api/${shortUrl}`, { + method: "DELETE" + }).then(_ => refreshData()); + }; + + return btn; +}; + const TD = (s) => { const td = document.createElement("td"); td.innerHTML = s;