├── .gitignore ├── LICENSE ├── README.md ├── app.json ├── app ├── Global.scala ├── controllers │ └── HomeController.scala ├── datastore │ └── UriDataStore.scala ├── engines │ └── UriEngine.scala ├── models │ ├── HostDetails.scala │ ├── HostType.scala │ ├── MetricsSummary.scala │ └── RemoveParamRes.scala └── views │ └── index.scala.html ├── build.sbt ├── conf ├── application.conf └── routes ├── project ├── build.properties └── plugins.sbt ├── public ├── images │ ├── favicon.png │ └── lang-logo.png ├── javascripts │ └── index.js └── stylesheets │ └── index.css └── system.properties /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | logs 3 | project/project 4 | project/target 5 | target 6 | tmp 7 | .history 8 | dist 9 | /.idea 10 | /*.iml 11 | /out 12 | /.idea_modules 13 | /.classpath 14 | /.project 15 | /RUNNING_PID 16 | /.settings 17 | hk-debug 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DontRefMe 2 | 3 | Don't you hate all these companies sending so many ref data to learn more things about you. 4 | ie: 5 | 1. From where you opened that link 6 | 2. How you're connected to the person who sent you the link 7 | 3. How many times you have clicked the link 8 | 9 | Let's try to avoid that. 10 | 11 | ## Help us Improve 12 | 13 | Please feel free to create a PR if you think your code can make it better. 14 | 15 | ## Running Locally 16 | 17 | Make sure you have Play and sbt installed. Also, install the [Heroku Toolbelt](https://toolbelt.heroku.com/). 18 | 19 | ```sh 20 | $ git clone https://github.com/rishijash/dontrefme.git 21 | $ cd dontrefme 22 | $ sbt run 23 | ``` 24 | 25 | ## Documentation 26 | 27 | For more information about using Play and Scala on Heroku, see these Dev Center articles: 28 | 29 | - [Play and Scala on Heroku](https://devcenter.heroku.com/categories/language-support#scala-and-play) 30 | 31 | 32 | Developed by Rushi Jash and Shivani Patel 33 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Start on Heroku: Scala", 3 | "description": "A barebones Scala app (using the Play framework), which can easily be deployed to Heroku.", 4 | "image": "heroku/scala", 5 | "addons": [ "heroku-postgresql" ] 6 | } 7 | -------------------------------------------------------------------------------- /app/Global.scala: -------------------------------------------------------------------------------- 1 | import play.api._ 2 | import play.api.mvc._ 3 | 4 | import play.api.Logger 5 | import scala.concurrent.Future 6 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 7 | 8 | object Global extends WithFilters() { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/controllers/HomeController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import engines.UriEngine 4 | import javax.inject._ 5 | import play.api.Configuration 6 | import play.api.mvc._ 7 | 8 | @Singleton 9 | class HomeController @Inject()(config: Configuration) extends Controller { 10 | 11 | val uriEngine = new UriEngine(config) 12 | 13 | def index() = Action { implicit request: Request[AnyContent] => 14 | val maybeUriObj = uriEngine.getRequestUri[AnyContent](request) 15 | if (maybeUriObj.isDefined) { 16 | val newUri = uriEngine.removeRefFromUri(maybeUriObj.get) 17 | Redirect(newUri) 18 | } else { 19 | val summary = uriEngine.getSummary() 20 | Ok(views.html.index(summary)) 21 | } 22 | } 23 | 24 | def displayNoRefUri() = Action { implicit request: Request[AnyContent] => 25 | val formData = request.body.asFormUrlEncoded 26 | val requestUri = formData.get("requestLink").headOption 27 | val responseUri = requestUri.map(uriEngine.removeRefFromUri(_)) 28 | val summary = uriEngine.getSummary().map(s => s.copy(totalCalls = s.totalCalls + 1)) 29 | Ok(views.html.index(summary, responseUri)) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/datastore/UriDataStore.scala: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import java.io.{File, FileInputStream} 4 | import java.util 5 | 6 | import com.google.auth.oauth2.ServiceAccountCredentials 7 | import com.google.cloud.firestore.{FirestoreOptions, QueryDocumentSnapshot} 8 | import javax.inject.Inject 9 | import org.slf4j.LoggerFactory 10 | import play.api.Configuration 11 | import java.io.BufferedWriter 12 | import java.io.FileWriter 13 | 14 | import scala.collection.JavaConversions._ 15 | import java.util.UUID.randomUUID 16 | import java.util.Calendar 17 | 18 | class UriDataStore @Inject()(config: Configuration) { 19 | 20 | private val log = LoggerFactory.getLogger(this.getClass.getName) 21 | 22 | private val collectionName = "metrics" 23 | 24 | val hostKey = "host" 25 | val totalParamsCountKey = "totalParamsCount" 26 | val safeParamsCountKey = "safeParamsCount" 27 | val createdKey = "created" 28 | 29 | private val privateKeyId = scala.util.Properties.envOrElse("private_key_id", "") 30 | private val privateKey = scala.util.Properties.envOrElse("private_key", "") 31 | private val clientEmail = scala.util.Properties.envOrElse("client_email", "") 32 | private val clientId = scala.util.Properties.envOrElse("client_id", "") 33 | private val clientCertUrl = scala.util.Properties.envOrElse("client_x509_cert_url", "") 34 | 35 | private val createKeyFileData = 36 | s""" 37 | |{ 38 | | "type": "service_account", 39 | | "project_id": "dontrefme", 40 | | "private_key_id": "${privateKeyId}", 41 | | "private_key": "${privateKey}", 42 | | "client_email": "${clientEmail}", 43 | | "client_id": "${clientId}", 44 | | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 45 | | "token_uri": "https://oauth2.googleapis.com/token", 46 | | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 47 | | "client_x509_cert_url": "${clientCertUrl}" 48 | |} 49 | """.stripMargin 50 | 51 | final val tempFile = File.createTempFile("dontrefmeKey",".json") 52 | final val writer: BufferedWriter = new BufferedWriter(new FileWriter(tempFile.getAbsolutePath)) 53 | writer.write(createKeyFileData) 54 | writer.close() 55 | 56 | private val firebaseClient = FirestoreOptions.newBuilder() 57 | .setCredentials(ServiceAccountCredentials.fromStream(new FileInputStream(tempFile.getAbsolutePath))) 58 | .build().getService 59 | 60 | def createMetrics(host: String, totalParamsCount: Int, safeParamsCount: Int = 0): Boolean = { 61 | try { 62 | val created = Calendar.getInstance().getTime().toString() 63 | val docData = new util.HashMap[String, Any](Map(hostKey -> host, 64 | totalParamsCountKey -> totalParamsCount, safeParamsCountKey -> safeParamsCount, createdKey -> created)) 65 | val id = randomUUID().toString 66 | firebaseClient.collection(collectionName).document(id).set(docData) 67 | true 68 | } catch { 69 | case e: Exception => 70 | log.error(s"Exception: ${e.getMessage} in creating metrics for host: ${host}, totalParamsCount: ${totalParamsCount}, safeParamsCount: ${safeParamsCount}") 71 | false 72 | } 73 | } 74 | 75 | def getMetricsSummary(): Option[List[QueryDocumentSnapshot]] = { 76 | try { 77 | val allData = firebaseClient.collection(collectionName).get().get() 78 | val allDocuments = allData.getDocuments.toList 79 | Some(allDocuments) 80 | } catch { 81 | case e: Exception => 82 | log.error(s"Exception: ${e.getMessage} in getting metrics.") 83 | None 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/engines/UriEngine.scala: -------------------------------------------------------------------------------- 1 | package engines 2 | 3 | import java.net.{URI, URLDecoder} 4 | 5 | import datastore.UriDataStore 6 | import javax.inject.Inject 7 | import models.{HostDetails, HostType, MetricsSummary, RemoveParamRes} 8 | import org.slf4j.LoggerFactory 9 | import play.api.Configuration 10 | import play.api.mvc.Request 11 | 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | import scala.concurrent.Future 14 | 15 | class UriEngine @Inject()(config: Configuration) { 16 | 17 | val baseUrl = config.getString("url.base").get 18 | val herokuBaseUrl = config.getString("url.herokuBase").get 19 | val store = new UriDataStore(config) 20 | 21 | val log = LoggerFactory.getLogger(this.getClass.getName) 22 | 23 | def getRequestUri[T](request: Request[T]): Option[URI] = { 24 | val uriObj = getUriObj(request.rawQueryString) 25 | val isSelfUriRequest = (uriObj.toString.contains(baseUrl) || uriObj.toString.contains(herokuBaseUrl)) 26 | if (isSelfUriRequest) None else uriObj 27 | } 28 | 29 | def removeRefFromUri(uri: String): String = { 30 | removeRefFromUri(new URI(uri)) 31 | } 32 | 33 | def removeRefFromUri(uriObj: URI): String = { 34 | try { 35 | val maybeHostTypeDetails = HostType.getHostTypeDetailsFromHostUriOpt(uriObj.getHost) 36 | val refRemoverResponse = maybeHostTypeDetails match { 37 | case Some(hostTypeDetails) => { 38 | val hostDetails = hostTypeDetails._2 39 | refRemoverWithRuleEngine(uriObj, hostDetails.safeParams, hostDetails.removeRefFromStringEnd, hostDetails.redirectParams) 40 | } 41 | case None => refRemoverWithRuleEngine(uriObj, HostType.commonSafeParams) 42 | } 43 | addMetrics(refRemoverResponse.host, refRemoverResponse.totalParams, refRemoverResponse.filterdParams) 44 | refRemoverResponse.newUri 45 | } catch { 46 | case e: Exception => { 47 | log.error(s"Exception in removing ref: ${e}") 48 | uriObj.toString 49 | } 50 | } 51 | } 52 | 53 | def getSummary(): Option[MetricsSummary] = { 54 | val maybeSummaryData = store.getMetricsSummary() 55 | val summary = maybeSummaryData.map(allDocuments => { 56 | val totalCalls = allDocuments.size 57 | val totalParamsFiltered = allDocuments.map(doc => { 58 | val totalParamCount = doc.getData.get(store.totalParamsCountKey).toString 59 | val totalSafeCount = doc.getData.get(store.safeParamsCountKey).toString 60 | val diff = totalParamCount.toInt - totalSafeCount.toInt 61 | diff 62 | }).sum 63 | MetricsSummary(totalCalls, totalParamsFiltered) 64 | }) 65 | summary 66 | } 67 | 68 | private def getUriObj[T](requestUri: String): Option[URI] = { 69 | try { 70 | var uri = requestUri 71 | // If starts with space, just remove it 72 | if(uri.startsWith("%20")) { 73 | uri = uri.replaceFirst("%20", "") 74 | } 75 | if (!uri.startsWith("http") && !uri.startsWith("https")) { 76 | uri = s"http://${uri}" 77 | } 78 | val res = new URI(uri) 79 | val maybeHost = Option(res.getHost) 80 | maybeHost.map(_ => res) 81 | } catch { 82 | case _: Exception => None 83 | } 84 | } 85 | 86 | private def addMetrics(host: String, totalParamsCount: Int, safeParamsCount: Int = 0): Future[Unit] = { 87 | Future { 88 | store.createMetrics(host, totalParamsCount, safeParamsCount) 89 | } 90 | } 91 | 92 | private def refRemoverWithRuleEngine(uriObj: URI, 93 | safeParamsList: List[String], 94 | removeRefFromUriEnd: Boolean = false, 95 | redirectParams: List[String] = List.empty[String]): RemoveParamRes = { 96 | val queryParamsMap = getQueryParamsMap(uriObj) 97 | val maybeRedirectParam = queryParamsMap.find(p => redirectParams.contains(p._1)) 98 | if(maybeRedirectParam.isEmpty) { 99 | val (newUri, safeParamsSize) = if (queryParamsMap.nonEmpty) { 100 | val uriWithNoParams = getUriWithNoParam(uriObj, removeRefFromUriEnd) 101 | // Add filtered Params 102 | val safeParams = queryParamsMap.filter { 103 | case (k, _) => safeParamsList.contains(k) 104 | } 105 | val safeParamsStr = safeParams.map { 106 | case (k, v) => s"${k}=${v}" 107 | }.mkString("&") 108 | val newUri = s"${uriWithNoParams}?${safeParamsStr}" 109 | (newUri, safeParams.size) 110 | } else { 111 | // If no query params, no need to filter anything. Just call the requested URI 112 | val newUri = uriObj.toString 113 | (newUri, 0) 114 | } 115 | RemoveParamRes(newUri, uriObj.getHost, queryParamsMap.size, safeParamsSize) 116 | } else { 117 | val newUriObj = new URI(maybeRedirectParam.get._2) 118 | refRemoverWithRuleEngine(newUriObj, safeParamsList, removeRefFromUriEnd, List.empty) 119 | } 120 | } 121 | 122 | private def getUriWithNoParam(uriObj: URI, removeRefFromUriEnd: Boolean = false): String = { 123 | val rawQuery = Option(uriObj.getRawQuery) 124 | var uriString = uriObj.toString.replace("?", "") 125 | if(removeRefFromUriEnd) { 126 | val indexofRef = uriString.indexOf("ref=") 127 | uriString = uriString.substring(0, indexofRef) 128 | } 129 | rawQuery.map(uriString.replace(_, "")).getOrElse(uriString)} 130 | 131 | private def getQueryParamsMap(uriObj: URI): Map[String, String] = { 132 | val queryParamsString = Option(uriObj.getRawQuery) 133 | queryParamsString.map(_.split("&").map(v => { 134 | val m = v.split("=", 2).map(s => URLDecoder.decode(s, "UTF-8")) 135 | m(0) -> (if(m.size > 1) m(1) else "") 136 | }).toMap 137 | ).getOrElse(Map.empty[String, String]) 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /app/models/HostDetails.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | case class HostDetails ( 4 | uri: String, 5 | safeParams: List[String], 6 | removeRefFromStringEnd: Boolean = false, 7 | redirectParams: List[String] = List.empty[String] 8 | ) 9 | -------------------------------------------------------------------------------- /app/models/HostType.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | object HostType extends Enumeration { 4 | 5 | type HostType = Value 6 | val Google, Youtube, Amazon, Yahoo, Facebook, Instagram, AmazonDe, ProductHunt, AMCTheatres = Value 7 | 8 | val commonSafeParams = List("g", "k", "p", "q", "v") 9 | 10 | val hostMap = Map( 11 | Google -> HostDetails("google.com", safeParams = List("q", "start")), 12 | Youtube -> HostDetails("youtube.com", safeParams = List("search_query", "v")), 13 | Amazon -> HostDetails("amazon.com", safeParams = List("k"), removeRefFromStringEnd = true), 14 | AmazonDe -> HostDetails("amazon.de", safeParams = List("node", "k"), removeRefFromStringEnd = true), 15 | Yahoo -> HostDetails("yahoo.com", safeParams = List("p")), 16 | Instagram -> HostDetails("instagram.com", safeParams = List.empty, redirectParams = List("u")), 17 | ProductHunt -> HostDetails("producthunt.com", safeParams = List("utm_source")), 18 | AMCTheatres -> HostDetails("amctheatres.com", safeParams = List("affiliateCode", "movieref")), 19 | Facebook -> HostDetails("facebook.com", safeParams = List.empty, redirectParams = List("u")) 20 | ) 21 | 22 | def withNameOpt(s: String): Option[Value] = values.find(_.toString == s) 23 | 24 | def getHostTypeDetailsFromHostUriOpt(hostUri: String): Option[(HostType.Value, HostDetails)] = { 25 | hostMap.find { 26 | case (k, v) => hostUri.contains(v.uri) 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/models/MetricsSummary.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | case class MetricsSummary ( 4 | totalCalls: Long, 5 | totalParamsFilterd: Long 6 | ) 7 | -------------------------------------------------------------------------------- /app/models/RemoveParamRes.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | case class RemoveParamRes ( 4 | newUri: String, 5 | host: String, 6 | totalParams: Int, 7 | filterdParams: Int 8 | ) 9 | -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(summary: Option[MetricsSummary], responseUri: Option[String] = None) 2 | 3 | 4 | 5 | dontref.me - remove ref data from your link 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |

Don't Ref me !!

20 |
21 |
22 |
23 | DontRef.Me - Save yourself from being tracked by evil URL references | Product Hunt Embed 24 |
25 |
26 |
27 |
28 |
29 |

Paste your link here:

30 |
31 | 32 | 33 |
34 |
35 | @if(responseUri.isDefined) { 36 |
Your link without Ref:
37 | 42 | } 43 | @if(summary.isDefined) { 44 |
45 |
46 |
So far..
47 | 48 | @{summary.get.totalParamsFilterd} 49 | references removed
50 | 51 |
52 | } 53 |
54 |
55 |
56 |

Ever wondered why the URLs are longer than required? 57 |
URLs can be confusing. Sometimes they are super short and other times they are three lines long. So, what exactly is all that stuff in between? Is it safe to forward that URL or it carries your information along with it? 58 | URLs can be confusing!! 59 |

60 |
Anatomy of URL:
61 |

URL stands for Uniform Resource Locator, or in other words, the web address of an online resource, i.e. a web site or document. 62 |

63 | In order to better understand this, let's take an example of Amazon. When you go to Amazon.com 64 | and type in a search term like “Shoes” and select a product, you’ll get the following resulting Amazon link:
65 | 66 | https://www.amazon.com/NIKE-Revolution-Running-Black-Cool-Regular/dp/B06XKL3DBZ/ref=sr_1_4?keywords=shoes&qid=1560048374&s=gateway&sr=8-4

67 | Like our search query, this link shows the keywords we used to find the product. It’s fun to look at what other people typed to find their product when they 68 | send this long link. Notice the parameter "qid" - it changes when the same search is performed using different account or different IP address. 69 | Therefore, as you can see, using a full-width URL on a search will leave a trail that goes back to you! And these full length URLs are used by many companies to add information 70 | that leads to you. 71 |

Let's try to avoid that!
72 | Simply paste your Url here or add dontref.me/? before the url and we will remove all references from it.
73 | Example: Call dontref.me/?https://www.amazon.com/NIKE-Revolution-Running-Black-Cool-Regular/dp/B06XKL3DBZ/ref=sr_1_4?keywords=shoes&qid=1560048374&s=gateway&sr=8-4 and get url without any references: https://www.amazon.com/NIKE-Revolution-Running-Black-Cool-Regular/dp/B06XKL3DBZ/ 74 |

75 |
76 |
77 |
78 |
79 |
80 | Created by Rushi & Shivani while trying to respect Privacy one step at a time. :) 81 | We are open source. Help us improve: http://github.com/rishijash/dontrefme 82 |
83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := """play-getting-started""" 2 | 3 | version := "1.0-SNAPSHOT" 4 | 5 | lazy val root = (project in file(".")).enablePlugins(PlayScala) 6 | 7 | scalaVersion := "2.11.7" 8 | 9 | libraryDependencies ++= Seq( 10 | jdbc, 11 | cache, 12 | "org.postgresql" % "postgresql" % "9.4-1201-jdbc41", 13 | ws, 14 | "com.google.cloud" % "google-cloud-firestore" % "1.7.0", 15 | "org.postgresql" % "postgresql" % "9.4-1201-jdbc41" 16 | ) 17 | 18 | libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _ ) 19 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # 8 | # This must be changed for production, but we recommend not changing it in this file. 9 | # 10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 11 | application.secret="" 12 | 13 | # The application languages 14 | # ~~~~~ 15 | application.langs="en" 16 | 17 | 18 | url { 19 | base = "dontref.me" 20 | herokuBase = "dontrefme.herokuapp.com" 21 | } 22 | 23 | play.filters.disabled += play.filters.csrf.CSRFFilter 24 | 25 | # Root logger: 26 | logger.root=ERROR 27 | 28 | # Logger used by the framework: 29 | logger.play=INFO 30 | 31 | # Logger provided to your application: 32 | logger.application=DEBUG 33 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / @controllers.HomeController.index() 7 | POST / @controllers.HomeController.displayNoRefUri() 8 | 9 | # Map static resources from the /public folder to the /assets URL path 10 | GET /assets/*file controllers.Assets.at(path="/public", file) 11 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Fri Oct 10 13:27:43 CEST 2014 3 | template.uuid=a4c33fda-0fa0-458b-a153-0a28c61df830 4 | sbt.version=0.13.11 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 2 | 3 | // The Play plugin 4 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3") 5 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rishijash/DontRefMe/41b8ce342884ed485293560f90a71398a083aca5/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/lang-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rishijash/DontRefMe/41b8ce342884ed485293560f90a71398a083aca5/public/images/lang-logo.png -------------------------------------------------------------------------------- /public/javascripts/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rishijash/DontRefMe/41b8ce342884ed485293560f90a71398a083aca5/public/javascripts/index.js -------------------------------------------------------------------------------- /public/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | #myDIV { 2 | width: 100%; 3 | margin-top: 20px; 4 | } 5 | 6 | hr { 7 | margin-top: 0px; 8 | margin-bottom: 5px; 9 | } 10 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.8 2 | --------------------------------------------------------------------------------