├── Tedd.ShortUrl.Web
├── wwwroot
│ ├── robots.txt
│ └── favicon.ico
├── Models
│ ├── AccessTokenDataModel.cs
│ ├── AdminGetRequestModel.cs
│ ├── AdminGetResponseModel.cs
│ ├── AdminUpgradeResponseModel.cs
│ ├── AdminCreateRequestModel.cs
│ ├── AdminCreateResponseModel.cs
│ ├── ShortUrlTokenModel.cs
│ ├── ShortUrlLogEntryModel.cs
│ └── ShortUrlModel.cs
├── appsettings.Development.json
├── Views
│ └── Navigate
│ │ └── UrlNotFound.cshtml
├── Services
│ └── ManagedConfig.cs
├── Db
│ ├── INavigationDatabase.cs
│ ├── ShortUrlDbContext.cs
│ ├── CacheNavigationDatabase.cs
│ └── SqlNavigationDatabase.cs
├── appsettings.json
├── AutoMapperBootStrapper.cs
├── Program.cs
├── Tedd.ShortUrl.Web.csproj
├── Controllers
│ ├── NavigateController.cs
│ └── AdminController.cs
├── Utils
│ └── KeyGenerator.cs
├── Startup.cs
└── Migrations
│ ├── ShortUrlDbContextModelSnapshot.cs
│ ├── 20171122222323_InitialDb.Designer.cs
│ └── 20171122222323_InitialDb.cs
├── LICENSE
├── Tedd.ShortUrl.Test
├── Tedd.ShortUrl.Test.csproj
├── ShortUrlTextExtensions.cs
└── ShortUrlTest.cs
├── Tedd.ShortUrl.sln
├── README.md
└── .gitignore
/Tedd.ShortUrl.Web/wwwroot/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tedd/ShortUrl/HEAD/Tedd.ShortUrl.Web/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/AccessTokenDataModel.cs:
--------------------------------------------------------------------------------
1 | namespace Tedd.ShortUrl.Web.Models
2 | {
3 | public class AccessTokenDataModel
4 | {
5 | public bool Enabled { get; set; } = true;
6 | }
7 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/AdminGetRequestModel.cs:
--------------------------------------------------------------------------------
1 | namespace Tedd.ShortUrl.Web.Models
2 | {
3 | public class AdminGetRequestModel
4 | {
5 | public string AccessToken { get; set; }
6 | public string Key { get; set; }
7 |
8 | }
9 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "LogLevel": {
5 | "Default": "Debug",
6 | "System": "Information",
7 | "Microsoft": "Information"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/AdminGetResponseModel.cs:
--------------------------------------------------------------------------------
1 | namespace Tedd.ShortUrl.Web.Models
2 | {
3 | public class AdminGetResponseModel
4 | {
5 | public bool Success { get; set; }
6 | public ShortUrlModel ShortUrlModel { get; set; }
7 | }
8 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/AdminUpgradeResponseModel.cs:
--------------------------------------------------------------------------------
1 | namespace Tedd.ShortUrl.Web.Models
2 | {
3 | public class AdminUpgradeResponseModel
4 | {
5 | public bool Success { get; set; }
6 | public string ErrorMessage { get; set; }
7 | }
8 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Views/Navigate/UrlNotFound.cshtml:
--------------------------------------------------------------------------------
1 | @model string
2 |
3 |
4 |
5 |
6 |
7 | Url does not exist
8 |
9 |
10 |
11 |
Unknown url: @Model
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/AdminCreateRequestModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Tedd.ShortUrl.Web.Models
4 | {
5 | public class AdminCreateRequestModel {
6 | public string AccessToken { get; set; }
7 | public string Url { get; set; }
8 | public string MetaData { get; set; }
9 | public DateTime? ExpiresUtc { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/AdminCreateResponseModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Tedd.ShortUrl.Web.Models
4 | {
5 | public class AdminCreateResponseModel
6 | {
7 | public bool Success { get; set; } = true;
8 | public string Key { get; set; }
9 | public string Url { get; set; }
10 | public string ShortUrl { get; set; }
11 | public DateTime? ExpiresUtc { get; set; }
12 |
13 | }
14 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Services/ManagedConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Tedd.ShortUrl.Web.Models;
6 | using Microsoft.Extensions.Configuration;
7 |
8 | namespace Tedd.ShortUrl.Web.Services
9 | {
10 | public class ManagedConfig
11 | {
12 | public int KeyLength { get; set; }
13 | public string UpgradePassword { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Db/INavigationDatabase.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Threading.Tasks;
3 | using Tedd.ShortUrl.Web.Models;
4 |
5 | namespace Tedd.ShortUrl.Web.Db
6 | {
7 | public interface INavigationDatabase
8 | {
9 | Task GetData(string urlId);
10 | Task<(bool Success, string Key)> AddData(ShortUrlModel data);
11 | Task LogAccess(string urlId, IPAddress remoteIp);
12 | Task Upgrade();
13 | Task GetAccessToken(string accessToken);
14 | }
15 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "DefaultConnection": "Server=SQL1.localdev.tedd.no;Database=DevDb;persist security info=True;user id=dev;password=yoghurt;MultipleActiveResultSets=True;"
4 | },
5 | "Logging": {
6 | "IncludeScopes": false,
7 | "Debug": {
8 | "LogLevel": {
9 | "Default": "Warning"
10 | }
11 | },
12 | "Console": {
13 | "LogLevel": {
14 | "Default": "Warning"
15 | }
16 | }
17 | },
18 | "KeyLength": 8,
19 | "UpgradePassword": "$$UPGRADEPWD$$"
20 | }
21 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/ShortUrlTokenModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel.DataAnnotations;
3 | using Newtonsoft.Json;
4 |
5 | namespace Tedd.ShortUrl.Web.Models
6 | {
7 | public class ShortUrlTokenModel
8 | {
9 | [Key]
10 | public int Id { get; set; }
11 |
12 | [Required]
13 | [MaxLength(36)]
14 | public string CreatorAccessToken { get; set; }
15 | [JsonIgnore]
16 | public List ShortUrls { get; set; }
17 | [Required]
18 | public bool Enabled { get; set; }
19 | [Required]
20 | public bool Admin { get; set; }
21 | }
22 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/ShortUrlLogEntryModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel.DataAnnotations;
3 | using System.ComponentModel.DataAnnotations.Schema;
4 | using Newtonsoft.Json;
5 |
6 | namespace Tedd.ShortUrl.Web.Models
7 | {
8 | public class ShortUrlLogEntryModel
9 | {
10 | [Key]
11 | public long Id { get; set; }
12 | public long ShortUrlId { get; set; }
13 | [JsonIgnore]
14 | public ShortUrlModel ShortUrl { get; set; }
15 | [Required]
16 | [Column(TypeName = "smalldatetime")]
17 | public DateTime AccessTimeUtc { get; set; }
18 | [MaxLength(16)]
19 | public byte[] ClientIp { get; set; }
20 | }
21 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/AutoMapperBootStrapper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using AutoMapper;
6 | using Tedd.ShortUrl.Web.Models;
7 | using Microsoft.CodeAnalysis.Options;
8 |
9 | namespace Tedd.ShortUrl.Web
10 | {
11 | public static class AutoMapperBootstrapper
12 | {
13 | public static void Initialize()
14 | {
15 | Mapper.Initialize(cfg =>
16 | {
17 | cfg.CreateMap(MemberList.None);
18 | cfg.CreateMap(MemberList.None);
19 | });
20 |
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace Tedd.ShortUrl.Web
12 | {
13 | public class Program
14 | {
15 | public static void Main(string[] args)
16 | {
17 | BuildWebHost(args).Run();
18 | }
19 |
20 | public static IWebHost BuildWebHost(string[] args) =>
21 | WebHost.CreateDefaultBuilder(args)
22 | .UseContentRoot(Directory.GetCurrentDirectory())
23 | .UseIISIntegration()
24 | .UseStartup()
25 | .Build();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 tedd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Models/ShortUrlModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel.DataAnnotations;
4 | using System.ComponentModel.DataAnnotations.Schema;
5 | using Microsoft.EntityFrameworkCore.Metadata.Internal;
6 | using Newtonsoft.Json;
7 |
8 | namespace Tedd.ShortUrl.Web.Models
9 | {
10 | public class ShortUrlModel
11 |
12 | {
13 | [Key]
14 | public long Id { get; set; }
15 |
16 | [MaxLength(10)]
17 | public string Key { get; set; }
18 | [Required]
19 | [MaxLength(1000)]
20 | public string Url { get; set; }
21 | public string MetaData { get; set; }
22 |
23 | public int CreatorAccessTokenId { get; set; }
24 | [JsonIgnore]
25 | public ShortUrlTokenModel CreatorAccessToken { get; set; }
26 |
27 | [Required]
28 | [Column(TypeName = "smalldatetime")]
29 | public DateTime CreatedUtc { get; set; } = DateTime.Now;
30 | [Column(TypeName = "smalldatetime")]
31 | public DateTime? ExpiresUtc { get; set; }
32 |
33 |
34 | public List VisitLog { get; set; }
35 | }
36 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Tedd.ShortUrl.Web.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Always
28 |
29 |
30 | Always
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Test/Tedd.ShortUrl.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Controllers/NavigateController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text.RegularExpressions;
5 | using System.Threading.Tasks;
6 | using Tedd.ShortUrl.Web.Services;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Options;
10 | using Tedd.ShortUrl.Web.Db;
11 |
12 | namespace Tedd.ShortUrl.Web.Controllers
13 | {
14 | [Produces("text/html")]
15 | [Route("/")]
16 | public class NavigateController : Controller
17 | {
18 | private readonly INavigationDatabase _database;
19 | public static readonly Regex UrlKeyReplacementRegex = new Regex(@"\$KEY\$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
20 |
21 | public NavigateController(INavigationDatabase database)
22 | {
23 | _database = database;
24 | }
25 |
26 | [ResponseCache(Duration = 60 * 60 * 48, Location = ResponseCacheLocation.Client)]
27 | [HttpGet("/{key}")]
28 | public async Task NavigateTo(string key)
29 | {
30 | var data = await _database.GetData(key);
31 | if (data != null && (!data.ExpiresUtc.HasValue || DateTime.UtcNow < data.ExpiresUtc.Value))
32 | {
33 | await _database.LogAccess(key, Request.HttpContext.Connection.RemoteIpAddress);
34 | var url = UrlKeyReplacementRegex.Replace(data.Url, key);
35 | return Redirect(url);
36 | }
37 |
38 | return View("UrlNotFound", key);
39 |
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Db/ShortUrlDbContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.EntityFrameworkCore;
6 | using Tedd.ShortUrl.Web.Models;
7 |
8 | namespace Tedd.ShortUrl.Web.Db
9 | {
10 | public class ShortUrlDbContext : DbContext
11 | {
12 | public ShortUrlDbContext(DbContextOptions options) : base(options)
13 | {
14 |
15 | }
16 |
17 | public DbSet ShortUrl { get; set; }
18 | public DbSet ShortUrlVisitLog { get; set; }
19 | public DbSet ShortUrlAccessTokens { get; set; }
20 |
21 | protected override void OnModelCreating(ModelBuilder modelBuilder)
22 | {
23 | modelBuilder.Entity()
24 | .HasIndex(su => su.Key)
25 | .IsUnique();
26 |
27 | modelBuilder.Entity()
28 | .HasIndex(su => su.ShortUrlId);
29 |
30 | modelBuilder.Entity()
31 | .HasIndex(su => su.CreatorAccessToken)
32 | .IsUnique();
33 |
34 | modelBuilder.Entity()
35 | .HasOne(su => su.CreatorAccessToken)
36 | .WithMany(su => su.ShortUrls)
37 | .HasForeignKey(su => su.CreatorAccessTokenId);
38 |
39 | modelBuilder.Entity()
40 | .HasOne(su => su.ShortUrl)
41 | .WithMany(su => su.VisitLog)
42 | .HasForeignKey(su => su.ShortUrlId);
43 |
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Test/ShortUrlTextExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 | using Newtonsoft.Json;
6 | using Tedd.ShortUrl.Web.Models;
7 | using Xunit;
8 |
9 | namespace Tedd.ShortUrl.Test
10 | {
11 | public static class ShortUrlTextExtensions
12 | {
13 | public static async Task<(AdminCreateRequestModel request, AdminCreateResponseModel response)> CreateUrl(this HttpClient client, string testAccessToken)
14 | {
15 | // Arrange
16 |
17 |
18 | // Create URL
19 | var data = new AdminCreateRequestModel()
20 | {
21 | AccessToken = testAccessToken,
22 | ExpiresUtc = DateTime.Now.AddHours(1),
23 | MetaData = "123",
24 | Url = "https://www.google.com/?q=$key$"
25 | };
26 |
27 | var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
28 |
29 | var response1 = await client.PostAsync("/Admin/Create", content);
30 | response1.EnsureSuccessStatusCode();
31 | var response1String = await response1.Content.ReadAsStringAsync();
32 | var response1Object = JsonConvert.DeserializeObject(response1String);
33 | Assert.True(response1Object.Success);
34 | Assert.True(!string.IsNullOrEmpty(response1Object.Key));
35 | Assert.True(response1Object.Key.Length > 4);
36 | Assert.True(response1Object.Url.ToUpperInvariant().Contains(response1Object.Key.ToUpperInvariant()));
37 | return (request: data, response: response1Object);
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27004.2005
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tedd.ShortUrl.Web", "Tedd.ShortUrl.Web\Tedd.ShortUrl.Web.csproj", "{F3F9B80F-A31F-4545-9983-F8BB13541836}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tedd.ShortUrl.Test", "Tedd.ShortUrl.Test\Tedd.ShortUrl.Test.csproj", "{6C0FCCFC-C7E3-4309-939F-304B7E488395}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {F3F9B80F-A31F-4545-9983-F8BB13541836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {F3F9B80F-A31F-4545-9983-F8BB13541836}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {F3F9B80F-A31F-4545-9983-F8BB13541836}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {F3F9B80F-A31F-4545-9983-F8BB13541836}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {6C0FCCFC-C7E3-4309-939F-304B7E488395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {6C0FCCFC-C7E3-4309-939F-304B7E488395}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {6C0FCCFC-C7E3-4309-939F-304B7E488395}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {6C0FCCFC-C7E3-4309-939F-304B7E488395}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {E61D428A-586C-43A4-B66D-A1F100CD608D}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Utils/KeyGenerator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Tedd.ShortUrl.Web.Utils
7 | {
8 | public static class KeyGenerator
9 | {
10 | private const string ValidChars = "abcdefghjkmnprstwxz2345789"; // Letters and numbers that are not easily mixed with others when reading
11 | //private const string ValidChars = "ABCDEFHJKLMNPRSTUWXYZ2345789";
12 | private static readonly Dictionary ValidCharLookup = new Dictionary();
13 | private static readonly Tedd.MoreRandom.Random Rnd = new Tedd.MoreRandom.Random();
14 |
15 | static KeyGenerator()
16 | {
17 | // Set up a quick lookup dictionary for all valid characters
18 | foreach (var c in ValidChars.ToUpperInvariant())
19 | ValidCharLookup.Add((long)c, true);
20 | }
21 |
22 | public static string Generate(int length)
23 | {
24 | var ret = new char[length];
25 | for (var i = 0; i < length; i++)
26 | {
27 | int c;
28 | lock (Rnd)
29 | c = Rnd.Next(0, ValidChars.Length);
30 | ret[i] = ValidChars[c];
31 | }
32 | return new string(ret);
33 | }
34 |
35 | public static bool Validate(int maxLength, string key)
36 | {
37 | if (key.Length > maxLength)
38 | return false;
39 |
40 | foreach (var c in key.ToUpperInvariant())
41 | {
42 | if (!ValidCharLookup.ContainsKey((long)c))
43 | return false;
44 | }
45 | return true;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Db/CacheNavigationDatabase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.Options;
6 | using Tedd.ShortUrl.Web.Models;
7 | using Tedd.ShortUrl.Web.Services;
8 |
9 | namespace Tedd.ShortUrl.Web.Db
10 | {
11 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
12 | public class CacheNavigationDatabase : INavigationDatabase
13 | {
14 | public static Dictionary _cache = new Dictionary(StringComparer.OrdinalIgnoreCase);
15 | private readonly ManagedConfig _managedConfig;
16 | public CacheNavigationDatabase(IOptions managedConfig)
17 | {
18 | _managedConfig = managedConfig.Value;
19 | }
20 |
21 | public async Task GetData(string urlId)
22 | {
23 | ShortUrlModel ret = null;
24 | lock (_cache)
25 | _cache.TryGetValue(urlId, out ret);
26 | return ret;
27 | }
28 |
29 | public async Task LogAccess(string urlId, IPAddress connectionRemoteIpAddress)
30 | {
31 | ShortUrlModel ret = null;
32 | lock (_cache)
33 | _cache.TryGetValue(urlId, out ret);
34 | //if (!ret.FirstVisitUtc.HasValue)
35 | // ret.FirstVisitUtc = DateTime.Now;
36 |
37 | return true;
38 | }
39 |
40 | public Task Upgrade()
41 | {
42 | throw new NotImplementedException();
43 | }
44 |
45 | public Task GetAccessToken(string accessToken)
46 | {
47 | throw new NotImplementedException();
48 | }
49 |
50 | public async Task<(bool Success, string Key)> AddData(ShortUrlModel data)
51 | {
52 | for (int i = 0; i < 100; i++)
53 | {
54 | lock (_cache)
55 | {
56 | var key = Utils.KeyGenerator.Generate(_managedConfig.KeyLength);
57 | if (!_cache.ContainsKey(key))
58 | {
59 | data.Key = key;
60 | _cache.Add(key, data);
61 | return (Success: true, Key: key);
62 | }
63 | }
64 | }
65 | return (Success: false, Key: null);
66 | }
67 | }
68 | #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
69 | }
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED
2 | There is a newer version at: https://github.com/tedd/Tedd.ShortUrl
3 |
4 | .
5 | .
6 | .
7 | .
8 | .
9 | .
10 | .
11 | .
12 | .
13 | .
14 |
15 | # ShortUrl
16 | ASP.Net Core C¤ implementation of an URL shortener much like "tinyurl.com", "bitl.y", "goo.gl", "ow.ly", etc...
17 |
18 | ## Version 1
19 | This is version 1. It only has a WebAPI and no webpage.
20 | It comes with near 100% unit test coverage.
21 |
22 | ## Setup
23 | ### Webserver
24 | Solution is written in ASP.Net Core 2.0.
25 |
26 | 1. Set up solution by publishing it to a folder and pointing a webserver at it.
27 | For example: [Guide for setting up ASP.Net Core on IIS](https://docs.microsoft.com/en-us/aspnet/core/publishing/iis?tabs=aspnetcore2x)
28 | 2. Change `appsettings.json` to configure a database. You should also change the `UgradePassword`.
29 | 3. Visit `http://yoururl.com/Admin/Upgrade/$$UPGRADEPWD$$` where `$$UPGRADEPWD$$` is whatever you set UpgradePassword to. This will run EF Migrations and create your database.
30 | 4. If the token database is empty, a default token is created. Check `ShortUrlAccessToken` table.
31 | Tokens are required for services to be able to create shorturls on system.
32 |
33 | Note: Admin means it can initialize/upgrade database and request metadata for urls it did not create.
34 |
35 | ## WebAPI
36 | ### Create
37 | #### Request
38 | ```json
39 | POST /Admin/Create
40 | {
41 | "AccessToken": "$$TESTTOKEN$!!$CHANGEME$$",
42 | "Url": "http://mysite.com/$KEY$",
43 | "MetaData": "Put anything here, for example json",
44 | "Expires": "2018-01-01 15:30"
45 | }
46 | ```
47 |
48 | Note:
49 | * `Expires` is optional, and should be given in UTC.
50 | * `$KEY$` in the URL will be replaced with the shorturl key. You can use this to pass reference for target solution to go back into ShortUrl system to pick up metadata. This way metadata can be passed in URL without going via user browser.
51 |
52 | #### Response
53 | ```json
54 | {
55 | "success": true,
56 | "key": "abc123",
57 | "shortUrl": "http://myshorturlservice.com/abc123",
58 | "url": "http://www.stackoverflow.com/",
59 | "metaData": "Put anything here, for example json",
60 | "expiresUtc": "2018-01-01 15:30"
61 | }
62 | ```
63 |
64 | ### Get (metadata)
65 | ```json
66 | GET /Admin/Get/key?AccessToken=$$TESTTOKEN$!!$CHANGEME$$
67 | {
68 | "accessToken": "$$TESTTOKEN$!!$CHANGEME$$",
69 | "url": "http://www.stackoverflow.com/$KEY$",
70 | "metaData": "Put anything here, for example json",
71 | "expiresUtc": "2018-01-01 15:30"
72 | }
73 | ```
74 |
75 | ### Initialize database (Migrations)
76 | GET /Admin/Upgrade/$$UPGRADEPWD$$
77 |
78 | ### Visit URL
79 | Simply visit ShortUrl returned from Create operation to be forwarded to Url.
80 |
81 | # HTTPS
82 | You may want to add automatic forwarding to HTTPS, maybe even with support for letsencrypt:
83 |
84 | https://blog.tedd.no/2017/05/09/iis-redirect-http-to-https-but-allow-lets-encrypt/
85 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Db/SqlNavigationDatabase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Net;
4 | using System.Threading.Tasks;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.Extensions.Options;
7 | using Tedd.ShortUrl.Web.Models;
8 | using Tedd.ShortUrl.Web.Services;
9 |
10 | namespace Tedd.ShortUrl.Web.Db
11 | {
12 | public class SqlNavigationDatabase : INavigationDatabase
13 | {
14 | private readonly ShortUrlDbContext _shortUrlDbContext;
15 | private readonly ManagedConfig _managedConfig;
16 |
17 | public SqlNavigationDatabase(IOptions managedConfig, ShortUrlDbContext shortUrlDbContext)
18 | {
19 | _shortUrlDbContext = shortUrlDbContext;
20 | _managedConfig = managedConfig.Value;
21 | }
22 |
23 | public async Task GetData(string urlId)
24 | {
25 | return await _shortUrlDbContext.ShortUrl.Where(su => su.Key == urlId).FirstOrDefaultAsync();
26 | }
27 |
28 | public async Task<(bool Success, string Key)> AddData(ShortUrlModel data)
29 | {
30 | // Try to find an available random key
31 | for (int i = 0; i < 100; i++)
32 | {
33 | var key = Utils.KeyGenerator.Generate(_managedConfig.KeyLength);
34 | var existing = await GetData(key);
35 | if (existing == null)
36 | {
37 | data.Key = key;
38 | // Save to database
39 | _shortUrlDbContext.Add(data);
40 | await _shortUrlDbContext.SaveChangesAsync();
41 | // No further processing
42 | return (Success: true, Key: key);
43 | }
44 | }
45 | return (Success: false, Key: null);
46 | }
47 |
48 | public async Task LogAccess(string urlId, IPAddress remoteIp)
49 | {
50 | var data = await _shortUrlDbContext.ShortUrl.Where(su => su.Key == urlId).FirstOrDefaultAsync();
51 | if (data == null)
52 | return false;
53 |
54 | //if (!data.FirstVisitUtc.HasValue)
55 | // data.FirstVisitUtc = DateTime.Now;
56 |
57 | var log = new ShortUrlLogEntryModel();
58 | log.ShortUrlId = data.Id;
59 | log.AccessTimeUtc = DateTime.UtcNow;
60 | log.ClientIp = remoteIp?.GetAddressBytes();
61 |
62 | await _shortUrlDbContext.ShortUrlVisitLog.AddAsync(log);
63 |
64 | await _shortUrlDbContext.SaveChangesAsync();
65 |
66 | return true;
67 | }
68 |
69 | public async Task Upgrade()
70 | {
71 | await _shortUrlDbContext.Database.MigrateAsync();
72 |
73 | // We add a test user if database is empty.
74 | if (_shortUrlDbContext.ShortUrlAccessTokens.ToList().Count == 0)
75 | {
76 | _shortUrlDbContext.ShortUrlAccessTokens.Add(new ShortUrlTokenModel()
77 | {
78 | CreatorAccessToken = "$$TESTTOKEN$!!$CHANGEME$$",
79 | Enabled = true,
80 | Admin = false
81 | });
82 | await _shortUrlDbContext.SaveChangesAsync();
83 | }
84 | }
85 |
86 | public async Task GetAccessToken(string accessToken)
87 | {
88 | // TODO: This can be cached for a few minutes...?
89 | return await _shortUrlDbContext.ShortUrlAccessTokens.Where(at => at.CreatorAccessToken == accessToken && at.Enabled).FirstOrDefaultAsync();
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Tedd.ShortUrl.Web.Db;
6 | using Tedd.ShortUrl.Web.Services;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.Routing;
10 | using Microsoft.EntityFrameworkCore;
11 | using Microsoft.Extensions.Configuration;
12 | using Microsoft.Extensions.DependencyInjection;
13 | using Microsoft.Extensions.Logging;
14 | using Microsoft.Extensions.Options;
15 |
16 | namespace Tedd.ShortUrl.Web
17 | {
18 | public class Startup
19 | {
20 | public IConfiguration Configuration { get; }
21 | //public Startup(IConfiguration configuration)
22 | //{
23 | // Configuration = configuration;
24 | //}
25 | public Startup(IHostingEnvironment env)
26 | {
27 |
28 | var builder = new ConfigurationBuilder()
29 | .SetBasePath(env.ContentRootPath)
30 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
31 | Configuration = builder.Build();
32 |
33 | AutoMapperBootstrapper.Initialize();
34 | }
35 |
36 |
37 | // This method gets called by the runtime. Use this method to add services to the container.
38 | public void ConfigureServices(IServiceCollection services)
39 | {
40 | services.AddMemoryCache();
41 | services.AddMvc();
42 | services.AddResponseCaching();
43 |
44 | var connection = Configuration.GetConnectionString("DefaultConnection");
45 | services.AddDbContext(options => options.UseSqlServer(connection));
46 |
47 | //services.AddSingleton();
48 | services.AddScoped();
49 |
50 | //services.AddSingleton();
51 | // Register the IConfiguration instance which MyOptions binds against.
52 | services.Configure(Configuration);
53 |
54 |
55 | }
56 |
57 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
58 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
59 | {
60 | if (env.IsDevelopment())
61 | {
62 | app.UseDeveloperExceptionPage();
63 | app.UseDatabaseErrorPage();
64 | app.UseBrowserLink();
65 | }
66 |
67 | //app.UseMvc();
68 | app.UseStaticFiles();
69 | app.UseMvc(
70 | routes =>
71 | {
72 | routes.MapRoute("default", "{controller=Home}/{action=Index}");
73 |
74 | routes.MapRoute(
75 | "NavigateTo",
76 | "{actionURL}",
77 | new { controller = "Navigate", action = "NavigateTo" }
78 | );
79 | //routes.MapRoute(name: "createRoute",
80 | // template: "{*url}",
81 | // defaults: new { controller = "Navigate", action = "NavigateTo" });
82 | //routes.MapRoute(name: "navigateRoute",
83 | // template: "{*url}",
84 | // defaults: new { controller = "Navigate", action = "NavigateTo" });
85 | });
86 | app.UseResponseCaching();
87 |
88 |
89 | loggerFactory.AddConsole(Configuration.GetSection("Logging"));
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Controllers/AdminController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using AutoMapper;
6 | using Tedd.ShortUrl.Web.Models;
7 | using Tedd.ShortUrl.Web.Services;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 | using Microsoft.AspNetCore.Rewrite.Internal.UrlActions;
11 | using Microsoft.Extensions.Options;
12 | using Tedd.ShortUrl.Web.Db;
13 |
14 | namespace Tedd.ShortUrl.Web.Controllers
15 | {
16 | [Produces("application/json")]
17 | [Route("Admin")]
18 | public class AdminController : Controller
19 | {
20 | private readonly INavigationDatabase _navigationDatabase;
21 | private readonly ManagedConfig _managedConfig;
22 |
23 | public AdminController(IOptions managedConfig, INavigationDatabase navigationDatabase)
24 | {
25 | _navigationDatabase = navigationDatabase;
26 | _managedConfig = managedConfig.Value;
27 | }
28 |
29 | [HttpPost("Create")]
30 | public async Task Create([FromBody] AdminCreateRequestModel model)
31 | {
32 | var accessToken = await _navigationDatabase.GetAccessToken(model.AccessToken);
33 | if (accessToken == null || !accessToken.Enabled)
34 | return new AdminCreateResponseModel() { Success = false };
35 |
36 |
37 | // Create a URL forward entry
38 | var data = Mapper.Map(model);
39 | data.CreatorAccessTokenId = accessToken.Id;
40 | var result = await _navigationDatabase.AddData(data);
41 | var ret = Mapper.Map(data);
42 | ret.Url = NavigateController.UrlKeyReplacementRegex.Replace(ret.Url, ret.Key);
43 | ret.ShortUrl = new Uri(Request.Scheme + "://" + Request.Host.Value + Request.PathBase + "/" + ret.Key).ToString();
44 | //ret.Key = result.Key;
45 | ret.Success = result.Success;
46 |
47 | return ret;
48 | }
49 |
50 | [HttpGet("Get/{key}")]
51 | public async Task Get(AdminGetRequestModel model)
52 | {
53 | var accessToken = await _navigationDatabase.GetAccessToken(model.AccessToken);
54 | if (accessToken == null || !accessToken.Enabled)
55 | return new AdminGetResponseModel() { Success = false };
56 |
57 | // Get data on existing URL forward entry
58 | var result = await _navigationDatabase.GetData(model.Key);
59 |
60 | var ret = new AdminGetResponseModel()
61 | {
62 | Success = true,
63 | ShortUrlModel = result
64 | };
65 |
66 | return ret;
67 | }
68 |
69 | [HttpGet("Upgrade/{key}")]
70 | public async Task Upgrade(string key)
71 | {
72 | // Either UpgradePassword from config matches
73 | if (_managedConfig.UpgradePassword != key)
74 | {
75 | // Or access token from SQL must have admin rights
76 | var accessToken = await _navigationDatabase.GetAccessToken(key);
77 | if (accessToken == null || !accessToken.Enabled)
78 | return new AdminUpgradeResponseModel() { Success = false, ErrorMessage = "Access denied: Invalid key" };
79 |
80 | if (!accessToken.Admin)
81 | return new AdminUpgradeResponseModel() { Success = false, ErrorMessage = "Access denied: Not admin" };
82 | }
83 |
84 | await _navigationDatabase.Upgrade();
85 |
86 | return new AdminUpgradeResponseModel() { Success = true, ErrorMessage = null }; ;
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Migrations/ShortUrlDbContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Infrastructure;
4 | using Microsoft.EntityFrameworkCore.Metadata;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage;
7 | using Microsoft.EntityFrameworkCore.Storage.Internal;
8 | using System;
9 | using Tedd.ShortUrl.Web.Db;
10 |
11 | namespace Tedd.ShortUrl.Web.Migrations
12 | {
13 | [DbContext(typeof(ShortUrlDbContext))]
14 | partial class ShortUrlDbContextModelSnapshot : ModelSnapshot
15 | {
16 | protected override void BuildModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
21 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
22 |
23 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlLogEntryModel", b =>
24 | {
25 | b.Property("Id")
26 | .ValueGeneratedOnAdd();
27 |
28 | b.Property("AccessTimeUtc")
29 | .HasColumnType("smalldatetime");
30 |
31 | b.Property("ClientIp")
32 | .HasMaxLength(16);
33 |
34 | b.Property("ShortUrlId");
35 |
36 | b.HasKey("Id");
37 |
38 | b.HasIndex("ShortUrlId");
39 |
40 | b.ToTable("ShortUrlVisitLog");
41 | });
42 |
43 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlModel", b =>
44 | {
45 | b.Property("Id")
46 | .ValueGeneratedOnAdd();
47 |
48 | b.Property("CreatedUtc")
49 | .HasColumnType("smalldatetime");
50 |
51 | b.Property("CreatorAccessTokenId");
52 |
53 | b.Property("ExpiresUtc")
54 | .HasColumnType("smalldatetime");
55 |
56 | b.Property("Key")
57 | .HasMaxLength(10);
58 |
59 | b.Property("MetaData");
60 |
61 | b.Property("Url")
62 | .IsRequired()
63 | .HasMaxLength(1000);
64 |
65 | b.HasKey("Id");
66 |
67 | b.HasIndex("CreatorAccessTokenId");
68 |
69 | b.HasIndex("Key")
70 | .IsUnique()
71 | .HasFilter("[Key] IS NOT NULL");
72 |
73 | b.ToTable("ShortUrl");
74 | });
75 |
76 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlTokenModel", b =>
77 | {
78 | b.Property("Id")
79 | .ValueGeneratedOnAdd();
80 |
81 | b.Property("Admin");
82 |
83 | b.Property("CreatorAccessToken")
84 | .IsRequired()
85 | .HasMaxLength(36);
86 |
87 | b.Property("Enabled");
88 |
89 | b.HasKey("Id");
90 |
91 | b.HasIndex("CreatorAccessToken")
92 | .IsUnique();
93 |
94 | b.ToTable("ShortUrlAccessTokens");
95 | });
96 |
97 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlLogEntryModel", b =>
98 | {
99 | b.HasOne("Tedd.ShortUrl.Web.Models.ShortUrlModel", "ShortUrl")
100 | .WithMany("VisitLog")
101 | .HasForeignKey("ShortUrlId")
102 | .OnDelete(DeleteBehavior.Cascade);
103 | });
104 |
105 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlModel", b =>
106 | {
107 | b.HasOne("Tedd.ShortUrl.Web.Models.ShortUrlTokenModel", "CreatorAccessToken")
108 | .WithMany("ShortUrls")
109 | .HasForeignKey("CreatorAccessTokenId")
110 | .OnDelete(DeleteBehavior.Cascade);
111 | });
112 | #pragma warning restore 612, 618
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Migrations/20171122222323_InitialDb.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Infrastructure;
4 | using Microsoft.EntityFrameworkCore.Metadata;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage;
7 | using Microsoft.EntityFrameworkCore.Storage.Internal;
8 | using System;
9 | using Tedd.ShortUrl.Web.Db;
10 |
11 | namespace Tedd.ShortUrl.Web.Migrations
12 | {
13 | [DbContext(typeof(ShortUrlDbContext))]
14 | [Migration("20171122222323_InitialDb")]
15 | partial class InitialDb
16 | {
17 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
18 | {
19 | #pragma warning disable 612, 618
20 | modelBuilder
21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
23 |
24 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlLogEntryModel", b =>
25 | {
26 | b.Property("Id")
27 | .ValueGeneratedOnAdd();
28 |
29 | b.Property("AccessTimeUtc")
30 | .HasColumnType("smalldatetime");
31 |
32 | b.Property("ClientIp")
33 | .HasMaxLength(16);
34 |
35 | b.Property("ShortUrlId");
36 |
37 | b.HasKey("Id");
38 |
39 | b.HasIndex("ShortUrlId");
40 |
41 | b.ToTable("ShortUrlVisitLog");
42 | });
43 |
44 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlModel", b =>
45 | {
46 | b.Property("Id")
47 | .ValueGeneratedOnAdd();
48 |
49 | b.Property("CreatedUtc")
50 | .HasColumnType("smalldatetime");
51 |
52 | b.Property("CreatorAccessTokenId");
53 |
54 | b.Property("ExpiresUtc")
55 | .HasColumnType("smalldatetime");
56 |
57 | b.Property("Key")
58 | .HasMaxLength(10);
59 |
60 | b.Property("MetaData");
61 |
62 | b.Property("Url")
63 | .IsRequired()
64 | .HasMaxLength(1000);
65 |
66 | b.HasKey("Id");
67 |
68 | b.HasIndex("CreatorAccessTokenId");
69 |
70 | b.HasIndex("Key")
71 | .IsUnique()
72 | .HasFilter("[Key] IS NOT NULL");
73 |
74 | b.ToTable("ShortUrl");
75 | });
76 |
77 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlTokenModel", b =>
78 | {
79 | b.Property("Id")
80 | .ValueGeneratedOnAdd();
81 |
82 | b.Property("Admin");
83 |
84 | b.Property("CreatorAccessToken")
85 | .IsRequired()
86 | .HasMaxLength(36);
87 |
88 | b.Property("Enabled");
89 |
90 | b.HasKey("Id");
91 |
92 | b.HasIndex("CreatorAccessToken")
93 | .IsUnique();
94 |
95 | b.ToTable("ShortUrlAccessTokens");
96 | });
97 |
98 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlLogEntryModel", b =>
99 | {
100 | b.HasOne("Tedd.ShortUrl.Web.Models.ShortUrlModel", "ShortUrl")
101 | .WithMany("VisitLog")
102 | .HasForeignKey("ShortUrlId")
103 | .OnDelete(DeleteBehavior.Cascade);
104 | });
105 |
106 | modelBuilder.Entity("Tedd.ShortUrl.Web.Models.ShortUrlModel", b =>
107 | {
108 | b.HasOne("Tedd.ShortUrl.Web.Models.ShortUrlTokenModel", "CreatorAccessToken")
109 | .WithMany("ShortUrls")
110 | .HasForeignKey("CreatorAccessTokenId")
111 | .OnDelete(DeleteBehavior.Cascade);
112 | });
113 | #pragma warning restore 612, 618
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Web/Migrations/20171122222323_InitialDb.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Metadata;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | namespace Tedd.ShortUrl.Web.Migrations
7 | {
8 | public partial class InitialDb : Migration
9 | {
10 | protected override void Up(MigrationBuilder migrationBuilder)
11 | {
12 | migrationBuilder.CreateTable(
13 | name: "ShortUrlAccessTokens",
14 | columns: table => new
15 | {
16 | Id = table.Column(type: "int", nullable: false)
17 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
18 | Admin = table.Column(type: "bit", nullable: false),
19 | CreatorAccessToken = table.Column(type: "nvarchar(36)", maxLength: 36, nullable: false),
20 | Enabled = table.Column(type: "bit", nullable: false)
21 | },
22 | constraints: table =>
23 | {
24 | table.PrimaryKey("PK_ShortUrlAccessTokens", x => x.Id);
25 | });
26 |
27 | migrationBuilder.CreateTable(
28 | name: "ShortUrl",
29 | columns: table => new
30 | {
31 | Id = table.Column(type: "bigint", nullable: false)
32 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
33 | CreatedUtc = table.Column(type: "smalldatetime", nullable: false),
34 | CreatorAccessTokenId = table.Column(type: "int", nullable: false),
35 | ExpiresUtc = table.Column(type: "smalldatetime", nullable: true),
36 | Key = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: true),
37 | MetaData = table.Column(type: "nvarchar(max)", nullable: true),
38 | Url = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: false)
39 | },
40 | constraints: table =>
41 | {
42 | table.PrimaryKey("PK_ShortUrl", x => x.Id);
43 | table.ForeignKey(
44 | name: "FK_ShortUrl_ShortUrlAccessTokens_CreatorAccessTokenId",
45 | column: x => x.CreatorAccessTokenId,
46 | principalTable: "ShortUrlAccessTokens",
47 | principalColumn: "Id",
48 | onDelete: ReferentialAction.Cascade);
49 | });
50 |
51 | migrationBuilder.CreateTable(
52 | name: "ShortUrlVisitLog",
53 | columns: table => new
54 | {
55 | Id = table.Column(type: "bigint", nullable: false)
56 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
57 | AccessTimeUtc = table.Column(type: "smalldatetime", nullable: false),
58 | ClientIp = table.Column(type: "varbinary(16)", maxLength: 16, nullable: true),
59 | ShortUrlId = table.Column(type: "bigint", nullable: false)
60 | },
61 | constraints: table =>
62 | {
63 | table.PrimaryKey("PK_ShortUrlVisitLog", x => x.Id);
64 | table.ForeignKey(
65 | name: "FK_ShortUrlVisitLog_ShortUrl_ShortUrlId",
66 | column: x => x.ShortUrlId,
67 | principalTable: "ShortUrl",
68 | principalColumn: "Id",
69 | onDelete: ReferentialAction.Cascade);
70 | });
71 |
72 | migrationBuilder.CreateIndex(
73 | name: "IX_ShortUrl_CreatorAccessTokenId",
74 | table: "ShortUrl",
75 | column: "CreatorAccessTokenId");
76 |
77 | migrationBuilder.CreateIndex(
78 | name: "IX_ShortUrl_Key",
79 | table: "ShortUrl",
80 | column: "Key",
81 | unique: true,
82 | filter: "[Key] IS NOT NULL");
83 |
84 | migrationBuilder.CreateIndex(
85 | name: "IX_ShortUrlAccessTokens_CreatorAccessToken",
86 | table: "ShortUrlAccessTokens",
87 | column: "CreatorAccessToken",
88 | unique: true);
89 |
90 | migrationBuilder.CreateIndex(
91 | name: "IX_ShortUrlVisitLog_ShortUrlId",
92 | table: "ShortUrlVisitLog",
93 | column: "ShortUrlId");
94 | }
95 |
96 | protected override void Down(MigrationBuilder migrationBuilder)
97 | {
98 | migrationBuilder.DropTable(
99 | name: "ShortUrlVisitLog");
100 |
101 | migrationBuilder.DropTable(
102 | name: "ShortUrl");
103 |
104 | migrationBuilder.DropTable(
105 | name: "ShortUrlAccessTokens");
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
--------------------------------------------------------------------------------
/Tedd.ShortUrl.Test/ShortUrlTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using Tedd.ShortUrl.Web;
9 | using Tedd.ShortUrl.Web.Controllers;
10 | using Tedd.ShortUrl.Web.Models;
11 | using Microsoft.AspNetCore.Hosting;
12 | using Microsoft.AspNetCore.TestHost;
13 | using Newtonsoft.Json;
14 | using Xunit;
15 | using Xunit.Abstractions;
16 |
17 | namespace Tedd.ShortUrl.Test
18 | {
19 | public class ShortUrlTest
20 | {
21 | private readonly ITestOutputHelper _output;
22 | private string testAccessToken = "$$TESTTOKEN$!!$CHANGEME$$"; // This one must be created in database!
23 | private TestServer _server;
24 | private HttpClient _client;
25 |
26 | public ShortUrlTest(ITestOutputHelper output)
27 | {
28 | _output = output;
29 | // Arrange
30 | _server = new TestServer(new WebHostBuilder().UseStartup());
31 | _client = _server.CreateClient();
32 | }
33 |
34 |
35 |
36 |
37 | [Fact]
38 | public async Task CreateAndVisit()
39 | {
40 | var (data, response1Object) = await _client.CreateUrl(testAccessToken);
41 |
42 | // Test URL
43 | var response2 = await _client.GetAsync($"/{response1Object.Key}");
44 | Assert.True(response2.StatusCode == HttpStatusCode.Redirect);
45 | Assert.True(response2.Headers.Location.AbsoluteUri == new Uri(data.Url.Replace("$key$", response1Object.Key)).AbsoluteUri);
46 | }
47 |
48 | [Fact]
49 | public async Task CreateAndGet()
50 | {
51 | var (data, response1Object) = await _client.CreateUrl(testAccessToken);
52 |
53 | // Test URL
54 |
55 | var response2 = await _client.GetAsync($"/Admin/Get/{response1Object.Key}?AccessToken=" + data.AccessToken);
56 | response2.EnsureSuccessStatusCode();
57 | var response2String = await response2.Content.ReadAsStringAsync();
58 | var response2Object = JsonConvert.DeserializeObject(response2String);
59 | Assert.True(response2Object.Success);
60 | // Assert.True(response2Object.ShortUrlModel.);
61 | }
62 |
63 | private async Task CreateAccessDenied()
64 | {
65 | // Create URL
66 | var data = new AdminCreateRequestModel()
67 | {
68 | AccessToken = "INVALID",
69 | ExpiresUtc = DateTime.Now.AddHours(1),
70 | MetaData = "123",
71 | Url = "https://www.google.com"
72 | };
73 |
74 | var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
75 |
76 | var response1 = await _client.PostAsync("/Admin/Create", content);
77 | response1.EnsureSuccessStatusCode();
78 | var response1String = await response1.Content.ReadAsStringAsync();
79 | var response1Object = JsonConvert.DeserializeObject(response1String);
80 | Assert.False(response1Object.Success);
81 | Assert.True(string.IsNullOrEmpty(response1Object.Key));
82 | Assert.True(string.IsNullOrEmpty(response1Object.Key));
83 |
84 | }
85 |
86 |
87 | [Fact]
88 | public async Task GetNotExist()
89 | {
90 | var response2 = await _client.GetAsync($"/Admin/Get/FAKE?AccessToken=" + testAccessToken);
91 | response2.EnsureSuccessStatusCode();
92 | var response2String = await response2.Content.ReadAsStringAsync();
93 | var response2Object = JsonConvert.DeserializeObject(response2String);
94 | Assert.True(response2Object.Success);
95 | // Assert.True(response2Object.ShortUrlModel.);
96 | }
97 |
98 | [Fact]
99 | public async Task GetAccessDenied()
100 | {
101 | var (data, response1Object) = await _client.CreateUrl(testAccessToken);
102 |
103 | var response2 = await _client.GetAsync($"/Admin/Get/{response1Object.Key}?AccessToken=INVALID");
104 | response2.EnsureSuccessStatusCode();
105 | var response2String = await response2.Content.ReadAsStringAsync();
106 | var response2Object = JsonConvert.DeserializeObject(response2String);
107 | Assert.False(response2Object.Success);
108 | // Assert.True(response2Object.ShortUrlModel.);
109 | }
110 |
111 |
112 |
113 | [Fact]
114 | public async Task KeyNotFound()
115 | {
116 | var falseKey = "ABC123";
117 |
118 | // Test URL
119 | var response2 = await _client.GetAsync($"/{falseKey}");
120 | Assert.True(response2.StatusCode == HttpStatusCode.OK);
121 | var str = await response2.Content.ReadAsStringAsync();
122 | var strU = str.ToUpper();
123 | Assert.Contains("> 1;
145 | var cc2 = createCount - cc1;
146 | stopwatch.Start();
147 | Parallel.For(0, cc1, () =>
148 | {
149 | var server = new TestServer(new WebHostBuilder().UseStartup());
150 | var client = server.CreateClient();
151 | return (Server: server, Client: client);
152 | },
153 | (i, pls, sc) =>
154 | //for (var i = 0; i< cc1;i++)
155 | {
156 | created[i] = sc.Client.CreateUrl(testAccessToken).Result;
157 | return sc;
158 | }, (sc) =>
159 | {
160 | sc.Client.Dispose();
161 | sc.Server.Dispose();
162 | });
163 | var cc1TimeUsed = stopwatch.ElapsedMilliseconds;
164 | _output.WriteLine($"Round 1: Create time used for {cc1} urls: {(double)cc1TimeUsed} ms total, {(double)cc1TimeUsed / (double)cc1} ms per create");
165 | stopwatch.Restart();
166 |
167 | Parallel.For(cc1, createCount, () =>
168 | {
169 | var server = new TestServer(new WebHostBuilder().UseStartup());
170 | var client = server.CreateClient();
171 | return (Server: server, Client: client);
172 | },
173 | (i, pls, sc) =>
174 | //for (var i = 0; i< cc1;i++)
175 | {
176 | created[i] = sc.Client.CreateUrl(testAccessToken).Result;
177 | return sc;
178 | }, (sc) =>
179 | {
180 | sc.Client.Dispose();
181 | sc.Server.Dispose();
182 | });
183 | var cc2TimeUsed = stopwatch.ElapsedMilliseconds;
184 | _output.WriteLine($"Round 2: Create time used for {cc2} urls: {(double)cc2TimeUsed} ms total, {(double)cc2TimeUsed / (double)cc2} ms per create");
185 |
186 | var diffMs = Math.Abs(((double)cc1TimeUsed / (double)cc1) - ((double)cc2TimeUsed / (double)cc2));
187 | _output.WriteLine($"Differential time between first and second batch: {diffMs} ms");
188 | Assert.True(diffMs < 10D);
189 | //Assert.True(createTimeUsed < 1000);
190 |
191 | stopwatch.Restart();
192 |
193 | // Test URL
194 | var rnd = new Random();
195 |
196 | //Parallel.For((long) 0, visitCount, async (i) =>
197 | for (var i = 0; i < visitCount; i++)
198 | {
199 | var c = rnd.Next(0, createCount);
200 | var data = created[c].request;
201 | var response1Object = created[c].response;
202 | var response2 = await _client.GetAsync($"/{response1Object.Key}");
203 | Assert.True(response2.StatusCode == HttpStatusCode.Redirect);
204 | Assert.True(response2.Headers.Location.AbsoluteUri == new Uri(data.Url).AbsoluteUri);
205 | }//);
206 | var visitTimeUsed = stopwatch.ElapsedMilliseconds;
207 | _output.WriteLine($"Visit time used for {visitCount} visits: {visitTimeUsed} ms");
208 |
209 | //Assert.True(visitTimeUsed < 1000);
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------