import scala.util._
import scala.util.chaining._
import scala.collection.immutable.SortedMap
import cats._
import cats.syntax.all._
object Generator {
object Domain {
import java.nio.charset.StandardCharsets.UTF_8
import scala.xml.Elem
case class VPNProvider(name: String, domain: Vector[String]) {
import java.util.UUID
def org(reverse: Boolean, prefixes: String*): String =
(prefixes ++ domain).pipe(xs => if (reverse) xs.reverseIterator else xs).mkString(".")
def uuid(prefixes: String*): UUID = UUID.nameUUIDFromBytes(org(false, prefixes: _*).getBytes(UTF_8))
}
opaque type VPNLang = String
object VPNLang {
def apply(name: String): VPNLang = name
given Show[Option[VPNLang]] = _.fold(Monoid.empty[String])(_.show)
}
opaque type VPNNumber = Int
object VPNNumber {
final val Prefix = "0"
def unapply(str: String): Option[VPNNumber] = str.stripPrefix(Prefix).toIntOption
given Show[VPNNumber] = {
case n if n < 10 => s"$Prefix$n"
case n => s"$n"
}
}
enum VPNTier { case Free, Basic, Plus }
object VPNTier {
import io.circe.Decoder
import CommandLineParser.FromString
def orEmpty(tiers: Seq[VPNTier]): Set[VPNTier] = if (tiers.nonEmpty) tiers.toSet else Set(VPNTier.Free)
def fromOrdinalEither(ordinal: Int): Either[String, VPNTier] =
VPNTier.values.find(_.ordinal == ordinal).toRight(s"No such ${classOf[VPNTier].getSimpleName} value: $ordinal")
given Order[VPNTier] = Order.by(_.ordinal)
given Show[VPNTier] = {
case Free => s"$Free"
case _ => Monoid.empty[String]
}
given Decoder[VPNTier] = Decoder[Int].emap(fromOrdinalEither)
given FromString[VPNTier] = _.toInt.pipe(fromOrdinalEither).fold(e => throw IllegalArgumentException(e), identity)
}
sealed trait VPNEntity
object VPNEntity {
import cats.instances.order._
case class Single(tier: VPNTier, num: VPNNumber, lang1: VPNLang, lang2: Option[VPNLang], postfix: Option[String]) extends VPNEntity {
import VPNLang.given
import VPNNumber.given
val (name, host) = Vector(lang1.show, lang2.show, tier.show) pipe { prefix =>
def fmt(it: Iterable[String]*)(up: Boolean) =
Iterator.concat(it: _*).filter(_.nonEmpty).mkString("-").pipe(s => if (up) s.toUpperCase else s.toLowerCase)
show"${fmt(prefix, postfix)(up = true)}#$num" -> fmt(prefix :+ num.show, postfix)(up = false)
}
}
class Many(underlying: Vector[Single], grouped: SortedMap[VPNTier, Int]) extends VPNEntity with Iterable[Single] {
def tiers: String = grouped.keys.mkString(" + ")
def countByTier: String = grouped.view.map { case (t, n) => s"${n}x $t" }.mkString(" + ")
def iterator: Iterator[Single] = underlying.iterator
}
def apply(entities: Vector[Single]): Many = entities.sorted.pipe { sorted =>
Many(sorted, sorted.groupMapReduce(_.tier)(_ => 1)(_ + _).to(SortedMap))
}
def apply(
tier: VPNTier = VPNTier.Free,
lang1: VPNLang,
lang2: Option[VPNLang] = None,
num: VPNNumber,
postfix: Option[String] = None
): Single = Single(tier, num, lang1, lang2, postfix)
given Order[Single] = Order.fromOrdering(
Ordering
.by[Single, VPNTier](_.tier)
.orElseBy(it => (it.lang1, it.lang2))
.orElseBy(_.num)
.orElseBy(_.postfix)
)
}
opaque type VPNSchema = Elem
object VPNSchema {
def apply(e: Elem): VPNSchema = e
given Show[VPNSchema] = schema => {
import java.io.StringWriter
import java.lang.System.{lineSeparator => EOL}
import scala.xml.PrettyPrinter
import scala.xml.dtd.{DocType, PublicID}
StringBuilder()
.append(s"""""")
.append(EOL)
.append(DocType("plist", PublicID("-//Apple//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd"), Nil))
.append(EOL)
.tap(PrettyPrinter(width = 120, step = 2).format(schema, _))
.result
}
}
case class Credentials(usr: String, pwd: String, cert: String)
}
object Parse {
import io.circe.{Json, Decoder}
import io.circe.jawn.JawnParser
import Domain._
def apply(bytes: Array[Byte], tiers: Set[VPNTier]): Either[String, Vector[VPNEntity.Single]] = {
val N: VPNNumber.type = VPNNumber
val L: VPNLang.type = VPNLang
extension (json: Json) def field[A: Decoder](field: String): Either[String, A] =
json.hcursor.get[A](field).leftMap(_.show)
JawnParser(false)
.parseByteArray(bytes)
.leftMap(_.show)
.flatMap(_.field[Vector[Json]]("LogicalServers"))
.flatMap(_.traverseFilter { server =>
(server.field[VPNTier]("Tier"), server.field[String]("Name")).mapN { (tier, name) =>
if (!tiers(tier)) None
else name match {
case s"$lang-FREE#${N(num)}" =>
VPNEntity(lang1 = L(lang), num = num).some
case s"$lang1-$lang2#${N(num)}-$postfix" =>
VPNEntity(tier, L(lang1), L(lang2).some, num, postfix.some).some
case s"$lang1-$lang2#${N(num)}" =>
VPNEntity(tier, L(lang1), L(lang2).some, num).some
case s"$lang#${N(num)}-$postfix" =>
VPNEntity(tier, L(lang), num = num, postfix = postfix.some).some
case s"$lang#${N(num)}" =>
VPNEntity(tier, L(lang), num = num).some
}
}
})
}
}
object Download {
import java.net.URI
import java.net.http.{HttpRequest, HttpClient, HttpResponse}
import java.time.Duration
import java.nio.file.{Paths, Files}
import java.util.Base64.{getEncoder => B64}
import Domain._
def cert(prov: VPNProvider, url: String): Either[String, String] = {
// download(url, s"${prov.name} Root CA")(B64.encodeToString)
Right(B64.encodeToString(Files.readAllBytes(Paths.get("/Users/rb/Documents/projects/Scala/VPNConf/data/ProtonVPN_Root_CA.der"))))
}
def registry(prov: VPNProvider, url: String, tiers: Set[VPNTier]): Either[String, Vector[VPNEntity.Single]] = {
// download(url, s"${prov.name} Registry")(Parse(_, tiers).fold(sys.error, identity))
Parse(Files.readAllBytes(Paths.get("/Users/rb/Documents/projects/Scala/VPNConf/data/VPNs.json")), tiers)
}
def download[A](url: String, name: String)(fn: Array[Byte] => A): Either[String, A] =
(for {
client <- Try(HttpClient.newBuilder.connectTimeout(Duration.ofSeconds(20)).build)
res <- Try(client.send(HttpRequest.newBuilder(URI(url)).build, HttpResponse.BodyHandlers.ofByteArray()))
decoded <- Try(fn(res.body))
} yield decoded)
.fold(e => s"Failed to download $name: $e".asLeft, _.asRight)
}
object Make {
import Domain._
def vpn(prov: VPNProvider, vpn: VPNEntity.Single, cred: Credentials) =
PayloadIdentifier
{prov.org(reverse = true, vpn.host, "vpn")}
PayloadUUID
{prov.uuid(vpn.host, "vpn")}
PayloadType
com.apple.vpn.managed
PayloadVersion
1
PayloadDisplayName
{prov.name} {vpn.name}
UserDefinedName
{prov.name} {vpn.name}
VPNType
IKEv2
IKEv2
RemoteAddress
{prov.org(reverse = false, vpn.host)}
RemoteIdentifier
{prov.org(reverse = false, vpn.host)}
LocalIdentifier
{cred.usr}
ServerCertificateIssuerCommonName
{prov.name} Root CA
TLSMinimumVersion
1.2
EnablePFS
1
DisableRedirect
OnDemandEnabled
0
OnDemandRules
Action
Connect
AuthenticationMethod
Certificate
ExtendedAuthEnabled
1
AuthName
{cred.usr}
AuthPassword
{cred.pwd}
def cert(prov: VPNProvider, cred: Credentials) =
PayloadIdentifier
{prov.org(reverse = true, "ca")}
PayloadUUID
{prov.uuid("ca")}
PayloadType
com.apple.security.root
PayloadVersion
1
PayloadContent
{cred.cert}
def profile(prov: VPNProvider, entities: VPNEntity.Many, cred: Credentials) = VPNSchema {
PayloadIdentifier
{prov.org(reverse = true, "profile")}
PayloadUUID
{prov.uuid("profile")}
PayloadType
Configuration
PayloadVersion
1
PayloadDisplayName
{prov.name} {entities.tiers}
PayloadDescription
This profile installs {entities.countByTier} VPN servers from {prov.name} using native IKEv2.
PayloadContent
{cert(prov, cred)}
{entities.map { x => println(x.name); x }.flatMap(vpn(prov, _, cred))}
}
}
@main def main(user: String, password: String, tiers: Domain.VPNTier*): Unit = {
import Domain._
import VPNSchema.given
val provider = VPNProvider(
name = "ProtonVPN",
domain = Vector("protonvpn", "com")
)
(for {
cert <- Download.cert(provider, "https://protonvpn.com/download/ProtonVPN_ike_root.der")
reg <- Download.registry(provider, "https://api.protonmail.ch/vpn/logicals", VPNTier.orEmpty(tiers))
} yield Make.profile(
prov = provider,
entities = VPNEntity(reg),
cred = Credentials(usr = user, pwd = password, cert = cert)
))
.fold(sys.error, _.show.tap(println))
}
}