├── global.json
├── src
├── AirDropAnywhere.Core
│ ├── libnative.so
│ ├── InternalsVisibleTo.cs
│ ├── Resources
│ │ ├── AppleRootCA.crt
│ │ └── ResourceLoader.cs
│ ├── AirDropOptions.cs
│ ├── Protocol
│ │ ├── MediaCapabilities.cs
│ │ ├── RecordData.cs
│ │ ├── AskResponse.cs
│ │ ├── FileMetadata.cs
│ │ ├── DiscoverResponse.cs
│ │ ├── DiscoverRequest.cs
│ │ └── AskRequest.cs
│ ├── Interop.cs
│ ├── libnative.m
│ ├── AirDropAnywhere.Core.csproj
│ ├── AirDropReceiverFlags.cs
│ ├── AirDropKestrelExtensions.cs
│ ├── AirDropPeer.cs
│ ├── Serialization
│ │ ├── PropertyListSerializer.cs
│ │ └── PropertyListConverter.cs
│ ├── AirDropServiceCollectionExtensions.cs
│ ├── AirDropEndpointRouteBuilderExtensions.cs
│ ├── MulticastDns
│ │ ├── UdpSocketExtensions.cs
│ │ ├── MulticastDnsService.cs
│ │ ├── UdpAwaitableSocketAsyncEventArgs.cs
│ │ └── MulticastDnsServer.cs
│ ├── Certificates
│ │ └── CertificateManager.cs
│ ├── Utils.cs
│ ├── AirDropRouteHandler.cs
│ ├── AirDropService.cs
│ └── Compression
│ │ └── CpioArchiveReader.cs
└── AirDropAnywhere.Cli
│ ├── Hubs
│ ├── OnFileUploadedResponseMessage.cs
│ ├── ConnectMessage.cs
│ ├── CanAcceptFilesResponseMessage.cs
│ ├── CanAcceptFileMetadata.cs
│ ├── OnFileUploadedRequestMessage.cs
│ ├── CanAcceptFilesRequestMessage.cs
│ ├── PolymorphicJsonIncludeAttribute.cs
│ ├── AirDropHubMessage.cs
│ ├── PolymorphicJsonConverter.cs
│ └── AirDropHub.cs
│ ├── wwwroot
│ └── index.html
│ ├── appsettings.json
│ ├── Properties
│ └── launchSettings.json
│ ├── Commands
│ ├── CommandBase.cs
│ ├── ServerCommand.cs
│ └── ClientCommand.cs
│ ├── AirDropAnywhere.Cli.csproj
│ ├── Logging
│ ├── SpectreInlineLoggerProvider.cs
│ └── SpectreInlineLogger.cs
│ └── Program.cs
├── tests
└── AirDropAnywhere.Tests
│ ├── test.large.cpio
│ ├── test.multiple.cpio
│ ├── test.single.cpio
│ ├── OctalParsingTests.cs
│ ├── test.nested.cpio
│ ├── AirDropAnywhere.Tests.csproj
│ └── CpioArchiveReaderTests.cs
├── .idea
└── .idea.AirDropAnywhere
│ └── .idea
│ ├── encodings.xml
│ ├── vcs.xml
│ ├── indexLayout.xml
│ └── .gitignore
├── nuget.config
├── version.json
├── .github
└── workflows
│ ├── build.yml
│ └── publish.yml
├── .run
├── CLI_ Client.run.xml
└── CLI_ Server.run.xml
├── LICENSE
├── Directory.Packages.props
├── Directory.Build.props
├── .vscode
├── launch.json
└── tasks.json
├── restore.ps1
├── README.md
├── restore.sh
├── AirDropAnywhere.sln
└── .gitignore
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "6.0.100-rc.1.21463.6"
4 | }
5 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/libnative.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deanward81/AirDropAnywhere/HEAD/src/AirDropAnywhere.Core/libnative.so
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/InternalsVisibleTo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly:InternalsVisibleTo("AirDropAnywhere.Tests")]
--------------------------------------------------------------------------------
/tests/AirDropAnywhere.Tests/test.large.cpio:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deanward81/AirDropAnywhere/HEAD/tests/AirDropAnywhere.Tests/test.large.cpio
--------------------------------------------------------------------------------
/tests/AirDropAnywhere.Tests/test.multiple.cpio:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deanward81/AirDropAnywhere/HEAD/tests/AirDropAnywhere.Tests/test.multiple.cpio
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Resources/AppleRootCA.crt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deanward81/AirDropAnywhere/HEAD/src/AirDropAnywhere.Core/Resources/AppleRootCA.crt
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/OnFileUploadedResponseMessage.cs:
--------------------------------------------------------------------------------
1 | namespace AirDropAnywhere.Cli.Hubs
2 | {
3 | internal class OnFileUploadedResponseMessage : AirDropHubMessage
4 | {
5 | }
6 | }
--------------------------------------------------------------------------------
/.idea/.idea.AirDropAnywhere/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hello World
6 |
7 |
8 | Hello World!
9 |
10 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/ConnectMessage.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 | namespace AirDropAnywhere.Cli.Hubs
3 | {
4 | internal class ConnectMessage : AirDropHubMessage
5 | {
6 | public string Name { get; set; }
7 | }
8 | }
--------------------------------------------------------------------------------
/.idea/.idea.AirDropAnywhere/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/CanAcceptFilesResponseMessage.cs:
--------------------------------------------------------------------------------
1 | namespace AirDropAnywhere.Cli.Hubs
2 | {
3 | internal class CanAcceptFilesResponseMessage : AirDropHubMessage
4 | {
5 | public bool Accepted { get; set; }
6 | }
7 | }
--------------------------------------------------------------------------------
/.idea/.idea.AirDropAnywhere/.idea/indexLayout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/CanAcceptFileMetadata.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 | namespace AirDropAnywhere.Cli.Hubs
3 | {
4 | internal class CanAcceptFileMetadata
5 | {
6 | public string Name { get; set; }
7 | public string Type { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/OnFileUploadedRequestMessage.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 | namespace AirDropAnywhere.Cli.Hubs
3 | {
4 | internal class OnFileUploadedRequestMessage : AirDropHubMessage
5 | {
6 | public string Name { get; set; }
7 | public string Url { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "System": "Warning",
6 | "Microsoft": "Warning",
7 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Information"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/.idea.AirDropAnywhere/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /modules.xml
6 | /contentModel.xml
7 | /.idea.AirDropAnywhere.iml
8 | /projectSettingsUpdater.xml
9 | # Datasource local storage ignored files
10 | /dataSources/
11 | /dataSources.local.xml
12 | # Editor-based HTTP Client requests
13 | /httpRequests/
14 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/AirDropOptions.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Microsoft.Extensions.FileProviders;
3 |
4 | namespace AirDropAnywhere.Core
5 | {
6 | public class AirDropOptions
7 | {
8 | [Required]
9 | public ushort ListenPort { get; set; }
10 |
11 | [Required]
12 | public string UploadPath { get; set; } = null!;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Protocol/MediaCapabilities.cs:
--------------------------------------------------------------------------------
1 | namespace AirDropAnywhere.Core.Protocol
2 | {
3 | internal class MediaCapabilities
4 | {
5 | public static readonly MediaCapabilities Default = new(1);
6 |
7 | public MediaCapabilities(int version)
8 | {
9 | Version = version;
10 | }
11 |
12 | public int Version { get; private set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Protocol/RecordData.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 |
4 | namespace AirDropAnywhere.Core.Protocol
5 | {
6 | internal class RecordData
7 | {
8 | public IEnumerable ValidatedEmailHashes { get; private set; } = Enumerable.Empty();
9 | public IEnumerable ValidatedPhoneHashes { get; private set; } = Enumerable.Empty();
10 | }
11 | }
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1",
3 | "versionHeightOffset": -1,
4 | "assemblyVersion": "0.1",
5 | "publicReleaseRefSpec": [
6 | "^refs/heads/main$",
7 | "^refs/tags/v\\d+\\.\\d+"
8 | ],
9 | "nugetPackageVersion": {
10 | "semVer": 2
11 | },
12 | "cloudBuild": {
13 | "buildNumber": {
14 | "enabled": true,
15 | "setVersionVariables": true
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Interop.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace AirDropAnywhere.Core
4 | {
5 | internal static class Interop
6 | {
7 | [DllImport("libnative.so", EntryPoint = "StartAWDLBrowsing", SetLastError = true)]
8 | public static extern void StartAWDLBrowsing();
9 |
10 | [DllImport("libnative.so", EntryPoint = "StopAWDLBrowsing", SetLastError = true)]
11 | public static extern void StopAWDLBrowsing();
12 | }
13 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/CanAcceptFilesRequestMessage.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace AirDropAnywhere.Cli.Hubs
6 | {
7 | internal class CanAcceptFilesRequestMessage : AirDropHubMessage
8 | {
9 | public string SenderComputerName { get; set; }
10 | public byte[] FileIcon { get; set; }
11 | public IEnumerable Files { get; set; } = Enumerable.Empty();
12 | }
13 | }
--------------------------------------------------------------------------------
/tests/AirDropAnywhere.Tests/test.single.cpio:
--------------------------------------------------------------------------------
1 | 0707077777770000011006440007650000240000010000001404402665400001100000000041test.txt This is an example text document
2 | 0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!!
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "Server": {
5 | "commandName": "Project",
6 | "commandLineArgs": "server --port 8181",
7 | "environmentVariables": {
8 | "ASPNETCORE_ENVIRONMENT": "Local"
9 | }
10 | },
11 | "Client": {
12 | "commandName": "Project",
13 | "commandLineArgs": "client --server localhost --port 8181 --path downloads",
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Local"
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build & Test
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - 'docs/**' # Don't run workflow when files are only in the /docs directory
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | if: "!contains(github.event.head_commit.message, 'ci skip')"
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 | - uses: actions/setup-dotnet@v1
20 | - name: .NET Build
21 | run: dotnet build -c Release --nologo
22 | - name: .NET Test
23 | run: dotnet test -c Release --no-build --nologo
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Commands/CommandBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Logging;
3 | using Spectre.Console;
4 | using Spectre.Console.Cli;
5 |
6 | namespace AirDropAnywhere.Cli.Commands
7 | {
8 | public abstract class CommandBase : AsyncCommand where TSettings : CommandSettings
9 | {
10 | protected IAnsiConsole Console { get; }
11 | protected ILogger Logger { get; }
12 |
13 | protected CommandBase(IAnsiConsole console, ILogger logger)
14 | {
15 | Console = console ?? throw new ArgumentNullException(nameof(console));
16 | Logger = logger ?? throw new ArgumentNullException(nameof(logger));
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/libnative.m:
--------------------------------------------------------------------------------
1 | /*
2 | * Code derived from https://github.com/yggdrasil-network/yggdrasil-go/blob/master/src/multicast/multicast_darwin.go
3 | * Forces the system to initialize AWDL on MacOS so that we can advertise AirDrop
4 | * services using it.
5 | */
6 | #import
7 | NSNetServiceBrowser *serviceBrowser;
8 | void StartAWDLBrowsing() {
9 | if (serviceBrowser == nil) {
10 | serviceBrowser = [[NSNetServiceBrowser alloc] init];
11 | serviceBrowser.includesPeerToPeer = YES;
12 | }
13 | [serviceBrowser searchForServicesOfType:@"_airdrop_proxy._tcp" inDomain:@""];
14 | }
15 | void StopAWDLBrowsing() {
16 | if (serviceBrowser == nil) {
17 | return;
18 | }
19 | [serviceBrowser stop];
20 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Protocol/AskResponse.cs:
--------------------------------------------------------------------------------
1 | namespace AirDropAnywhere.Core.Protocol
2 | {
3 | ///
4 | /// Body of a response from the /Ask endpoint in the AirDrop HTTP API.
5 | ///
6 | internal class AskResponse
7 | {
8 | public AskResponse(string computerName, string modelName)
9 | {
10 | ReceiverComputerName = computerName;
11 | ReceiverModelName = modelName;
12 | }
13 |
14 | ///
15 | /// Gets the receiver computer's name.
16 | ///
17 | public string ReceiverComputerName { get; }
18 | ///
19 | /// Gets the model name of the receiver.
20 | ///
21 | public string ReceiverModelName { get; }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/PolymorphicJsonIncludeAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace AirDropAnywhere.Cli.Hubs
4 | {
5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
6 | internal class PolymorphicJsonIncludeAttribute : Attribute
7 | {
8 | public PolymorphicJsonIncludeAttribute(string name, Type type)
9 | {
10 | Name = name ?? throw new ArgumentNullException(nameof(name));
11 | Type = type ?? throw new ArgumentNullException(nameof(type));
12 | }
13 |
14 | ///
15 | /// Gets the name of the mapping when serialized by the .
16 | ///
17 | public string Name { get; }
18 |
19 | ///
20 | /// Gets the that should be handled by the class.
21 | ///
22 | public Type Type { get; }
23 | }
24 | }
--------------------------------------------------------------------------------
/.run/CLI_ Client.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.run/CLI_ Server.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/AirDropAnywhere.Cli.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | true
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | PreserveNewest
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Logging/SpectreInlineLoggerProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using Microsoft.Extensions.Logging;
4 | using Spectre.Console;
5 |
6 | namespace AirDropAnywhere.Cli.Logging
7 | {
8 | internal class SpectreInlineLoggerProvider : ILoggerProvider
9 | {
10 | private readonly IAnsiConsole _console;
11 | private readonly ConcurrentDictionary _loggers = new();
12 |
13 | public SpectreInlineLoggerProvider(IAnsiConsole console)
14 | {
15 | _console = console ?? throw new ArgumentNullException(nameof(console));
16 | }
17 |
18 | public ILogger CreateLogger(string categoryName)
19 | {
20 | return _loggers.GetOrAdd(categoryName, name => new SpectreInlineLogger(name, _console));
21 | }
22 |
23 | public void Dispose()
24 | {
25 | _loggers.Clear();
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build, Test & Package
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - 'docs/**' # Don't run workflow when files are only in the /docs directory
9 |
10 | jobs:
11 | publish:
12 | runs-on: ubuntu-latest
13 | if: "!contains(github.event.head_commit.message, 'ci skip')"
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 | - uses: actions/setup-dotnet@v1
20 | - name: .NET Build
21 | run: dotnet build -c Release --nologo
22 | - name: .NET Test
23 | run: dotnet test -c Release --no-build --nologo
24 | - name: .NET Pack
25 | run: dotnet pack --no-build -c Release --nologo /p:PackageOutputPath=$GITHUB_WORKSPACE/.nupkgs
26 | - name: Push to GH Packages
27 | run: dotnet nuget push $GITHUB_WORKSPACE/.nupkgs/*.nupkg --source https://nuget.pkg.github.com/deanward81/index.json --api-key ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Protocol/FileMetadata.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable UnusedAutoPropertyAccessor.Local
2 | // ReSharper disable ClassNeverInstantiated.Global
3 | #pragma warning disable 8618
4 | namespace AirDropAnywhere.Core.Protocol
5 | {
6 | public class FileMetadata
7 | {
8 | ///
9 | /// Gets the name of the file.
10 | ///
11 | public string FileName { get; private set; }
12 | ///
13 | /// Gets the type of the file.
14 | ///
15 | public string FileType { get; private set; }
16 | ///
17 | /// Gets a value indicating whether the "file" is actually a directory.
18 | ///
19 | public bool FileIsDirectory { get; private set; }
20 | ///
21 | /// Gets a value indicating whether the file needs to be converted or not.
22 | ///
23 | public bool ConvertMediaFormats { get; private set; }
24 | }
25 | }
--------------------------------------------------------------------------------
/tests/AirDropAnywhere.Tests/OctalParsingTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using AirDropAnywhere.Core;
4 | using Xunit;
5 |
6 | namespace AirDropAnywhere.Tests
7 | {
8 | public class OctalParsingTests
9 | {
10 | [Theory]
11 | [InlineData("7645342", 2050786, true)]
12 | [InlineData("00004567", 2423, true)]
13 | [InlineData("0", 0, true)]
14 | [InlineData("", 0, false)] // zero length
15 | [InlineData("7AB5342", 0, false)] // no numbers
16 | [InlineData("a string", 0, false)] // no numbers
17 | [InlineData("-3423423", 0, false)] // parsing to a ulong - sign is not allowed
18 | public void ParseOctalToNumber(string input, ulong expectedValue, bool success)
19 | {
20 | var stringAsByteSpan = Encoding.ASCII.GetBytes(input);
21 | Assert.Equal(success, Utils.TryParseOctalToUInt32(stringAsByteSpan, out var actualValue));
22 | Assert.Equal(expectedValue, actualValue);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/tests/AirDropAnywhere.Tests/test.nested.cpio:
--------------------------------------------------------------------------------
1 | 0707077777770000011006440007650000240000010000001405676516300002100000000005./test1/test.txt test
2 | 0707077777770000020407550007650000240000030000001405676515700001000000000000./test1 0707077777770000031006440007650000240000010000001405676524300002700000000005./test3/test4/test.csv test
3 | 0707077777770000040407550007650000240000030000001405676524300001600000000000./test3/test4 0707077777770000050407550007650000240000030000001405676520400001000000000000./test3 0707077777770000061006440007650000240000010000001405676517400002100000000005./test2/test.log test
4 | 0707077777770000070407550007650000240000030000001405676517400001000000000000./test2 0707077777770000100407550007650000240000050000001405676534500000200000000000. 0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!!
5 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/AirDropAnywhere.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | true
6 | AirDropAnywhere
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 |
--------------------------------------------------------------------------------
/tests/AirDropAnywhere.Tests/AirDropAnywhere.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 | all
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Dean Ward
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 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Resources/ResourceLoader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.CompilerServices;
4 | using System.Security.Cryptography.X509Certificates;
5 |
6 | namespace AirDropAnywhere.Core.Resources
7 | {
8 | internal static class ResourceLoader
9 | {
10 | private static X509Certificate2? _appleRootCA;
11 |
12 | public static X509Certificate2 AppleRootCA =>
13 | _appleRootCA ??= GetResource(
14 | "AppleRootCA.crt",
15 | s =>
16 | {
17 | Span readBuffer = stackalloc byte[(int) s.Length];
18 | s.Read(readBuffer);
19 | return new X509Certificate2(readBuffer);
20 | });
21 |
22 | private static T GetResource(string resourceName, Func converter)
23 | {
24 | using var resourceStream = typeof(ResourceLoader).Assembly.GetManifestResourceStream(
25 | typeof(ResourceLoader),
26 | resourceName!
27 | );
28 |
29 | if (resourceStream == null)
30 | {
31 | throw new ArgumentException($"Could not load resource '{resourceName}'");
32 | }
33 |
34 | return converter(resourceStream);
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 6.0.0-rc.1.21452.15
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/AirDropReceiverFlags.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace AirDropAnywhere.Core
4 | {
5 | ///
6 | /// Flags attached to mDNS responses indicating what is supported by
7 | /// the AirDrop service.
8 | ///
9 | ///
10 | /// Derived from OpenDrop: https://github.com/seemoo-lab/opendrop/blob/master/opendrop/config.py#L32-L51
11 | /// On MacOS the default is configured to be 0x3fb which is defined in .
12 | /// OpenDrop currently supports a subset of this so we'll use that as our default for now.
13 | ///
14 | [Flags]
15 | internal enum AirDropReceiverFlags : ushort
16 | {
17 | Url = 1 << 0,
18 | DvZip = 1 << 1,
19 | Pipelining = 1 << 2,
20 | MixedTypes = 1 << 3,
21 | Unknown1 = 1 << 4,
22 | Unknown2 = 1 << 5,
23 | Iris = 1 << 6,
24 | Discover = 1 << 7,
25 | Unknown3 = 1 << 8,
26 | AssetBundle = 1 << 9,
27 |
28 | ///
29 | /// Default broadcast by MacOS
30 | ///
31 | DefaultMacOS = Url | DvZip | MixedTypes | Unknown1 | Unknown2 | Iris | Discover | Unknown3 | AssetBundle,
32 | ///
33 | /// Default used by AirDropAnywhere, will extend as more of AirDrop is implemented.
34 | ///
35 | Default = Url | Pipelining | MixedTypes | Discover | AssetBundle,
36 | }
37 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Protocol/DiscoverResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace AirDropAnywhere.Core.Protocol
4 | {
5 | ///
6 | /// Body of a response from the /Discover endpoint in the AirDrop HTTP API.
7 | ///
8 | internal class DiscoverResponse
9 | {
10 | public DiscoverResponse(string computerName, string modelName, MediaCapabilities mediaCapabilities)
11 | {
12 | ReceiverComputerName = computerName;
13 | ReceiverModelName = modelName;
14 | ReceiverMediaCapabilities = JsonSerializer.SerializeToUtf8Bytes(mediaCapabilities);
15 | // TODO: implement contact data
16 | //ReceiverRecordData = Array.Empty();
17 | }
18 |
19 | ///
20 | /// Gets the receiver computer's name. Displayed when selecting a "contact" to send to.
21 | ///
22 | public string ReceiverComputerName { get; }
23 | ///
24 | /// Gets the model name of the receiver.
25 | ///
26 | public string ReceiverModelName { get; }
27 | ///
28 | /// Gets the UTF-8 encoded bytes of a JSON payload detailing the
29 | /// media capabilities of the receiver.
30 | ///
31 | ///
32 | /// This payload is represented in code by the class.
33 | ///
34 | public byte[] ReceiverMediaCapabilities { get; }
35 | //public byte[] ReceiverRecordData { get; }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/AirDropKestrelExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography.X509Certificates;
3 | using AirDropAnywhere.Core;
4 | using Microsoft.AspNetCore.Server.Kestrel.Core;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Options;
7 |
8 | // ReSharper disable once CheckNamespace
9 | namespace Microsoft.AspNetCore.Hosting
10 | {
11 | ///
12 | /// Extensions used to configure AirDrop defaults in Kestrel.
13 | ///
14 | public static class AirDropKestrelExtensions
15 | {
16 | ///
17 | /// Configures AirDrop defaults for Kestrel.
18 | ///
19 | /// A instance to configure.
20 | /// An representing the certificate to use for the AirDrop HTTPS endpoint.
21 | public static void ConfigureAirDropDefaults(this KestrelServerOptions options, X509Certificate2 cert)
22 | {
23 | if (cert == null)
24 | {
25 | throw new ArgumentNullException(nameof(cert));
26 | }
27 |
28 | var airDropOptions = options.ApplicationServices.GetRequiredService>();
29 | options.ConfigureEndpointDefaults(
30 | endpointDefaults =>
31 | {
32 | endpointDefaults.UseHttps(cert);
33 | });
34 |
35 | options.ListenAnyIP(airDropOptions.Value.ListenPort);
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1.0.0
4 | 2021 Dean Ward
5 | strict
6 | latest
7 | enable
8 | Dean Ward
9 | https://github.com/deanward81/AirDropAnywhere
10 | MIT
11 | https://github.com/deanward81/AirDropAnywhere
12 | git
13 | true
14 | embedded
15 | en-GB
16 | false
17 | true
18 | false
19 | true
20 | true
21 | true
22 |
23 |
24 | true
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/AirDropPeer.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using AirDropAnywhere.Core.Protocol;
3 |
4 | namespace AirDropAnywhere.Core
5 | {
6 | ///
7 | /// Exposes a way for the AirDrop HTTP API to communicate with an arbitrary peer that does
8 | /// not directly support the AirDrop protocol.
9 | ///
10 | public abstract class AirDropPeer
11 | {
12 | protected AirDropPeer()
13 | {
14 | Id = Utils.GetRandomString();
15 | Name = Id;
16 | }
17 |
18 | ///
19 | /// Gets the unique identifier of this peer.
20 | ///
21 | public string Id { get; }
22 |
23 | ///
24 | /// Gets the (display) name of this peer.
25 | ///
26 | public string Name { get; protected set; }
27 |
28 | ///
29 | /// Determines whether the peer wants to receive files from a sender.
30 | ///
31 | ///
32 | /// An object representing information about the sender
33 | /// and the files that they wish to send.
34 | /// sender
35 | ///
36 | ///
37 | /// true if the receiver wants to accept the file transfer, false otherwise.
38 | ///
39 | public abstract ValueTask CanAcceptFilesAsync(AskRequest request);
40 |
41 | ///
42 | /// Notifies the peer that a file has been uploaded. This method is for every
43 | /// file extracted from the archive sent by an AirDrop-compatible device.
44 | ///
45 | ///
46 | /// Path to an extracted file.
47 | ///
48 | public abstract ValueTask OnFileUploadedAsync(string filePath);
49 | }
50 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Protocol/DiscoverRequest.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
2 |
3 | using System;
4 | using System.Security.Cryptography.Pkcs;
5 | using System.Security.Cryptography.X509Certificates;
6 | using AirDropAnywhere.Core.Resources;
7 | using AirDropAnywhere.Core.Serialization;
8 |
9 | namespace AirDropAnywhere.Core.Protocol
10 | {
11 | ///
12 | /// Body of a request to the /Discover endpoint in the AirDrop HTTP API.
13 | ///
14 | internal class DiscoverRequest
15 | {
16 | ///
17 | /// Gets a binary blob representing a PKCS7 signed plist containing
18 | /// sender email and phone hashes. This is validated and deserialized into a
19 | /// object by .
20 | ///
21 | public byte[] SenderRecordData { get; private set; } = Array.Empty();
22 |
23 | public bool TryGetSenderRecordData(out RecordData? recordData)
24 | {
25 | if (SenderRecordData == null || SenderRecordData.Length == 0)
26 | {
27 | recordData = default;
28 | return false;
29 | }
30 |
31 | // validate that the signature is valid
32 | var signedCms = new SignedCms();
33 | try
34 | {
35 | signedCms.Decode(SenderRecordData);
36 | signedCms.CheckSignature(
37 | new X509Certificate2Collection(ResourceLoader.AppleRootCA), true
38 | );
39 | }
40 | catch
41 | {
42 | recordData = default;
43 | return false;
44 | }
45 |
46 | recordData = PropertyListSerializer.Deserialize(signedCms.ContentInfo.Content);
47 | return true;
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/AirDropHubMessage.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 | using System;
3 | using System.Threading.Tasks;
4 |
5 | namespace AirDropAnywhere.Cli.Hubs
6 | {
7 | [PolymorphicJsonInclude("connect", typeof(ConnectMessage))]
8 | [PolymorphicJsonInclude("askRequest", typeof(CanAcceptFilesRequestMessage))]
9 | [PolymorphicJsonInclude("askResponse", typeof(CanAcceptFilesResponseMessage))]
10 | [PolymorphicJsonInclude("fileUploadRequest", typeof(OnFileUploadedRequestMessage))]
11 | [PolymorphicJsonInclude("fileUploadResponse", typeof(OnFileUploadedResponseMessage))]
12 | internal abstract class AirDropHubMessage
13 | {
14 | public string Id { get; set; }
15 | public string ReplyTo { get; set; }
16 |
17 | public static async ValueTask CreateAsync(Func modifier, TState state) where TMessage : AirDropHubMessage, new()
18 | {
19 | if (modifier == null)
20 | {
21 | throw new ArgumentNullException(nameof(modifier));
22 | }
23 |
24 | var message = Create();
25 | await modifier(message, state);
26 | return message;
27 | }
28 |
29 | public static async ValueTask CreateAsync(Func modifier) where TMessage : AirDropHubMessage, new()
30 | {
31 | if (modifier == null)
32 | {
33 | throw new ArgumentNullException(nameof(modifier));
34 | }
35 |
36 | var message = Create();
37 | await modifier(message);
38 | return message;
39 | }
40 |
41 | private static TMessage Create() where TMessage : AirDropHubMessage, new() =>
42 | new()
43 | {
44 | Id = Guid.NewGuid().ToString("N"),
45 | };
46 | }
47 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Logging/SpectreInlineLogger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using Microsoft.Extensions.Logging;
4 | using Spectre.Console;
5 |
6 | namespace AirDropAnywhere.Cli.Logging
7 | {
8 | internal class SpectreInlineLogger : ILogger
9 | {
10 | private readonly string _name;
11 | private readonly IAnsiConsole _console;
12 |
13 | public SpectreInlineLogger(string name, IAnsiConsole console)
14 | {
15 | _name = name;
16 | _console = console;
17 | }
18 |
19 | public IDisposable BeginScope(TState state) => null!;
20 |
21 | public bool IsEnabled(LogLevel logLevel) => true;
22 |
23 | ///
24 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
25 | {
26 | if (!IsEnabled(logLevel))
27 | {
28 | return;
29 | }
30 |
31 | var stringBuilder = new StringBuilder(80);
32 | stringBuilder.Append(GetLevelMarkup(logLevel));
33 | stringBuilder.AppendFormat("[dim grey]{0}[/] ", _name);
34 | stringBuilder.Append(Markup.Escape(formatter(state, exception)));
35 | _console.MarkupLine(stringBuilder.ToString());
36 | }
37 |
38 | private static string GetLevelMarkup(LogLevel level)
39 | {
40 | return level switch
41 | {
42 | LogLevel.Critical => "[bold underline white on red]|CRIT|:[/] ",
43 | LogLevel.Error => "[bold red]|ERROR|:[/] ",
44 | LogLevel.Warning => "[bold orange3]| WARN|:[/] ",
45 | LogLevel.Information => "[bold dim]| INFO|:[/] ",
46 | LogLevel.Debug => "[dim]|DEBUG|:[/] ",
47 | LogLevel.Trace => "[dim grey]|TRACE|:[/] ",
48 | _ => "|UNKWN|: "
49 | };
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Protocol/AskRequest.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable UnusedAutoPropertyAccessor.Local
2 | // ReSharper disable ClassNeverInstantiated.Global
3 | // ReSharper disable InconsistentNaming
4 | #pragma warning disable 8618
5 | using System.Collections.Generic;
6 | using System.Linq;
7 |
8 | namespace AirDropAnywhere.Core.Protocol
9 | {
10 | ///
11 | /// Body of a request to the /Ask endpoint in the AirDrop HTTP API.
12 | ///
13 | public class AskRequest
14 | {
15 | ///
16 | /// Gets the sender computer's name. Displayed when asking for receiving a file not from a contact
17 | ///
18 | public string SenderComputerName { get; private set; }
19 | ///
20 | /// Gets the model name of the sender
21 | ///
22 | public string SenderModelName { get; private set; }
23 | ///
24 | /// Gets the service id distributed over mDNS
25 | ///
26 | public string SenderID { get; private set; }
27 | ///
28 | /// Gets the bundle id of the sending application
29 | ///
30 | public string BundleID { get; private set; }
31 | ///
32 | /// Gets a value indicating whether the sender wants that media formats are converted
33 | ///
34 | public bool ConvertMediaFormats { get; private set; }
35 | ///
36 | /// Gets the sender's contact information.
37 | ///
38 | public byte[] SenderRecordData { get; private set; }
39 | ///
40 | /// Gets a JPEG2000 encoded file icon used for display.
41 | ///
42 | public byte[] FileIcon { get; private set; }
43 | ///
44 | /// Gets an of objects
45 | /// containing metadata about the files the sender wishes to send.
46 | ///
47 | public IEnumerable Files { get; private set; } = Enumerable.Empty();
48 | }
49 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Net.Security;
4 | using System.Threading.Tasks;
5 | using AirDropAnywhere.Cli.Commands;
6 | using AirDropAnywhere.Cli.Logging;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Logging;
9 | using Spectre.Cli.Extensions.DependencyInjection;
10 | using Spectre.Console;
11 | using Spectre.Console.Cli;
12 |
13 | namespace AirDropAnywhere.Cli
14 | {
15 | internal static class Program
16 | {
17 | public const string ApplicationName = "🌐 AirDrop Anywhere";
18 | public static Task Main(string[] args)
19 | {
20 | var services = new ServiceCollection();
21 |
22 | services
23 | .AddHttpClient("airdrop")
24 | .ConfigurePrimaryHttpMessageHandler(
25 | () => new SocketsHttpHandler
26 | {
27 | // we using a self-signed certificate, so ignore it
28 | SslOptions = new SslClientAuthenticationOptions
29 | {
30 | RemoteCertificateValidationCallback = delegate { return true; }
31 | }
32 | });
33 |
34 | services
35 | .AddSingleton(AnsiConsole.Console)
36 | .AddLogging(
37 | builder =>
38 | {
39 | builder.ClearProviders();
40 | builder.AddProvider(new SpectreInlineLoggerProvider(AnsiConsole.Console));
41 | }
42 | );
43 |
44 | var typeRegistrar = new DependencyInjectionRegistrar(services);
45 | var app = new CommandApp(typeRegistrar);
46 | app.Configure(
47 | c =>
48 | {
49 | c.AddCommand("server");
50 | c.AddCommand("client");
51 | }
52 | );
53 |
54 | AnsiConsole.WriteLine(ApplicationName);
55 | return app.RunAsync(args);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to find out which attributes exist for C# debugging
3 | // Use hover for the description of the existing attributes
4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.mdn
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (console)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/bin/Debug/net6.0/${workspaceFolderBasename}.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}",
16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17 | "console": "internalConsole",
18 | "stopAtEntry": false
19 | },
20 | {
21 | "name": ".NET Core Attach",
22 | "type": "coreclr",
23 | "request": "attach",
24 | "processId": "${command:pickProcess}"
25 | },
26 | {
27 | "name": ".NET Core Launch (remote)",
28 | "type": "coreclr",
29 | "request": "launch",
30 | "preLaunchTask": "RaspberryPiDeploy",
31 | "program": "./AirDropAnywhere.Cli",
32 | "args": [
33 | "server",
34 | "--port",
35 | "8080"
36 | ],
37 | "cwd": "~/src/${workspaceFolderBasename}",
38 | "stopAtEntry": false,
39 | "console": "internalConsole",
40 | "pipeTransport": {
41 | "pipeCwd": "${workspaceFolder}",
42 | "pipeProgram": "ssh",
43 | "pipeArgs": [
44 | "pi@${input:host}"
45 | ],
46 | "debuggerPath": "~/vsdbg/vsdbg"
47 | }
48 | }
49 | ],
50 | "inputs": [
51 | {
52 | "id": "host",
53 | "description": "Raspberry Pi host to debug",
54 | "default": "dward-pi",
55 | "type": "promptString"
56 | }
57 | ]
58 | }
--------------------------------------------------------------------------------
/restore.ps1:
--------------------------------------------------------------------------------
1 | # Admin scripts from https://www.autoitscript.com/forum/topic/174609-powershell-script-to-self-elevate/
2 | function Test-IsAdmin()
3 | {
4 | # Get the current ID and its security principal
5 | $windowsID = [System.Security.Principal.WindowsIdentity]::GetCurrent()
6 | $windowsPrincipal = new-object System.Security.Principal.WindowsPrincipal($windowsID)
7 |
8 | # Get the Admin role security principal
9 | $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
10 |
11 | # Are we an admin role?
12 | if ($windowsPrincipal.IsInRole($adminRole))
13 | {
14 | $true
15 | }
16 | else
17 | {
18 | $false
19 | }
20 | }
21 |
22 | if (!$IsWindows) {
23 | Write-Host -ForegroundColor Yellow "This script should only be run on Windows"
24 | return
25 | }
26 |
27 | if (!(Test-IsAdmin)) {
28 | Write-Host -ForegroundColor Yellow "This script should only be run as an admin"
29 | return
30 | }
31 |
32 | # work out which version of .NET we should be running (based upon global.json)
33 | Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser
34 | $globalJson = Get-Content -Raw -Path 'global.json' | ConvertFrom-Json
35 | $dotnetVersion = $globalJson.sdk.version
36 |
37 | Write-Host -ForegroundColor Green "Found .NET SDK version '$dotnetVersion'"
38 |
39 | $dotnetPath = Join-Path $env:LOCALAPPDATA "Microsoft" "dotnet"
40 | if (!(Test-Path -Path $dotnetPath -PathType Container)) {
41 | Write-Host -ForegroundColor Green "Creating '$dotnetPath'"
42 | New-Item -ItemType Directory -Path $dotnetPath
43 | }
44 |
45 | # check to see if the installation root is in path _above_ the system-wide installation path
46 | $machinePath = [Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Machine)
47 | $paths = $machinePath.Split(';')
48 | $systemIndex = $paths.IndexOf((Join-Path $env:ProgramFiles "dotnet" "\"))
49 | $userIndex = $paths.IndexOf($dotnetPath)
50 | if ($userIndex -eq -1 -or $systemIndex -lt $userIndex) {
51 | Write-Host -ForegroundColor Green "Updating PATH to include '$dotnetPath'"
52 |
53 | [Environment]::SetEnvironmentVariable("Path", $dotnetPath + ";" + $machinePath, [System.EnvironmentVariableTarget]::Machine)
54 | }
55 |
56 | & ./build/dotnet-install.ps1 -Version $dotnetVersion
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "${workspaceFolder}",
36 | "/property:GenerateFullPaths=true",
37 | "/consoleloggerparameters:NoSummary"
38 | ],
39 | "problemMatcher": "$msCompile"
40 | },
41 | {
42 | "label": "RaspberryPiPublish",
43 | "command": "dotnet",
44 | "type": "process",
45 | "args": [
46 | "publish",
47 | "-r:linux-arm",
48 | "-o:bin/linux-arm/publish",
49 | "--self-contained"
50 | ],
51 | "problemMatcher": "$msCompile"
52 | },
53 | {
54 | "label": "RaspberryPiDeploy",
55 | "type": "shell",
56 | "dependsOn": "RaspberryPiPublish",
57 | "presentation": {
58 | "reveal": "always",
59 | "panel": "new"
60 | },
61 | "command": "rsync -rvuz --rsh=ssh bin/linux-arm/publish/ pi@${input:host}:~/src/${workspaceFolderBasename}/",
62 | "problemMatcher": [],
63 | }
64 | ],
65 | "inputs": [
66 | {
67 | "id": "host",
68 | "description": "Raspberry Pi host to deploy to",
69 | "default": "dward-pi",
70 | "type": "promptString"
71 | }
72 | ]
73 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/Serialization/PropertyListSerializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 | using Claunia.PropertyList;
6 |
7 | namespace AirDropAnywhere.Core.Serialization
8 | {
9 | ///
10 | /// Helper class for serializing .NET types to / from property lists.
11 | ///
12 | internal class PropertyListSerializer
13 | {
14 | public const int MaxPropertyListLength = 1024 * 1024; // 1 MiB
15 |
16 | public static async ValueTask DeserializeAsync(Stream stream)
17 | {
18 | // this probably all seems a little convoluted but
19 | // plist-cil works best when it's passed a ReadOnlySpan
20 | // so try to minimize allocations as much as possible in this path
21 | var buffer = ArrayPool.Shared.Rent(MaxPropertyListLength);
22 | try
23 | {
24 | using (var memoryStream = new MemoryStream(buffer, 0, MaxPropertyListLength, true))
25 | {
26 | await stream.CopyToAsync(memoryStream, 4096);
27 | return Deserialize(buffer.AsSpan()[..(int)memoryStream.Position]);
28 | }
29 | }
30 | finally
31 | {
32 | ArrayPool.Shared.Return(buffer);
33 | }
34 | }
35 |
36 | public static T Deserialize(ReadOnlySpan buffer)
37 | {
38 | return PropertyListConverter.ToObject(
39 | PropertyListParser.Parse(buffer)
40 | );
41 | }
42 |
43 | public static async ValueTask SerializeAsync(object obj, Stream stream)
44 | {
45 | var buffer = ArrayPool.Shared.Rent(MaxPropertyListLength);
46 | try
47 | {
48 | using (var memoryStream = new MemoryStream(buffer, true))
49 | {
50 | BinaryPropertyListWriter.Write(
51 | memoryStream, PropertyListConverter.ToNSObject(obj)
52 | );
53 |
54 | await stream.WriteAsync(buffer, 0, (int)memoryStream.Position);
55 | }
56 | }
57 | finally
58 | {
59 | ArrayPool.Shared.Return(buffer);
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/AirDropServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Runtime.InteropServices;
4 | using AirDropAnywhere.Core;
5 | using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
6 | using Microsoft.Extensions.Hosting;
7 |
8 | // ReSharper disable once CheckNamespace
9 | namespace Microsoft.Extensions.DependencyInjection
10 | {
11 | ///
12 | /// Extensions used to add AirDrop services to .
13 | ///
14 | public static class AirDropServiceCollectionExtensions
15 | {
16 | ///
17 | /// Adds AirDrop services to the specified .
18 | ///
19 | /// The to add services to.
20 | /// An to configure the provided .
21 | public static IServiceCollection AddAirDrop(this IServiceCollection services, Action? setupAction = null)
22 | {
23 | if (services == null)
24 | {
25 | throw new ArgumentNullException(nameof(services));
26 | }
27 |
28 | Utils.AssertPlatform();
29 | Utils.AssertNetworkInterfaces();
30 |
31 | services.AddScoped();
32 | services.AddSingleton();
33 | services.AddSingleton(s => s.GetService()!);
34 | services.AddOptions().ValidateDataAnnotations();
35 |
36 | services.Configure(
37 | x =>
38 | {
39 | // on macOS, ensure we listen on the awdl0 interface
40 | // by setting the SO_RECV_ANYIF socket option
41 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
42 | {
43 | x.CreateBoundListenSocket = endpoint =>
44 | {
45 | var socket = SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint);
46 | if (endpoint is IPEndPoint)
47 | {
48 | socket.SetAwdlSocketOption();
49 | }
50 | return socket;
51 | };
52 | }
53 | });
54 |
55 | if (setupAction != null)
56 | {
57 | services.Configure(setupAction);
58 | }
59 |
60 | return services;
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AirDrop Anywhere
2 |
3 | > **NOTE** This project is still a work in progress - some of the details below are the intended end state of the project, not the current state! More information can be found at my blog at https://bakedbean.org.uk/tags/airdrop/
4 |
5 | A .NET Core implementation of the AirDrop protocol that allows arbitrary devices to send/receive files from devices that support AirDrop natively (i.e. Apple devices).
6 |
7 | ## Overview
8 |
9 | AirDropAnywhere implements the mDNS listener and HTTP API needed to handle the AirDrop protocol in such a way that they can be consumed by any application. However, in order to be able to successfully use AirDrop the hardware executing these components needs to support Apple Wireless Direct Link (AWDL) or Open Wireless Link ([OWL](https://owlink.org/)) - that usually means an Apple device or Linux with a wireless interface that supports RFMON.
10 |
11 | It also exposes a web server that serves up a website that can be used by arbitrary devices that do not support AirDrop natively to send/receive files to/from devices that do support AirDrop.
12 |
13 | ## Structure
14 |
15 | AirDropAnywhere is split into several projects:
16 |
17 | `AirDropAnywhere.Core` contains all our shared services (e.g. mDNS listener, core AirDrop HTTP API) and the means to configure them in a `WebHost`. This is exposed as a NuGet package called `AirDropAnywhere`.
18 |
19 | `AirDropAnywhere.Cli` is a CLI application hosting the services and rendered using [Spectre.Console](https://github.com/spectreconsole/spectre.console). It'll typically be used as a way to send / receive for the current machine via the command line.
20 |
21 | `AirDropAnywhere.Web` hosts the services and exposes a website that any device in the same network can connect to and be able to send/receive files to/from AirDrop-compatible devices. It makes use of Vue.js for its UI and SignalR for realtime communication needed for the backend to function.
22 |
23 | ## Components
24 |
25 | ### mDNS
26 | mDNS is implemented as an `IHostedService` that executes as part of the `WebHost`. Its sole purpose is to dynamically advertise each device that wants to send/receive files using AirDrop. It listens on IPv6 and IPv4 multicast on UDP 5353 and responds to mDNS queries over the AWDL interface exposed by Apple hardware or OWL's virtual interface.
27 |
28 | Bulk of the implementation is in the [MulticastDns](https://github.com/deanward81/AirDropAnywhere/tree/main/src/AirDropAnywhere.Core/MulticastDns) folder.
29 |
30 | ### HTTP API
31 | AirDrop is implemented as an HTTP API that ties into Kestrel using endpoint routing (i.e. no MVC, etc.). [AirDropRouteHandler](https://github.com/deanward81/AirDropAnywhere/tree/main/src/AirDropAnywhere.Core/AirDropRouteHandler.cs) implements the protocol.
32 |
33 | _TODO_ finish README!
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Core/AirDropEndpointRouteBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AirDropAnywhere.Core;
3 | using Microsoft.AspNetCore.Routing;
4 |
5 | // ReSharper disable once CheckNamespace
6 | namespace Microsoft.AspNetCore.Builder
7 | {
8 | ///
9 | /// Provides extension methods for to add an AirDrop service.
10 | ///
11 | public static class AirDropEndpointRouteBuilderExtensions
12 | {
13 | ///
14 | /// Adds an AirDrop endpoint to the .
15 | ///
16 | /// The to add the AirDrop endpoint to.
17 | /// A convention routes for the AirDrop endpoint.
18 | public static IEndpointRouteBuilder MapAirDrop(
19 | this IEndpointRouteBuilder endpoints
20 | )
21 | {
22 | if (endpoints == null)
23 | {
24 | throw new ArgumentNullException(nameof(endpoints));
25 | }
26 |
27 | return MapAirDropCore(endpoints, null);
28 | }
29 |
30 | ///
31 | /// Adds an AirDrop endpoint to the with the specified options.
32 | ///
33 | /// The to add the AirDrop endpoints to.
34 | /// A used to configure AirDrop.
35 | /// A convention routes for the AirDrop endpoints.
36 | public static IEndpointRouteBuilder MapAirDrop(
37 | this IEndpointRouteBuilder endpoints,
38 | AirDropOptions options
39 | )
40 | {
41 | if (endpoints == null)
42 | {
43 | throw new ArgumentNullException(nameof(endpoints));
44 | }
45 |
46 | if (options == null)
47 | {
48 | throw new ArgumentNullException(nameof(options));
49 | }
50 |
51 | return MapAirDropCore(endpoints, options);
52 | }
53 |
54 | private static IEndpointRouteBuilder MapAirDropCore(IEndpointRouteBuilder endpoints, AirDropOptions? options)
55 | {
56 | Utils.AssertPlatform();
57 | Utils.AssertNetworkInterfaces();
58 |
59 | if (endpoints.ServiceProvider.GetService(typeof(AirDropService)) == null)
60 | {
61 | throw new InvalidOperationException(
62 | "Unable to find services: make sure to call AddAirDrop in your ConfigureServices(...) method!"
63 | );
64 | }
65 |
66 | endpoints.MapPost("Discover", ctx => AirDropRouteHandler.ExecuteAsync(ctx, r => r.DiscoverAsync()));
67 | endpoints.MapPost("Ask", ctx => AirDropRouteHandler.ExecuteAsync(ctx, r => r.AskAsync()));
68 | endpoints.MapPost("Upload", ctx => AirDropRouteHandler.ExecuteAsync(ctx, r => r.UploadAsync()));
69 | return endpoints;
70 | }
71 | }
72 |
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/restore.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors.
4 | # See if stdout is a terminal
5 | if [ -t 1 ] && command -v tput > /dev/null; then
6 | # see if it supports colors
7 | ncolors=$(tput colors)
8 | if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then
9 | bold="$(tput bold || echo)"
10 | normal="$(tput sgr0 || echo)"
11 | black="$(tput setaf 0 || echo)"
12 | red="$(tput setaf 1 || echo)"
13 | green="$(tput setaf 2 || echo)"
14 | yellow="$(tput setaf 3 || echo)"
15 | blue="$(tput setaf 4 || echo)"
16 | magenta="$(tput setaf 5 || echo)"
17 | cyan="$(tput setaf 6 || echo)"
18 | white="$(tput setaf 7 || echo)"
19 | fi
20 | fi
21 |
22 | SCRIPT_PATH=$(cd "$(dirname "$0")"; pwd -P)
23 | USER_HOME=$(eval echo ~${SUDO_USER})
24 | sayError() {
25 | printf "%b\n" "${red:-}Error: $1${normal:-}"
26 | }
27 |
28 | sayWarning() {
29 | printf "%b\n" "${yellow:-}Warning: $1${normal:-}"
30 | }
31 |
32 | sayInfo() {
33 | printf "%b\n" "${green:-}$1${normal:-}"
34 | }
35 |
36 | updateProfile() {
37 | PROFILE=$HOME/.bashrc
38 | if [ -f "$PROFILE" ]; then
39 | if ! grep -q "^export $1=.*$" "$PROFILE"; then
40 | echo "export $1=$2" >> $PROFILE
41 | else
42 | sed -i '' 's;^export '"$1"'=.*$;export '"$1"'='"$2"';' "$PROFILE"
43 | fi
44 | fi
45 |
46 | PROFILE=$HOME/.zshrc
47 | if [ ! -f "$PROFILE" ]; then
48 | # create a .zshrc if one doesn't exist
49 | touch "$PROFILE"
50 | chown "$SUDO_USER:staff" "$PROFILE"
51 | fi
52 | if ! grep -q "^export $1=.*$" "$PROFILE"; then
53 | echo "export $1=$2" >> $PROFILE
54 | else
55 | sed -i '' 's;^export '"$1"'=.*$;export '"$1"'='"$2"';' "$PROFILE"
56 | fi
57 | }
58 |
59 | installDotNet() {
60 | sayInfo "Installing .NET SDK $DOTNET_VERSION..."
61 | # gotta download and install from scratch
62 | chmod 755 "$SCRIPT_PATH/build/dotnet-install.sh"
63 | "$SCRIPT_PATH/build/dotnet-install.sh" --version $DOTNET_VERSION --no-path --install-dir "$DOTNET_ROOT"
64 | if [ $? -ne 0 ]; then
65 | sayError "Error installing .NET SDK"
66 | exit 1
67 | fi
68 |
69 | sayInfo "Installed .NET SDK $DOTNET_VERSION!"
70 | }
71 |
72 | # make sure we're running elevated
73 | if [ $EUID -ne 0 ]; then
74 | sayWarning "Script must be run in an elevated context. Running using sudo"
75 | sudo env HOME=$USER_HOME $0
76 | exit 0
77 | fi
78 |
79 | # check if JQ is installed
80 | if [ ! -x "$(which jq)" ]; then
81 | sayError "jq is not installed. See https://stedolan.github.io/jq/download/"
82 | exit 1
83 | fi
84 |
85 | # check if dotnet is installed
86 | DOTNET_EXE=$(which dotnet)
87 | DOTNET_VERSION=$(cat global.json | jq --raw-output '.sdk.version')
88 | export DOTNET_ROOT=/usr/local/share/dotnet
89 | export PATH=$PATH:$DOTNET_ROOT
90 | if [ ! -x "$DOTNET_EXE" ]; then
91 | installDotNet
92 | else
93 | # make sure it's the latest build
94 | DOTNET_SDKS=$(dotnet --list-sdks | grep "$DOTNET_VERSION")
95 | if [ -z "$DOTNET_SDKS" ]; then
96 | sayWarning ".NET Core SDK $DOTNET_VERSION not installed"
97 | installDotNet
98 | fi
99 | fi
100 |
101 | # update the path
102 | updateProfile "DOTNET_ROOT" "$DOTNET_ROOT"
103 | updateProfile "PATH" "\$PATH:\$DOTNET_ROOT"
--------------------------------------------------------------------------------
/src/AirDropAnywhere.Cli/Hubs/PolymorphicJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Text.Json;
7 | using System.Text.Json.Serialization;
8 |
9 | namespace AirDropAnywhere.Cli.Hubs
10 | {
11 | ///
12 | /// Converter that ensures that the provided types are serialized/deserialized
13 | /// in a way that maintains type information over the wire.
14 | ///
15 | internal class PolymorphicJsonConverter : JsonConverter