diff --git a/src/Podsync/Caddy.Dockerfile b/src/Podsync/Caddy.Dockerfile new file mode 100644 index 0000000..820548a --- /dev/null +++ b/src/Podsync/Caddy.Dockerfile @@ -0,0 +1,2 @@ +FROM zzrot/alpine-caddy +COPY Caddyfile /etc/Caddyfile \ No newline at end of file diff --git a/src/Podsync/Caddyfile b/src/Podsync/Caddyfile new file mode 100644 index 0000000..17d9949 --- /dev/null +++ b/src/Podsync/Caddyfile @@ -0,0 +1,32 @@ +new.podsync.net +gzip + +errors stdout + +ratelimit 2 3 second { + /feed + /download +} + +proxy / {%REDIRECT_HOST%}:{%REDIRECT_PORT%} { + header_upstream Host {host} + transparent +} + +tls pavlenko.maksym@gmail.com + +header / { + # Remove ASP.NET specific headers + -Server + -X-Powered-By + -X-Sourcefiles + + # Enable cross-site filter (XSS) and tell browser to block detected attacks + X-XSS-Protection "1; mode=block" + + # Prevent some browsers from MIME-sniffing a response away from the declared Content-Type + X-Content-Type-Options "nosniff" + + # Disallow the site to be rendered within a frame (clickjacking protection) + X-Frame-Options "DENY" +} diff --git a/src/Podsync/Controllers/StatusController.cs b/src/Podsync/Controllers/StatusController.cs index bf40a60..5f85613 100644 --- a/src/Podsync/Controllers/StatusController.cs +++ b/src/Podsync/Controllers/StatusController.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Podsync.Services.Resolver; using Podsync.Services.Storage; @@ -7,6 +8,8 @@ namespace Podsync.Controllers { public class StatusController : Controller { + private const string ErrorStatus = "ERROR"; + private readonly IStorageService _storageService; private readonly IResolverService _resolverService; @@ -18,11 +21,23 @@ namespace Podsync.Controllers public async Task Index() { - var time = await _storageService.Ping(); + var storageStatus = ErrorStatus; + + try + { + var time = await _storageService.Ping(); + storageStatus = time.ToString(); + } + catch (Exception) + { + // Nothing to do + } + + var resolverStatus = _resolverService.Version ?? ErrorStatus; return $"Path: {Request.Path}\r\n" + - $"Redis: {time}\r\n" + - $"Resolve: {_resolverService.Version}"; + $"Redis: {storageStatus}\r\n" + + $"Resolve: {resolverStatus}"; } } } \ No newline at end of file diff --git a/src/Podsync/Deploy.txt b/src/Podsync/Deploy.txt new file mode 100644 index 0000000..abcba71 --- /dev/null +++ b/src/Podsync/Deploy.txt @@ -0,0 +1,14 @@ +$> ./Up.ps1 + +$> docker-machine create --driver digitalocean + --digitalocean-accescean-image=ubuntu-16-04-x64 + --digitalocean-region=ams2 + --digitalocean-backups=true + --digitalocean-size=512mb + podsync + +$> docker-machine env podsync + +$> $env:COMPOSE_CONVERT_WINDOWS_PATHS=0 + +$> docker-compose up -d \ No newline at end of file diff --git a/src/Podsync/Dockerfile b/src/Podsync/Dockerfile new file mode 100644 index 0000000..e1ea660 --- /dev/null +++ b/src/Podsync/Dockerfile @@ -0,0 +1,16 @@ +FROM microsoft/aspnetcore:1.0.3 + +ARG PORT=56247 +ARG PUBLISH_DIR="bin/Publish" + +WORKDIR /app + +ENV ASPNETCORE_URLS http://+:$PORT +EXPOSE $PORT + +COPY $PUBLISH_DIR . + +# Install Python for youtube-dl +RUN apt-get update && apt-get install -y python && chmod a+rx youtube-dl + +ENTRYPOINT ["dotnet", "Podsync.dll"] \ No newline at end of file diff --git a/src/Podsync/Services/Storage/RedisStorage.cs b/src/Podsync/Services/Storage/RedisStorage.cs index 7cfc023..e0da5de 100644 --- a/src/Podsync/Services/Storage/RedisStorage.cs +++ b/src/Podsync/Services/Storage/RedisStorage.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Threading; using System.Threading.Tasks; using HashidsNet; using Microsoft.Extensions.Options; @@ -28,31 +30,62 @@ namespace Podsync.Services.Storage private static readonly IHashids HashIds = new Hashids(IdSalt, IdLength); - private readonly IDatabase _db; + private readonly string _cs; + private IDatabase _db; public RedisStorage(IOptions configuration) { - var cs = configuration.Value.RedisConnectionString; - var connection = ConnectionMultiplexer.ConnectAsync(cs).GetAwaiter().GetResult(); + _cs = configuration.Value.RedisConnectionString; + } - _db = connection.GetDatabase(); + private IDatabase Db + { + get { return LazyInitializer.EnsureInitialized(ref _db, () => Connect(_cs).GetAwaiter().GetResult().GetDatabase()); } + } + + private static async Task Connect(string cs) + { + var options = ConfigurationOptions.Parse(cs); + + try + { + return await ConnectionMultiplexer.ConnectAsync(options); + } + catch (PlatformNotSupportedException) + { + // Can't connect via address on Linux environments, using workaround + // See https://github.com/StackExchange/StackExchange.Redis/issues/410#issuecomment-246332140 + var addressEndpoint = options.EndPoints.SingleOrDefault() as DnsEndPoint; + if (addressEndpoint != null) + { + var ip = await Dns.GetHostEntryAsync(addressEndpoint.Host); + options.EndPoints.Remove(addressEndpoint); + options.EndPoints.Add(ip.AddressList.First(), addressEndpoint.Port); + } + + return await ConnectionMultiplexer.ConnectAsync(options); + } } public void Dispose() { - _db.Multiplexer.Dispose(); + if (_db != null) + { + _db.Multiplexer.Dispose(); + _db = null; + } } public Task Ping() { - return _db.PingAsync(); + return Db.PingAsync(); } public async Task Save(FeedMetadata metadata) { var id = await MakeId(); - await _db.HashSetAsync(id, new[] + await Db.HashSetAsync(id, new[] { new HashEntry(ProviderField, metadata.Provider.ToString()), new HashEntry(TypeField, metadata.LinkType.ToString()), @@ -61,7 +94,7 @@ namespace Podsync.Services.Storage new HashEntry(PageSizeField, metadata.PageSize), }); - await _db.KeyExpireAsync(id, TimeSpan.FromDays(1)); + await Db.KeyExpireAsync(id, TimeSpan.FromDays(1)); return id; } @@ -73,10 +106,10 @@ namespace Podsync.Services.Storage throw new ArgumentException("Feed key can't be empty"); } - var entries = await _db.HashGetAllAsync(key); + var entries = await Db.HashGetAllAsync(key); // Expire after 3 month if no use - await _db.KeyExpireAsync(key, TimeSpan.FromDays(90)); + await Db.KeyExpireAsync(key, TimeSpan.FromDays(90)); if (entries.Length == 0) { @@ -107,12 +140,12 @@ namespace Podsync.Services.Storage public Task ResetCounter() { - return _db.KeyDeleteAsync(IdKey); + return Db.KeyDeleteAsync(IdKey); } public async Task MakeId() { - var id = await _db.StringIncrementAsync(IdKey); + var id = await Db.StringIncrementAsync(IdKey); return HashIds.EncodeLong(id); } diff --git a/src/Podsync/Up.ps1 b/src/Podsync/Up.ps1 new file mode 100644 index 0000000..e58b737 --- /dev/null +++ b/src/Podsync/Up.ps1 @@ -0,0 +1,13 @@ +$OUTPUT_DIR = 'bin/Publish' + +& "dotnet" restore + +Remove-Item $OUTPUT_DIR -Force -Recurse -ErrorAction Ignore + +& "dotnet" publish --configuration release --output $OUTPUT_DIR + +& "docker" build -t podsync . + +# docker run -d -p 5001:5001 podsync + +& "docker-compose" up -d \ No newline at end of file diff --git a/src/Podsync/docker-compose.yml b/src/Podsync/docker-compose.yml new file mode 100644 index 0000000..40c5a56 --- /dev/null +++ b/src/Podsync/docker-compose.yml @@ -0,0 +1,27 @@ +version: '2' + +services: + app: + image: podsync + build: + context: . + dockerfile: Dockerfile + args: + - PORT=56247 + environment: + - Podsync:RedisConnectionString=redis + redis: + image: redis + command: redis-server --appendonly yes + volumes: + - /redis_data:/data + caddy: + build: + context: . + dockerfile: Caddy.Dockerfile + ports: + - 80:80 + - 443:443 + environment: + - REDIRECT_HOST=app + - REDIRECT_PORT=56247 diff --git a/src/Podsync/project.json b/src/Podsync/project.json index 4e63715..a1ee1ae 100644 --- a/src/Podsync/project.json +++ b/src/Podsync/project.json @@ -57,7 +57,9 @@ "Views", "Areas/**/Views", "appsettings.json", - "web.config" + "web.config", + "Dockerfile", + "youtube-dl" ] }, "scripts": { diff --git a/src/Podsync/youtube-dl b/src/Podsync/youtube-dl new file mode 100644 index 0000000..90811dc Binary files /dev/null and b/src/Podsync/youtube-dl differ