Archived
Private
Public Access
1
0
This commit is contained in:
2022-12-18 13:30:02 +01:00
commit 0e94ffa3c6
85 changed files with 26673 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using Microsoft.Extensions.Options;
using ProjectManager.Backend.Options;
namespace ProjectManager.Backend.Apis;
public interface IDockerApi {
public Task<string> CreateContainer(string image, int port, string hostVolumePath, string name);
public Task StartContainer(string containerId);
public Task StopContainer(string containerId);
public Task DeleteContainer(string containerId);
public Task<bool> IsContainerStarted(string containerId);
}
public sealed class DockerApi : IDockerApi {
private readonly DockerClient _client;
public DockerApi(IOptions<GeneralOptions> options) {
_client = new DockerClientConfiguration(new Uri(options.Value.DockerPath)).CreateClient();
}
public async Task<string> CreateContainer(string image, int port, string hostVolumePath, string name) {
await _client.Images.CreateImageAsync(new() {
FromImage = image
}, null, new Progress<JSONMessage>());
var container = await _client.Containers.CreateContainerAsync(new CreateContainerParameters {
Image = image,
ExposedPorts = new Dictionary<string, EmptyStruct> {
{ "8090/tcp", default }
},
HostConfig = new HostConfig {
PortBindings = new Dictionary<string, IList<PortBinding>> {
{ "8090/tcp", new List<PortBinding> { new() { HostPort = port.ToString() } } }
},
Binds = new List<string> {
$"{hostVolumePath}:/pb_data"
}
},
Name = name
});
return container.ID;
}
public async Task StartContainer(string containerId) {
await _client.Containers.StartContainerAsync(containerId, new());
}
public async Task StopContainer(string containerId) {
await _client.Containers.StopContainerAsync(containerId, new());
}
public async Task DeleteContainer(string containerId) {
await _client.Containers.RemoveContainerAsync(containerId, new());
}
public async Task<bool> IsContainerStarted(string containerId) {
var containers = await _client.Containers.ListContainersAsync(new());
var container = containers.SingleOrDefault(c => c.ID == containerId);
return container != null;
}
}

View File

@@ -0,0 +1,89 @@
using Microsoft.Extensions.Options;
using ProjectManager.Backend.Entities;
using ProjectManager.Backend.Options;
namespace ProjectManager.Backend.Apis;
public interface IProjectApi {
public Project[] GetProjects(string userId);
public Project GetProject(string projectId);
public Task<string> AddProject(string name, string ownerId);
public bool EditProject(string projectId, string name);
public Task DeleteProject(string projectId);
}
public class ProjectApi : IProjectApi {
private readonly DatabaseContext _context;
private readonly GeneralOptions _options;
private readonly IDockerApi _docker;
private readonly IProxyApi _proxy;
public ProjectApi(DatabaseContext context, IOptions<GeneralOptions> options, IDockerApi docker, IProxyApi proxy) {
_context = context;
_options = options.Value;
_docker = docker;
_proxy = proxy;
}
public Project[] GetProjects(string userId) {
return _context.Projects.Where(project => project.OwnerId == userId).ToArray();
}
public Project GetProject(string projectId) {
return _context.Projects.SingleOrDefault(project => project.ProjectId == projectId);
}
public async Task<string> AddProject(string name, string ownerId) {
if (name.Length > 255) return null;
// Get available port
var split = _options.PortRange.Split('-');
var portRange = Enumerable.Range(int.Parse(split[0]), int.Parse(split[1]));
var usedPorts = _context.Projects.Select(p => p.Port).ToArray();
var port = portRange.FirstOrDefault(port => !usedPorts.Contains(port));
if (port == 0) return null;
var project = new Project {
ProjectId = Guid.NewGuid().ToString(),
OwnerId = ownerId,
Name = name,
Port = port
};
var container = await _docker.CreateContainer($"ghcr.io/muchobien/pocketbase:{_options.PocketBaseVersion}", port, _options.Root + project.ProjectId, $"{project.Name}_{project.ProjectId}");
await _docker.StartContainer(container);
project.ContainerName = container;
var (proxyId, certificateId) = await _proxy.AddLocation(project.ProjectId, project.Port);
project.ProxyId = proxyId;
project.CertificateId = certificateId;
_context.Projects.Add(project);
await _context.SaveChangesAsync();
return project.ProjectId;
}
public bool EditProject(string projectId, string name) {
if (name.Length > 255) return false;
var project = GetProject(projectId);
if (project == null) return false;
project.Name = name;
_context.Projects.Update(project);
_context.SaveChanges();
return true;
}
public async Task DeleteProject(string projectId) {
if (!_context.Projects.Any(project => project.ProjectId == projectId)) return;
var project = _context.Projects.Single(project => project.ProjectId == projectId);
await _docker.StopContainer(project.ContainerName);
await _docker.DeleteContainer(project.ContainerName);
await _proxy.RemoveLocation(project.ProxyId, project.CertificateId);
_context.Projects.Remove(project);
await _context.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,84 @@
using System.Dynamic;
using System.Text.Json;
using Microsoft.Extensions.Options;
using ProjectManager.Backend.Options;
namespace ProjectManager.Backend.Apis;
public interface IProxyApi {
public Task<(int, int)> AddLocation(string projectId, int port);
public Task RemoveLocation(int proxyId, int certificateId);
}
public sealed class ProxyApi : IProxyApi {
private readonly HttpClient _client;
private readonly ProxyOptions _options;
public ProxyApi(IOptions<ProxyOptions> options) {
_client = new HttpClient();
_options = options.Value;
}
private async Task Login() {
var result = await _client.PostAsJsonAsync(_options.Url + "/api/tokens",
new ProxyAuth(_options.Email, _options.Password), JsonSerializerOptions.Default);
var response = await result.Content.ReadFromJsonAsync<TokenResponse>();
_client.DefaultRequestHeaders.Clear();
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {response?.token}");
}
public async Task<(int, int)> AddLocation(string projectId, int port) {
if (!_options.Enable) return (-1, -1);
await Login();
var result = await _client.PostAsJsonAsync(_options.Url + "/api/nginx/proxy-hosts",
new CreateData($"{projectId}.{_options.Domain}", _options.Host, port, _options.Email));
dynamic data = await result.Content.ReadFromJsonAsync<ExpandoObject>();
if (data == null) return (-1, -1);
int id = Convert.ToInt32($"{data.id}");
int certificateId = Convert.ToInt32($"{data.certificate_id}");
return (id, certificateId);
}
public async Task RemoveLocation(int proxyId, int certificateId) {
if (proxyId == -1 || certificateId == -1) return;
await Login();
await _client.DeleteAsync(_options.Url + "/api/nginx/proxy-hosts/" + proxyId);
await _client.DeleteAsync(_options.Url + "/api/nginx/certificates/" + certificateId);
}
private sealed record ProxyAuth(string identity, string secret);
private sealed record TokenResponse(string token, string expires);
private sealed class CreateData {
public string access_list_id { get; set; } = "0";
public string advanced_config { get; set; } = " location / {\r\n proxy_pass http://%docker_ip%;\r\n proxy_hide_header X-Frame-Options;\r\n }";
public bool allow_websocket_upgrade { get; set; } = true;
public bool block_exploits { get; set; } = true;
public bool caching_enabled { get; set; } = true;
public string certificate_id { get; set; } = "new";
public string[] domain_names { get; set; }
public string forward_host { get; set; }
public int forward_port { get; set; }
public string forward_scheme { get; set; } = "http";
public bool hsts_enabled { get; set; } = false;
public bool hsts_subdomains { get; set; } = false;
public bool http2_support { get; set; } = true;
public string[] locations { get; set; } = Array.Empty<string>();
public bool ssl_forced { get; set; } = true;
public SslMeta meta { get; set; }
public CreateData(string domain, string ip, int port, string email) {
domain_names = new[] { domain };
forward_host = ip;
forward_port = port;
meta = new SslMeta {
letsencrypt_email = email
};
advanced_config = advanced_config.Replace("%docker_ip%", $"{ip}:{port}");
}
}
private sealed class SslMeta {
public bool dns_challenge { get; set; } = false;
public bool letsencrypt_agree { get; set; } = true;
public string letsencrypt_email { get; set; }
}
}

View File

@@ -0,0 +1,66 @@
using Microsoft.Extensions.Options;
using ProjectManager.Backend.Entities;
using ProjectManager.Backend.Options;
namespace ProjectManager.Backend.Apis;
public interface ITokenApi {
public string GetValidToken(string userId, string clientIp);
public bool ValidateToken(string tokenId, string clientIp);
public User GetUserFromToken(string tokenId);
}
public sealed class TokenApi : ITokenApi {
private readonly DatabaseContext _context;
private readonly GeneralOptions _options;
private readonly IUserApi _users;
public TokenApi(DatabaseContext context, IOptions<GeneralOptions> options, IUserApi users) {
_context = context;
_options = options.Value;
_users = users;
}
public string GetValidToken(string userId, string clientIp) {
var token = _context.Tokens.SingleOrDefault(token => token.UserId == userId && token.ClientIp == clientIp);
if (token == null) {
token = new() {
TokenId = Guid.NewGuid().ToString(),
UserId = userId,
ClientIp = clientIp,
Created = DateTime.Now
};
_context.Tokens.Add(token);
_context.SaveChanges();
return token.TokenId;
}
if (!ValidateToken(token.TokenId, clientIp)) {
_context.Tokens.Remove(token);
token = new() {
TokenId = Guid.NewGuid().ToString(),
UserId = userId,
ClientIp = clientIp,
Created = DateTime.Now
};
_context.Tokens.Add(token);
_context.SaveChanges();
return token.TokenId;
}
return token.TokenId;
}
public bool ValidateToken(string tokenId, string clientIp) {
if (tokenId is null || clientIp is null) return false;
var token = _context.Tokens.SingleOrDefault(token => token.TokenId == tokenId && token.ClientIp == clientIp);
if (token is null) return false;
if (_context.Users.SingleOrDefault(user => user.UserId == token.UserId) == null) return false;
return DateTime.Now - token.Created < TimeSpan.FromDays(_options.TokenTimeInDays);
}
public User GetUserFromToken(string tokenId) {
return _users.GetUser(_context.Tokens.SingleOrDefault(token => token.TokenId == tokenId)?.UserId);
}
}

View File

@@ -0,0 +1,121 @@
using System.Text;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Microsoft.Extensions.Options;
using ProjectManager.Backend.Entities;
using ProjectManager.Backend.Options;
namespace ProjectManager.Backend.Apis;
public interface IUserApi {
public User Login(User login);
public User Register(User register);
public User GetUser(string userId);
public IEnumerable<User> GetUsers();
public bool UpdateUser(User update);
public void DeleteUser(string userId);
public bool CanCreateProject(string userId);
}
public sealed class UserApi : IUserApi {
private readonly DatabaseContext _context;
private readonly IProjectApi _projects;
private readonly GeneralOptions _options;
public UserApi(DatabaseContext context, IProjectApi projects, IOptions<GeneralOptions> options) {
_context = context;
_projects = projects;
_options = options.Value;
}
public User Login(User login) {
if (string.IsNullOrEmpty(login.Email) || string.IsNullOrEmpty(login.Password)) return null;
var hash = Hash128(login.Password, login.Email);
return _context.Users.SingleOrDefault(user => user.Email == login.Email && user.Password == hash);
}
public User Register(User register) {
if (string.IsNullOrEmpty(register.Username) ||
string.IsNullOrEmpty(register.Email) ||
string.IsNullOrEmpty(register.Password)) return null;
if (_context.Users.Any(user => user.Email == register.Email || user.Username == register.Username))
return null;
if (register.Email.Length > 255 || register.Username.Length > 255 || register.Password.Length > 255)
return null;
if (!register.Email.Contains('@') || !register.Email.Contains('.')) return null;
if (!register.Password.Any(char.IsLetter) || !register.Password.Any(char.IsDigit) || register.Password.Length < 8) return null;
User user = new() {
UserId = Guid.NewGuid().ToString(),
Email = register.Email,
Username = register.Username,
Password = Hash128(register.Password, register.Email),
MaxProjects = _options.MaxProjects
};
_context.Users.Add(user);
_context.SaveChanges();
return user;
}
public User GetUser(string userId) {
return _context.Users.SingleOrDefault(user => user.UserId == userId);
}
public IEnumerable<User> GetUsers() {
return _context.Users;
}
public bool UpdateUser(User update) {
if (_context.Users.Any(user => user.UserId != update.UserId && (user.Email == update.Email || user.Username == update.Username)))
return false;
if (update.Email.Length > 255 || update.Username.Length > 255 || update.Password.Length > 255)
return false;
if (!string.IsNullOrEmpty(update.Email) && (!update.Email.Contains('@') || !update.Email.Contains('.'))) return false;
if (!string.IsNullOrEmpty(update.Password) && (!update.Password.Any(char.IsLetter) || !update.Password.Any(char.IsDigit) || update.Password.Length < 8)) return false;
var user = _context.Users.SingleOrDefault(user => user.UserId == update.UserId);
if (user == null) return false;
if (!string.IsNullOrEmpty(update.Email)) user.Email = update.Email;
if (!string.IsNullOrEmpty(update.Username)) user.Username = update.Username;
if (!string.IsNullOrEmpty(update.Password)) user.Password = Hash128(update.Password, user.Email);
_context.Users.Update(user);
_context.SaveChanges();
return true;
}
public void DeleteUser(string userId) {
if (!_context.Users.Any(user => user.UserId == userId)) return;
var user = _context.Users.Single(user => user.UserId == userId);
var projects = _projects.GetProjects(userId);
foreach (var project in projects) {
_projects.DeleteProject(project.ProjectId);
}
_context.Users.Remove(user);
_context.SaveChanges();
}
public bool CanCreateProject(string userId) {
return GetUser(userId).MaxProjects > _projects.GetProjects(userId).Length;
}
private static string Hash128(string plainText, string salt) {
try {
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: plainText,
salt: Encoding.Default.GetBytes(salt),
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 100000,
numBytesRequested: 256 / 8
));
return hashed;
} catch (Exception) { return ""; }
}
}