├── .github
├── FUNDING.yml
└── workflows
│ └── create-release.yml
├── global.json
├── SharpIppNextServer
├── appsettings.Development.json
├── appsettings.Production.json
├── SharpIppNextServer.http
├── Services
│ ├── IDateTimeProvider.cs
│ ├── DateTimeProvider.cs
│ ├── IDateTimeOffsetProvider.cs
│ ├── DateTimeOffsetProvider.cs
│ ├── SecurityHeadersMiddleware.cs
│ ├── JobService.cs
│ └── PrinterService.cs
├── appsettings.json
├── SharpIppNextServer.csproj
├── Properties
│ └── launchSettings.json
├── Program.cs
├── Models
│ ├── PrinterOptions.cs
│ └── PrinterJob.cs
└── THIRD-PARTY-NOTICES.txt
├── Setup
├── Update-ThirdPartyNotices.ps1
├── Remove-Printer.ps1
├── Update-OutdatedPackage.ps1
└── Add-Printer.ps1
├── NuGet.Config
├── LICENSE.txt
├── SharpIppNextServer.sln
├── README.md
├── Directory.Packages.props
├── .gitattributes
├── .gitignore
└── .editorconfig
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: danielklecha
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.0",
4 | "allowPrerelease": false,
5 | "rollForward": "latestMinor"
6 | }
7 | }
--------------------------------------------------------------------------------
/SharpIppNextServer/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Setup/Update-ThirdPartyNotices.ps1:
--------------------------------------------------------------------------------
1 | Push-Location
2 | cd "$PSScriptRoot\..\SharpIppNextServer"
3 | dotnet-thirdpartynotices --output-filename "THIRD-PARTY-NOTICES.txt"
4 | Pop-Location
--------------------------------------------------------------------------------
/SharpIppNextServer/appsettings.Production.json:
--------------------------------------------------------------------------------
1 | {
2 | "Kestrel": {
3 | "Endpoints": {
4 | "Http": {
5 | "Url": "http://0.0.0.0:631"
6 | }
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/SharpIppNextServer/SharpIppNextServer.http:
--------------------------------------------------------------------------------
1 | @HostAddress = http://localhost:631
2 |
3 | GET {{HostAddress}}/
4 | Accept: application/json
5 |
6 | ###
7 |
8 | POST {{HostAddress}}/
9 | Accept: application/ipp
--------------------------------------------------------------------------------
/SharpIppNextServer/Services/IDateTimeProvider.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace SharpIppNextServer.Services
3 | {
4 | public interface IDateTimeProvider
5 | {
6 | DateTime Now { get; }
7 | DateTime UtcNow { get; }
8 | }
9 | }
--------------------------------------------------------------------------------
/SharpIppNextServer/Services/DateTimeProvider.cs:
--------------------------------------------------------------------------------
1 | namespace SharpIppNextServer.Services;
2 |
3 | public class DateTimeProvider : IDateTimeProvider
4 | {
5 | public DateTime UtcNow => DateTime.UtcNow;
6 | public DateTime Now => DateTime.Now;
7 | }
8 |
--------------------------------------------------------------------------------
/SharpIppNextServer/Services/IDateTimeOffsetProvider.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace SharpIppNextServer.Services
3 | {
4 | public interface IDateTimeOffsetProvider
5 | {
6 | DateTimeOffset Now { get; }
7 | DateTimeOffset UtcNow { get; }
8 | }
9 | }
--------------------------------------------------------------------------------
/SharpIppNextServer/Services/DateTimeOffsetProvider.cs:
--------------------------------------------------------------------------------
1 | namespace SharpIppNextServer.Services;
2 |
3 | public class DateTimeOffsetProvider : IDateTimeOffsetProvider
4 | {
5 | public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
6 | public DateTimeOffset Now => DateTimeOffset.Now;
7 | }
--------------------------------------------------------------------------------
/SharpIppNextServer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.Hosting.Lifetime": "Information",
6 | "Microsoft.AspNetCore": "Warning",
7 | "Quartz": "Warning"
8 | }
9 | },
10 | "Printer": {
11 | "Name": "SharpIppNext"
12 | },
13 | "AllowedHosts": "*"
14 | }
15 |
--------------------------------------------------------------------------------
/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/SharpIppNextServer/Services/SecurityHeadersMiddleware.cs:
--------------------------------------------------------------------------------
1 | namespace SharpIppNextServer.Services;
2 |
3 | public class SecurityHeadersMiddleware(RequestDelegate next)
4 | {
5 | public async Task InvokeAsync(HttpContext httpContext)
6 | {
7 | httpContext.Response.Headers.TryAdd("X-Frame-Options", "DENY");
8 | httpContext.Response.Headers.TryAdd("Content-Security-Policy", $"default-src 'none';");
9 | httpContext.Response.Headers.TryAdd("X-Content-Type-Options", "nosniff");
10 | httpContext.Response.Headers.TryAdd("Referrer-Policy", "strict-origin-when-cross-origin");
11 | httpContext.Response.Headers.TryAdd("Permissions-Policy", "camera=(), geolocation=(), microphone=()");
12 | httpContext.Response.Headers.TryAdd("Cross-Origin-Opener-Policy", "unsafe-none");
13 | httpContext.Response.Headers.TryAdd("Cross-Origin-Embedder-Policy", "unsafe-none");
14 | httpContext.Response.Headers.TryAdd("Cross-Origin-Resource-Policy", "same-origin");
15 | await next(httpContext);
16 | }
17 | }
--------------------------------------------------------------------------------
/Setup/Remove-Printer.ps1:
--------------------------------------------------------------------------------
1 | $printerName = "SharpIppNext"
2 |
3 | function Restart-Spooler {
4 | try {
5 | Write-Output "Restarting Spooler service..."
6 | Restart-Service -Name 'Spooler' -ErrorAction Stop
7 | Start-Sleep -Seconds 3
8 | Write-Output "Spooler service restarted successfully."
9 | } catch {
10 | Write-Error "Error restarting Spooler service: $_"
11 | exit 1
12 | }
13 | }
14 |
15 | function Remove-IppPrinter {
16 | if (Get-Printer -Name $printerName -ErrorAction SilentlyContinue) {
17 | try {
18 | Write-Host "Removing printer: $printerName"
19 | Remove-Printer -Name $printerName
20 | Write-Host "Printer $printerName removed successfully."
21 | } catch {
22 | Write-Error "Error adding printer '$printerName': $_"
23 | exit 1
24 | }
25 | }
26 | else {
27 | Write-Output "Printer '$printerName' does not exist."
28 | }
29 | }
30 |
31 | # Execute steps
32 | Restart-Spooler
33 | Remove-IppPrinter
34 |
35 | Write-Output "Printer setup completed successfully."
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Daniel Klecha
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 |
--------------------------------------------------------------------------------
/SharpIppNextServer/SharpIppNextServer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | 1.0.0
8 | win-x64;linux-x64;osx-x64
9 | Daniel Klecha
10 | Daniel Klecha
11 | IPP printer
12 | false
13 | false
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Always
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/SharpIppNextServer.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34616.47
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpIppNextServer", "SharpIppNextServer\SharpIppNextServer.csproj", "{47476576-4D18-497A-9550-083D1760D7CA}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {47476576-4D18-497A-9550-083D1760D7CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {47476576-4D18-497A-9550-083D1760D7CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {47476576-4D18-497A-9550-083D1760D7CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {47476576-4D18-497A-9550-083D1760D7CA}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {32B71D4B-B5BB-4E75-B3B6-145BAC90E384}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/SharpIppNextServer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:631",
8 | "sslPort": 44364
9 | }
10 | },
11 | "profiles": {
12 | "http": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": false,
16 | "launchUrl": "",
17 | "applicationUrl": "http://0.0.0.0:631",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "https": {
23 | "commandName": "Project",
24 | "dotnetRunMessages": true,
25 | "launchBrowser": false,
26 | "launchUrl": "",
27 | "applicationUrl": "https://0.0.0.0:631",
28 | "environmentVariables": {
29 | "ASPNETCORE_ENVIRONMENT": "Development"
30 | }
31 | },
32 | "IIS Express": {
33 | "commandName": "IISExpress",
34 | "launchBrowser": false,
35 | "launchUrl": "",
36 | "environmentVariables": {
37 | "ASPNETCORE_ENVIRONMENT": "Development"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SharpIppNextServer
2 |
3 | [](https://github.com/danielklecha/SharpIppNextServer/releases)
4 | [](https://github.com/danielklecha/SharpIppNextServer/blob/master/LICENSE.txt)
5 |
6 | IPP printer (web app) based on `SharpIppNext` library.
7 |
8 | ## Installation
9 |
10 | The printer should be compatible with any IPP client.
11 |
12 | ### Windows (Printer wizard)
13 |
14 | 1. Open `Printers & scanners`
15 | 2. Click `Add device`
16 | 3. Click `Add a new device manually`
17 | 4. Select `Add a printer using an IP address or hostname`
18 | 5. Select `IPP Device` as device type
19 | 6. Write `http://127.0.0.1:631/` or `https://127.0.0.1:631/` as `Hostname or IP address`
20 | 7. Click `Next` - You should successfully add printer.
21 | 8. Click "Print a test page" (optionally)
22 | 9. Click `Finish`.
23 |
24 | ### Windows (Script)
25 |
26 | ```powershell
27 | Add-Printer -Name "SharpIppNext" -PortName "http://127.0.0.1:631/SharpIppNext" -DriverName "Microsoft Print To PDF"
28 | ```
29 |
30 | All steps are described in `Setup\Add-printer.ps1`
31 |
32 | ### Android
33 |
34 | Use the NetPrinter App.
35 |
36 | ## Requirements
37 |
38 | ASP.NET Core 8.0 Runtime needs to be installed.
39 |
40 | ## Integration testing (OpenSUSE)
41 |
42 | 1. Install package: `sudo zypper install cups-client cups-backends`
43 | 2. Obtain correct IP: `ip route`
44 | 3. Make test: `ipptool -t ipp://172.25.192.1 ipp-1.1.test`
45 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/SharpIppNextServer/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Server.Kestrel.Core;
2 | using Microsoft.Extensions.Options;
3 | using Quartz;
4 | using SharpIpp;
5 | using SharpIppNextServer.Models;
6 | using SharpIppNextServer.Services;
7 | using System.IO.Abstractions;
8 |
9 | var builder = WebApplication.CreateBuilder(args);
10 | builder.Services
11 | .AddSingleton()
12 | .AddSingleton()
13 | .AddSingleton()
14 | .AddSingleton()
15 | .Configure(options => options.AllowSynchronousIO = true)
16 | .Configure(options => options.AllowSynchronousIO = true)
17 | .Configure(builder.Configuration.GetSection("Printer"))
18 | .AddSingleton()
19 | .AddHttpContextAccessor()
20 | .AddCors()
21 | .AddQuartz(q =>
22 | {
23 | var jobKey = new JobKey("printerQueue");
24 | q.AddJob(opts => opts.WithIdentity(jobKey));
25 | q.AddTrigger(opts => opts
26 | .ForJob(jobKey)
27 | .WithIdentity($"printerQueue-trigger")
28 | .WithCronSchedule("0/10 * * * * ?"));
29 | })
30 | .AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
31 | var app = builder.Build();
32 | var printerOptions = app.Services.GetRequiredService>().Value;
33 | app.UseMiddleware();
34 | app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
35 |
36 | new List
37 | {
38 | "/",
39 | "/ipp",
40 | $"/{printerOptions.Name}",
41 | "/ipp/printer",
42 | $"/ipp/printer/{printerOptions.Name}"
43 | }.ForEach(path =>
44 | {
45 | app.MapGet(path, () => "IPP printer");
46 | app.MapPost(path, async (HttpContext context, PrinterService printerService) =>
47 | {
48 | context.Response.ContentType = "application/ipp";
49 | await printerService.ProcessRequestAsync(context.Request.Body, context.Response.Body);
50 | });
51 | });
52 |
53 | app.Run();
--------------------------------------------------------------------------------
/Setup/Update-OutdatedPackage.ps1:
--------------------------------------------------------------------------------
1 | Push-Location "$PSScriptRoot\.."
2 | $jsonOutput = dotnet list package --include-transitive --outdated --format json
3 | $parsedJson = $jsonOutput | ConvertFrom-Json
4 |
5 | # Initialize an empty dictionary
6 | $packageVersions = @{}
7 |
8 | # Iterate over each project
9 | foreach ($project in $parsedJson.projects) {
10 | foreach ($framework in $project.frameworks) {
11 | foreach ($package in $framework.topLevelPackages) {
12 | if ($package.latestVersion -match '^\d' -and -not $packageVersions.ContainsKey($package.id)) {
13 | $packageVersions[$package.id] = $package.latestVersion
14 | }
15 | }
16 | foreach ($package in $framework.transitivePackages) {
17 | if ($package.latestVersion -match '^\d' -and -not $packageVersions.ContainsKey($package.id)) {
18 | $packageVersions[$package.id] = $package.latestVersion
19 | }
20 | }
21 | }
22 | }
23 |
24 | # Output the dictionary
25 | $packageVersions
26 |
27 | # Determine the script's directory
28 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
29 |
30 | # Path to the XML file in the parent directory
31 | $xmlFilePath = Join-Path -Path (Split-Path -Parent $scriptDir) -ChildPath "Directory.Packages.props"
32 |
33 | # Load the XML file
34 | [xml]$xml = Get-Content $xmlFilePath
35 |
36 | # Iterate over the dictionary to update or add PackageVersion elements
37 | foreach ($packageId in $packageVersions.Keys) {
38 | $found = $false
39 |
40 | # Update the existing package version
41 | foreach ($package in $xml.Project.ItemGroup.PackageVersion) {
42 | if ($package.Include -eq $packageId) {
43 | $package.Version = $packageVersions[$packageId]
44 | $found = $true
45 | break
46 | }
47 | }
48 |
49 | # Add a new package version if it wasn't found
50 | if (-not $found) {
51 | $newPackage = $xml.CreateElement("PackageVersion")
52 | $newPackage.SetAttribute("Include", $packageId)
53 | $newPackage.SetAttribute("Version", $packageVersions[$packageId])
54 | $xml.Project.ItemGroup.AppendChild($newPackage) | Out-Null
55 | }
56 | }
57 |
58 | # Save the modified XML file
59 | $xml.Save($xmlFilePath)
60 | Pop-Location
--------------------------------------------------------------------------------
/.github/workflows/create-release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v**'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v4
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v4
18 | with:
19 | dotnet-version: 8.0.x
20 | - name: Replace Version in csproj files
21 | run: |
22 | tag=$(echo "${{ github.ref_name }}" | sed 's/^v//')
23 | find . -type f -name "*.csproj" -exec sed -i "s/.*<\/Version>/${tag}<\/Version>/g" {} \;
24 | - name: Install zip
25 | uses: montudor/action-zip@v1
26 | - name: Restore dependencies
27 | run: dotnet restore
28 | - name: Publish win-x64
29 | run: dotnet publish --no-restore --configuration Release --runtime win-x64 --no-self-contained
30 | - name: Copy license win-x64
31 | run: |
32 | cp LICENSE.txt SharpIppNextServer/bin/Release/net8.0/win-x64/publish/LICENSE.txt
33 | - name: Pack win-x64
34 | run: zip -qq -r "../../../SharpIppNextServer-${{github.ref_name}}-win-x64.zip" *.*
35 | working-directory: SharpIppNextServer/bin/Release/net8.0/win-x64/publish/
36 | - name: Publish linux-x64
37 | run: dotnet publish --no-restore --configuration Release --runtime linux-x64 --no-self-contained
38 | - name: Copy license linux-x64
39 | run: |
40 | cp LICENSE.txt SharpIppNextServer/bin/Release/net8.0/linux-x64/publish/LICENSE.txt
41 | - name: Pack linux-x64
42 | run: zip -qq -r ../../../SharpIppNextServer-${{github.ref_name}}-linux-x64.zip *.*
43 | working-directory: SharpIppNextServer/bin/Release/net8.0/linux-x64/publish/
44 | - name: Publish osx-x64
45 | run: dotnet publish --no-restore --configuration Release --runtime osx-x64 --no-self-contained
46 | - name: Copy license osx-x64
47 | run: |
48 | cp LICENSE.txt SharpIppNextServer/bin/Release/net8.0/osx-x64/publish/LICENSE.txt
49 | - name: Pack osx-x64
50 | run: zip -qq -r ../../../SharpIppNextServer-${{github.ref_name}}-osx-x64.zip *.*
51 | working-directory: SharpIppNextServer/bin/Release/net8.0/osx-x64/publish/
52 | - name: Create GitHub Release
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 | run: |
56 | tag=${{ github.ref_name }}
57 | gh release create "$tag" --generate-notes SharpIppNextServer/bin/Release/*.zip
58 |
--------------------------------------------------------------------------------
/SharpIppNextServer/Models/PrinterOptions.cs:
--------------------------------------------------------------------------------
1 | using SharpIpp.Protocol.Models;
2 |
3 | namespace SharpIppNextServer.Models;
4 |
5 | public class PrinterOptions
6 | {
7 | public string Name { get; set; } = "SharpIppNext";
8 | public string DnsSdName { get; set; } = "SharpIppNext [231076]";
9 | public Guid UUID { get; set; } = new Guid("d178b387-3a93-4d17-a561-007f876c4901");
10 | public string FirmwareName { get; set; } = "SIN22183498";
11 | public Sides[] Sides { get; set; } = [SharpIpp.Protocol.Models.Sides.OneSided];
12 | public PrintScaling[] PrintScaling { get; set; } = [SharpIpp.Protocol.Models.PrintScaling.Auto];
13 | public string[] Media { get; set; } = [
14 | "iso_a4_210x297mm",
15 | "na_executive_7.25x10.5in",
16 | "na_letter_8.5x11in",
17 | "na_legal_8.5x14in",
18 | "na_govt-letter_8x10in",
19 | "na_invoice_5.5x8.5in",
20 | "iso_a5_148x210mm",
21 | "jis_b5_182x257mm",
22 | "jpn_hagaki_100x148mm",
23 | "iso_a6_105x148mm",
24 | "na_index-4x6_4x6in",
25 | "na_index-5x8_5x8in",
26 | "na_index-3x5_3x5in",
27 | "na_monarch_3.875x7.5in",
28 | "na_number-10_4.125x9.5in",
29 | "iso_dl_110x220mm",
30 | "iso_c5_162x229mm",
31 | "iso_c6_114x162mm",
32 | "na_a2_4.375x5.75in",
33 | "jpn_chou3_120x235mm",
34 | "jpn_chou4_90x205mm",
35 | "oe_photo-l_3.5x5in",
36 | "jpn_photo-2l_127x177.8mm",
37 | "na_5x7_5x7in",
38 | "oe_photo_4x5in",
39 | "na_personal_3.625x6.5in",
40 | "iso_b5_176x250mm",
41 | "om_small-photo_100x150mm",
42 | "na_foolscap_8.5x13in",
43 | "custom_min_3x5in",
44 | "custom_max_8.5x14in",
45 | "stationery",
46 | "photographic-glossy"
47 | ];
48 | public Resolution[] Resolution { get; set; } = [new(600, 600, ResolutionUnit.DotsPerInch)];
49 | public Finishings[] Finishings { get; set; } = [SharpIpp.Protocol.Models.Finishings.None];
50 | public PrintQuality[] PrintQuality { get; set; } = [SharpIpp.Protocol.Models.PrintQuality.High];
51 | public int JobPriority { get; set; } = 1;
52 | public int Copies { get; set; } = 1;
53 | public Orientation Orientation { get; set; } = Orientation.Portrait;
54 | public JobHoldUntil JobHoldUntil { get; set; } = JobHoldUntil.NoHold;
55 | public string DocumentFormat { get; set; } = "application/pdf";
56 | public string[] OutputBin { get; set; } = ["top"];
57 | public PrintColorMode[] PrintColorModes { get; set; } = [PrintColorMode.Color];
58 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/Setup/Add-Printer.ps1:
--------------------------------------------------------------------------------
1 | $driverName = "Microsoft Print To PDF"
2 | $printerName = "SharpIppNext"
3 | $ipAddress = "127.0.0.1"
4 | $portNumber = 631
5 | $printerUrl = "http://$ipAddress`:$portNumber/$printerName"
6 |
7 | function Import-Modules {
8 | if ($PSVersionTable.PSVersion.Major -ge 7) {
9 | Write-Output "Detected PowerShell 7. Importing DISM module..."
10 | try {
11 | Import-Module DISM -UseWindowsPowerShell -ErrorAction Stop -WarningAction SilentlyContinue
12 | Write-Output "DISM module imported successfully."
13 | } catch {
14 | Write-Error "Failed to import DISM module. Ensure PowerShell 5.1 is available."
15 | exit 1
16 | }
17 | }
18 | }
19 |
20 | function Install-WindowsFeature {
21 | try {
22 | Write-Output "Checking Internet Print Client feature status..."
23 | $features = @("Printing-InternetPrinting-Client", "Printing-Foundation-InternetPrinting-Client")
24 | foreach ($featureName in $features) {
25 | $feature = Get-WindowsOptionalFeature -Online -FeatureName $featureName -ErrorAction SilentlyContinue
26 | if ($feature -and $feature.State -eq "Enabled") {
27 | Write-Output "$featureName is already enabled."
28 | return
29 | }
30 | if ($feature) {
31 | Write-Output "Enabling $featureName..."
32 | Enable-WindowsOptionalFeature -Online -FeatureName $featureName -NoRestart -ErrorAction Stop
33 | Write-Output "$featureName has been installed successfully."
34 | return
35 | }
36 | }
37 | Write-Error "Required features not found. Ensure this is a supported Windows version."
38 | exit 1
39 | } catch {
40 | Write-Error "Error enabling Internet Print Client feature: $_"
41 | exit 1
42 | }
43 | }
44 |
45 | function Restart-Spooler {
46 | try {
47 | Write-Output "Restarting Spooler service..."
48 | Restart-Service -Name 'Spooler' -Force -ErrorAction Stop
49 | Start-Sleep -Seconds 3
50 | Write-Output "Spooler service restarted successfully."
51 | } catch {
52 | Write-Error "Failed to restart Spooler service: $_"
53 | exit 1
54 | }
55 | }
56 |
57 | function Install-PrintDriver {
58 | if (Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue) {
59 | Write-Output "Printer driver '$driverName' is already installed."
60 | return
61 | }
62 | try {
63 | Write-Output "Installing print driver: $driverName..."
64 | Add-PrinterDriver -Name $driverName -ErrorAction Stop
65 | Write-Output "Print driver installed successfully."
66 | } catch {
67 | Write-Error "Error installing print driver: $_"
68 | exit 1
69 | }
70 | }
71 |
72 | function Add-IppPrinter {
73 | if (Get-Printer -Name $printerName -ErrorAction SilentlyContinue) {
74 | Write-Output "Printer '$printerName' already exists."
75 | return
76 | }
77 | try {
78 | Write-Output "Adding printer: $printerName..."
79 | Add-Printer -Name $printerName -PortName $printerUrl -DriverName $driverName -ErrorAction Stop
80 | Write-Output "Printer '$printerName' added successfully."
81 | } catch {
82 | Write-Error "Error adding printer '$printerName': $_"
83 | exit 1
84 | }
85 | }
86 |
87 | # Execute steps
88 | Import-Modules
89 | Install-WindowsFeature
90 | Restart-Spooler
91 | Install-PrintDriver
92 | Add-IppPrinter
93 |
94 | Write-Output "Printer setup completed successfully."
--------------------------------------------------------------------------------
/SharpIppNextServer/Services/JobService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.StaticFiles;
2 | using Quartz;
3 | using SharpIpp.Models;
4 | using SharpIpp.Protocol.Models;
5 | using System.IO.Abstractions;
6 |
7 | namespace SharpIppNextServer.Services;
8 |
9 | public class JobService(
10 | PrinterService printerService,
11 | IWebHostEnvironment env,
12 | IFileSystem fileSystem) : IJob
13 | {
14 | private readonly FileExtensionContentTypeProvider _contentTypeProvider = new();
15 |
16 | public async Task Execute(IJobExecutionContext context)
17 | {
18 | var job = await printerService.GetPendingJobAsync();
19 | if (job == null)
20 | return;
21 | try
22 | {
23 | for (var i = 0; i < job.Requests.Count; i++)
24 | {
25 | var prefix = $"{job.Id}.{i}";
26 | switch (job.Requests[i])
27 | {
28 | case PrintJobRequest printJobRequest:
29 | await SaveAsync(prefix, printJobRequest);
30 | break;
31 | case SendDocumentRequest sendJobRequest:
32 | await SaveAsync(prefix, sendJobRequest);
33 | break;
34 | case SendUriRequest sendUriRequest:
35 | await SaveAsync(prefix, sendUriRequest);
36 | break;
37 | }
38 | }
39 | await printerService.AddCompletedJobAsync(job.Id);
40 | }
41 | catch (Exception ex)
42 | {
43 | await printerService.AddAbortedJobAsync(job.Id, ex);
44 | }
45 | }
46 |
47 | private async Task SaveAsync(string prefix, PrintJobRequest request)
48 | {
49 | if (request.Document == null)
50 | return;
51 | if(request.Document.Position > 0)
52 | request.Document.Seek(0, SeekOrigin.Begin);
53 | await SaveAsync(request.Document, GetFileName(prefix, request.OperationAttributes?.DocumentName, request.OperationAttributes?.DocumentFormat));
54 | await request.Document.DisposeAsync();
55 | }
56 |
57 | private async Task SaveAsync(string prefix, SendDocumentRequest request)
58 | {
59 | if (request.Document == null)
60 | return;
61 | request.Document.Seek(0, SeekOrigin.Begin);
62 | await SaveAsync(request.Document, GetFileName(prefix, request.OperationAttributes?.DocumentName, request.OperationAttributes?.DocumentFormat));
63 | await request.Document.DisposeAsync();
64 | }
65 |
66 | private async Task SaveAsync(string prefix, SendUriRequest request)
67 | {
68 | if (request.OperationAttributes is null || request.OperationAttributes.DocumentUri is null)
69 | return;
70 | using var client = new HttpClient();
71 | using var result = await client.GetAsync(request.OperationAttributes.DocumentUri);
72 | if (!result.IsSuccessStatusCode)
73 | return;
74 | using var stream = await result.Content.ReadAsStreamAsync();
75 | await SaveAsync(stream, GetFileName(prefix, request.OperationAttributes.DocumentName, request.OperationAttributes.DocumentFormat, fileSystem.Path.GetFileNameWithoutExtension(request.OperationAttributes.DocumentUri.LocalPath), fileSystem.Path.GetExtension(request.OperationAttributes.DocumentUri.LocalPath)));
76 | }
77 |
78 | private string GetFileName(string prefix, string? documentName, string? documentFormat, string? alternativeDocumentName = null, string? alternativeExtension = null)
79 | {
80 | var extension = documentFormat is null
81 | ? null
82 | : _contentTypeProvider.Mappings.Where(x => x.Value == documentFormat).Select(x => x.Key).FirstOrDefault();
83 | return $"{prefix}_{documentName ?? alternativeDocumentName ?? "no-name"}{extension ?? alternativeExtension ?? ".unknown"}";
84 | }
85 |
86 | private async Task SaveAsync(Stream stream, string fileName)
87 | {
88 | var path = fileSystem.Path.Combine(env.ContentRootPath, "jobs", fileName);
89 | fileSystem.Directory.CreateDirectory(fileSystem.Path.Combine(env.ContentRootPath, "jobs"));
90 | using var fileStream = fileSystem.FileStream.New(path, FileMode.OpenOrCreate);
91 | await stream.CopyToAsync(fileStream);
92 | }
93 | }
--------------------------------------------------------------------------------
/SharpIppNextServer/Models/PrinterJob.cs:
--------------------------------------------------------------------------------
1 | using SharpIpp.Models;
2 | using SharpIpp.Protocol.Models;
3 |
4 | namespace SharpIppNextServer.Models;
5 |
6 | public class PrinterJob : IEquatable, IDisposable, IAsyncDisposable
7 | {
8 | private bool disposedValue;
9 |
10 | public PrinterJob(int id, string? userName, DateTimeOffset createdDateTime)
11 | {
12 | Id = id;
13 | UserName = userName;
14 | CreatedDateTime = createdDateTime;
15 | }
16 |
17 | ///
18 | /// Create shallow copy of the original object
19 | ///
20 | ///
21 | public PrinterJob(PrinterJob printerJob)
22 | {
23 | Id = printerJob.Id;
24 | UserName = printerJob.UserName;
25 | CreatedDateTime = printerJob.CreatedDateTime;
26 | CompletedDateTime = printerJob.CompletedDateTime;
27 | ProcessingDateTime = printerJob.ProcessingDateTime;
28 | State = printerJob.State;
29 | Requests = printerJob.Requests;
30 | }
31 |
32 | public int Id { get; }
33 | public JobState? State { get; private set; }
34 | public string? UserName { get; }
35 | public List Requests { get; set; } = [];
36 | public DateTimeOffset CreatedDateTime { get; }
37 | public DateTimeOffset? CompletedDateTime { get; set; }
38 | public DateTimeOffset? ProcessingDateTime { get; set; }
39 | public bool IsNew => !State.HasValue && !ProcessingDateTime.HasValue;
40 | public bool IsHold => !State.HasValue && ProcessingDateTime.HasValue;
41 |
42 | public bool Equals(PrinterJob? other)
43 | {
44 | return other != null
45 | && Id == other.Id
46 | && State == other.State
47 | && UserName == other.UserName
48 | && CreatedDateTime == other.CreatedDateTime
49 | && CompletedDateTime == other.CompletedDateTime
50 | && ProcessingDateTime == other.ProcessingDateTime
51 | && other.Requests.SequenceEqual(Requests);
52 | }
53 |
54 | public override bool Equals(object? obj)
55 | {
56 | return ReferenceEquals(this, obj) || obj is PrinterJob other && Equals(other);
57 | }
58 |
59 | public override int GetHashCode()
60 | {
61 | return HashCode.Combine(Id, State, Requests);
62 | }
63 |
64 | public static bool operator ==(PrinterJob? left, PrinterJob? right)
65 | {
66 | return Equals(left, right);
67 | }
68 |
69 | public static bool operator !=(PrinterJob? left, PrinterJob? right)
70 | {
71 | return !Equals(left, right);
72 | }
73 |
74 | public async Task TrySetStateAsync(JobState? state, DateTimeOffset dateTime)
75 | {
76 | switch (state)
77 | {
78 | case null when !State.HasValue || State == JobState.Pending:
79 | State = state;
80 | return true;
81 | case JobState.Pending when !State.HasValue || State == JobState.Aborted:
82 | State = state;
83 | return true;
84 | case JobState.Processing when State == JobState.Pending:
85 | State = state;
86 | ProcessingDateTime = dateTime;
87 | return true;
88 | case JobState.Canceled when !State.HasValue || State == JobState.Pending:
89 | await ClearDocumentStreamsAsync();
90 | State = state;
91 | ProcessingDateTime = dateTime;
92 | CompletedDateTime = dateTime;
93 | return true;
94 | case JobState.Completed when State == JobState.Processing:
95 | await ClearDocumentStreamsAsync();
96 | State = state;
97 | CompletedDateTime = dateTime;
98 | return true;
99 | case JobState.Aborted when State == JobState.Processing:
100 | State = state;
101 | CompletedDateTime = dateTime;
102 | return true;
103 | default:
104 | return false;
105 | }
106 | }
107 |
108 | public async ValueTask DisposeAsync()
109 | {
110 | await DisposeAsyncCore().ConfigureAwait(false);
111 | Dispose(disposing: false);
112 | GC.SuppressFinalize(this);
113 | }
114 |
115 | protected virtual async ValueTask ClearDocumentStreamsAsync()
116 | {
117 | foreach (var ippRequest in Requests)
118 | {
119 | switch (ippRequest)
120 | {
121 | case PrintJobRequest printJobRequest when printJobRequest.Document != null:
122 | await printJobRequest.Document.DisposeAsync().ConfigureAwait(false);
123 | printJobRequest.Document = Stream.Null;
124 | break;
125 | case SendDocumentRequest sendDocumentRequest when sendDocumentRequest.Document != null:
126 | await sendDocumentRequest.Document.DisposeAsync().ConfigureAwait(false);
127 | sendDocumentRequest.Document = Stream.Null;
128 | break;
129 | }
130 | }
131 | }
132 |
133 | protected virtual async ValueTask DisposeAsyncCore()
134 | {
135 | foreach (var ippRequest in Requests)
136 | {
137 | switch (ippRequest)
138 | {
139 | case PrintJobRequest printJobRequest when printJobRequest.Document != null:
140 | await printJobRequest.Document.DisposeAsync().ConfigureAwait(false);
141 | break;
142 | case SendDocumentRequest sendDocumentRequest when sendDocumentRequest.Document != null:
143 | await sendDocumentRequest.Document.DisposeAsync().ConfigureAwait(false);
144 | break;
145 | }
146 | }
147 | Requests.Clear();
148 | }
149 |
150 | protected virtual void Dispose(bool disposing)
151 | {
152 | if (disposedValue)
153 | return;
154 | if (disposing)
155 | {
156 | foreach (var ippRequest in Requests)
157 | {
158 | switch (ippRequest)
159 | {
160 | case PrintJobRequest printJobRequest when printJobRequest.Document != null:
161 | printJobRequest.Document.Dispose();
162 | break;
163 | case SendDocumentRequest sendDocumentRequest when sendDocumentRequest.Document != null:
164 | sendDocumentRequest.Document.Dispose();
165 | break;
166 | }
167 | }
168 | Requests.Clear();
169 | }
170 | disposedValue = true;
171 | }
172 |
173 | public void Dispose()
174 | {
175 | Dispose(disposing: true);
176 | GC.SuppressFinalize(this);
177 | }
178 | }
--------------------------------------------------------------------------------
/.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 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
364 |
365 | # Custom
366 | /SharpIppNextServer/jobs
367 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories
2 | root = true
3 |
4 | # C# files
5 | [*.cs]
6 |
7 | #### Core EditorConfig Options ####
8 |
9 | # Indentation and spacing
10 | indent_size = 4
11 | indent_style = space
12 | tab_width = 4
13 |
14 | # New line preferences
15 | end_of_line = crlf
16 | insert_final_newline = false
17 |
18 | #### .NET Code Actions ####
19 |
20 | # Type members
21 | dotnet_hide_advanced_members = false
22 | dotnet_member_insertion_location = with_other_members_of_the_same_kind
23 | dotnet_property_generation_behavior = prefer_throwing_properties
24 |
25 | # Symbol search
26 | dotnet_search_reference_assemblies = true
27 |
28 | #### .NET Coding Conventions ####
29 |
30 | # Organize usings
31 | dotnet_separate_import_directive_groups = false
32 | dotnet_sort_system_directives_first = false
33 | file_header_template = unset
34 |
35 | # this. and Me. preferences
36 | dotnet_style_qualification_for_event = false
37 | dotnet_style_qualification_for_field = false
38 | dotnet_style_qualification_for_method = false
39 | dotnet_style_qualification_for_property = false
40 |
41 | # Language keywords vs BCL types preferences
42 | dotnet_style_predefined_type_for_locals_parameters_members = true
43 | dotnet_style_predefined_type_for_member_access = true
44 |
45 | # Parentheses preferences
46 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
47 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
48 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary
49 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
50 |
51 | # Modifier preferences
52 | dotnet_style_require_accessibility_modifiers = for_non_interface_members
53 |
54 | # Expression-level preferences
55 | dotnet_prefer_system_hash_code = true
56 | dotnet_style_coalesce_expression = true
57 | dotnet_style_collection_initializer = true
58 | dotnet_style_explicit_tuple_names = true
59 | dotnet_style_namespace_match_folder = true
60 | dotnet_style_null_propagation = true
61 | dotnet_style_object_initializer = true
62 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
63 | dotnet_style_prefer_auto_properties = true
64 | dotnet_style_prefer_collection_expression = when_types_loosely_match
65 | dotnet_style_prefer_compound_assignment = true
66 | dotnet_style_prefer_conditional_expression_over_assignment = true
67 | dotnet_style_prefer_conditional_expression_over_return = true
68 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
69 | dotnet_style_prefer_inferred_anonymous_type_member_names = true
70 | dotnet_style_prefer_inferred_tuple_names = true
71 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true
72 | dotnet_style_prefer_simplified_boolean_expressions = true
73 | dotnet_style_prefer_simplified_interpolation = true
74 |
75 | # Field preferences
76 | dotnet_style_readonly_field = true
77 |
78 | # Parameter preferences
79 | dotnet_code_quality_unused_parameters = all
80 |
81 | # Suppression preferences
82 | dotnet_remove_unnecessary_suppression_exclusions = none
83 |
84 | # New line preferences
85 | dotnet_style_allow_multiple_blank_lines_experimental = false
86 | dotnet_style_allow_statement_immediately_after_block_experimental = true
87 |
88 | #### C# Coding Conventions ####
89 |
90 | # var preferences
91 | csharp_style_var_elsewhere = false
92 | csharp_style_var_for_built_in_types = false
93 | csharp_style_var_when_type_is_apparent = false
94 |
95 | # Expression-bodied members
96 | csharp_style_expression_bodied_accessors = true
97 | csharp_style_expression_bodied_constructors = when_on_single_line
98 | csharp_style_expression_bodied_indexers = true
99 | csharp_style_expression_bodied_lambdas = true
100 | csharp_style_expression_bodied_local_functions = when_on_single_line
101 | csharp_style_expression_bodied_methods = when_on_single_line
102 | csharp_style_expression_bodied_operators = when_on_single_line
103 | csharp_style_expression_bodied_properties = true
104 |
105 | # Pattern matching preferences
106 | csharp_style_pattern_matching_over_as_with_null_check = true
107 | csharp_style_pattern_matching_over_is_with_cast_check = true
108 | csharp_style_prefer_extended_property_pattern = true
109 | csharp_style_prefer_not_pattern = true
110 | csharp_style_prefer_pattern_matching = true
111 | csharp_style_prefer_switch_expression = true
112 |
113 | # Null-checking preferences
114 | csharp_style_conditional_delegate_call = true
115 |
116 | # Modifier preferences
117 | csharp_prefer_static_anonymous_function = true
118 | csharp_prefer_static_local_function = true
119 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
120 | csharp_style_prefer_readonly_struct = true
121 | csharp_style_prefer_readonly_struct_member = true
122 |
123 | # Code-block preferences
124 | csharp_prefer_braces = when_multiline
125 | csharp_prefer_simple_using_statement = true
126 | csharp_prefer_system_threading_lock = true
127 | csharp_style_namespace_declarations = file_scoped
128 | csharp_style_prefer_method_group_conversion = true
129 | csharp_style_prefer_primary_constructors = true
130 | csharp_style_prefer_top_level_statements = true
131 |
132 | # Expression-level preferences
133 | csharp_prefer_simple_default_expression = true
134 | csharp_style_deconstructed_variable_declaration = true
135 | csharp_style_implicit_object_creation_when_type_is_apparent = true
136 | csharp_style_inlined_variable_declaration = true
137 | csharp_style_prefer_index_operator = true
138 | csharp_style_prefer_local_over_anonymous_function = true
139 | csharp_style_prefer_null_check_over_type_check = true
140 | csharp_style_prefer_range_operator = true
141 | csharp_style_prefer_tuple_swap = true
142 | csharp_style_prefer_utf8_string_literals = true
143 | csharp_style_throw_expression = true
144 | csharp_style_unused_value_assignment_preference = discard_variable
145 | csharp_style_unused_value_expression_statement_preference = discard_variable
146 |
147 | # 'using' directive preferences
148 | csharp_using_directive_placement = outside_namespace
149 |
150 | # New line preferences
151 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
152 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
153 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
154 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
155 | csharp_style_allow_embedded_statements_on_same_line_experimental = false
156 |
157 | #### C# Formatting Rules ####
158 |
159 | # New line preferences
160 | csharp_new_line_before_catch = true
161 | csharp_new_line_before_else = true
162 | csharp_new_line_before_finally = true
163 | csharp_new_line_before_members_in_anonymous_types = true
164 | csharp_new_line_before_members_in_object_initializers = true
165 | csharp_new_line_before_open_brace = all
166 | csharp_new_line_between_query_expression_clauses = true
167 |
168 | # Indentation preferences
169 | csharp_indent_block_contents = true
170 | csharp_indent_braces = false
171 | csharp_indent_case_contents = true
172 | csharp_indent_case_contents_when_block = true
173 | csharp_indent_labels = one_less_than_current
174 | csharp_indent_switch_labels = true
175 |
176 | # Space preferences
177 | csharp_space_after_cast = false
178 | csharp_space_after_colon_in_inheritance_clause = true
179 | csharp_space_after_comma = true
180 | csharp_space_after_dot = false
181 | csharp_space_after_keywords_in_control_flow_statements = true
182 | csharp_space_after_semicolon_in_for_statement = true
183 | csharp_space_around_binary_operators = before_and_after
184 | csharp_space_around_declaration_statements = false
185 | csharp_space_before_colon_in_inheritance_clause = true
186 | csharp_space_before_comma = false
187 | csharp_space_before_dot = false
188 | csharp_space_before_open_square_brackets = false
189 | csharp_space_before_semicolon_in_for_statement = false
190 | csharp_space_between_empty_square_brackets = false
191 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
192 | csharp_space_between_method_call_name_and_opening_parenthesis = false
193 | csharp_space_between_method_call_parameter_list_parentheses = false
194 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
195 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
196 | csharp_space_between_method_declaration_parameter_list_parentheses = false
197 | csharp_space_between_parentheses = false
198 | csharp_space_between_square_brackets = false
199 |
200 | # Wrapping preferences
201 | csharp_preserve_single_line_blocks = true
202 | csharp_preserve_single_line_statements = true
203 |
204 | #### Naming styles ####
205 |
206 | # Naming rules
207 |
208 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
209 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
210 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
211 |
212 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
213 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
214 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
215 |
216 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
217 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
218 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
219 |
220 | # Symbol specifications
221 |
222 | dotnet_naming_symbols.interface.applicable_kinds = interface
223 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
224 | dotnet_naming_symbols.interface.required_modifiers =
225 |
226 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
227 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
228 | dotnet_naming_symbols.types.required_modifiers =
229 |
230 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
231 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
232 | dotnet_naming_symbols.non_field_members.required_modifiers =
233 |
234 | # Naming styles
235 |
236 | dotnet_naming_style.pascal_case.required_prefix =
237 | dotnet_naming_style.pascal_case.required_suffix =
238 | dotnet_naming_style.pascal_case.word_separator =
239 | dotnet_naming_style.pascal_case.capitalization = pascal_case
240 |
241 | dotnet_naming_style.begins_with_i.required_prefix = I
242 | dotnet_naming_style.begins_with_i.required_suffix =
243 | dotnet_naming_style.begins_with_i.word_separator =
244 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
245 |
--------------------------------------------------------------------------------
/SharpIppNextServer/THIRD-PARTY-NOTICES.txt:
--------------------------------------------------------------------------------
1 | Microsoft.Extensions.Configuration.Abstractions.dll
2 | Microsoft.Extensions.DependencyInjection.Abstractions.dll
3 | Microsoft.Extensions.Diagnostics.Abstractions.dll
4 | Microsoft.Extensions.FileProviders.Abstractions.dll
5 | Microsoft.Extensions.Hosting.Abstractions.dll
6 | Microsoft.Extensions.Logging.Abstractions.dll
7 | Microsoft.Extensions.Options.dll
8 | Microsoft.Extensions.Primitives.dll
9 | System.Configuration.ConfigurationManager.dll
10 | System.Diagnostics.DiagnosticSource.dll
11 | System.Diagnostics.EventLog.dll
12 | System.Security.Cryptography.ProtectedData.dll
13 | runtimes\win\lib\net8.0\System.Diagnostics.EventLog.Messages.dll
14 | runtimes\win\lib\net8.0\System.Diagnostics.EventLog.dll
15 | ----------------------------------------------------------------
16 | The MIT License (MIT)
17 |
18 | Copyright (c) .NET Foundation and Contributors
19 |
20 | All rights reserved.
21 |
22 | Permission is hereby granted, free of charge, to any person obtaining a copy
23 | of this software and associated documentation files (the "Software"), to deal
24 | in the Software without restriction, including without limitation the rights
25 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26 | copies of the Software, and to permit persons to whom the Software is
27 | furnished to do so, subject to the following conditions:
28 |
29 | The above copyright notice and this permission notice shall be included in all
30 | copies or substantial portions of the Software.
31 |
32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38 | SOFTWARE.
39 |
40 | Quartz.dll
41 | Quartz.Extensions.DependencyInjection.dll
42 | Quartz.Extensions.Hosting.dll
43 | -----------------------------------------
44 | Apache License
45 | Version 2.0, January 2004
46 | http://www.apache.org/licenses/
47 |
48 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
49 |
50 | 1. Definitions.
51 |
52 | "License" shall mean the terms and conditions for use, reproduction,
53 | and distribution as defined by Sections 1 through 9 of this document.
54 |
55 | "Licensor" shall mean the copyright owner or entity authorized by
56 | the copyright owner that is granting the License.
57 |
58 | "Legal Entity" shall mean the union of the acting entity and all
59 | other entities that control, are controlled by, or are under common
60 | control with that entity. For the purposes of this definition,
61 | "control" means (i) the power, direct or indirect, to cause the
62 | direction or management of such entity, whether by contract or
63 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
64 | outstanding shares, or (iii) beneficial ownership of such entity.
65 |
66 | "You" (or "Your") shall mean an individual or Legal Entity
67 | exercising permissions granted by this License.
68 |
69 | "Source" form shall mean the preferred form for making modifications,
70 | including but not limited to software source code, documentation
71 | source, and configuration files.
72 |
73 | "Object" form shall mean any form resulting from mechanical
74 | transformation or translation of a Source form, including but
75 | not limited to compiled object code, generated documentation,
76 | and conversions to other media types.
77 |
78 | "Work" shall mean the work of authorship, whether in Source or
79 | Object form, made available under the License, as indicated by a
80 | copyright notice that is included in or attached to the work
81 | (an example is provided in the Appendix below).
82 |
83 | "Derivative Works" shall mean any work, whether in Source or Object
84 | form, that is based on (or derived from) the Work and for which the
85 | editorial revisions, annotations, elaborations, or other modifications
86 | represent, as a whole, an original work of authorship. For the purposes
87 | of this License, Derivative Works shall not include works that remain
88 | separable from, or merely link (or bind by name) to the interfaces of,
89 | the Work and Derivative Works thereof.
90 |
91 | "Contribution" shall mean any work of authorship, including
92 | the original version of the Work and any modifications or additions
93 | to that Work or Derivative Works thereof, that is intentionally
94 | submitted to Licensor for inclusion in the Work by the copyright owner
95 | or by an individual or Legal Entity authorized to submit on behalf of
96 | the copyright owner. For the purposes of this definition, "submitted"
97 | means any form of electronic, verbal, or written communication sent
98 | to the Licensor or its representatives, including but not limited to
99 | communication on electronic mailing lists, source code control systems,
100 | and issue tracking systems that are managed by, or on behalf of, the
101 | Licensor for the purpose of discussing and improving the Work, but
102 | excluding communication that is conspicuously marked or otherwise
103 | designated in writing by the copyright owner as "Not a Contribution."
104 |
105 | "Contributor" shall mean Licensor and any individual or Legal Entity
106 | on behalf of whom a Contribution has been received by Licensor and
107 | subsequently incorporated within the Work.
108 |
109 | 2. Grant of Copyright License. Subject to the terms and conditions of
110 | this License, each Contributor hereby grants to You a perpetual,
111 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
112 | copyright license to reproduce, prepare Derivative Works of,
113 | publicly display, publicly perform, sublicense, and distribute the
114 | Work and such Derivative Works in Source or Object form.
115 |
116 | 3. Grant of Patent License. Subject to the terms and conditions of
117 | this License, each Contributor hereby grants to You a perpetual,
118 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
119 | (except as stated in this section) patent license to make, have made,
120 | use, offer to sell, sell, import, and otherwise transfer the Work,
121 | where such license applies only to those patent claims licensable
122 | by such Contributor that are necessarily infringed by their
123 | Contribution(s) alone or by combination of their Contribution(s)
124 | with the Work to which such Contribution(s) was submitted. If You
125 | institute patent litigation against any entity (including a
126 | cross-claim or counterclaim in a lawsuit) alleging that the Work
127 | or a Contribution incorporated within the Work constitutes direct
128 | or contributory patent infringement, then any patent licenses
129 | granted to You under this License for that Work shall terminate
130 | as of the date such litigation is filed.
131 |
132 | 4. Redistribution. You may reproduce and distribute copies of the
133 | Work or Derivative Works thereof in any medium, with or without
134 | modifications, and in Source or Object form, provided that You
135 | meet the following conditions:
136 |
137 | (a) You must give any other recipients of the Work or
138 | Derivative Works a copy of this License; and
139 |
140 | (b) You must cause any modified files to carry prominent notices
141 | stating that You changed the files; and
142 |
143 | (c) You must retain, in the Source form of any Derivative Works
144 | that You distribute, all copyright, patent, trademark, and
145 | attribution notices from the Source form of the Work,
146 | excluding those notices that do not pertain to any part of
147 | the Derivative Works; and
148 |
149 | (d) If the Work includes a "NOTICE" text file as part of its
150 | distribution, then any Derivative Works that You distribute must
151 | include a readable copy of the attribution notices contained
152 | within such NOTICE file, excluding those notices that do not
153 | pertain to any part of the Derivative Works, in at least one
154 | of the following places: within a NOTICE text file distributed
155 | as part of the Derivative Works; within the Source form or
156 | documentation, if provided along with the Derivative Works; or,
157 | within a display generated by the Derivative Works, if and
158 | wherever such third-party notices normally appear. The contents
159 | of the NOTICE file are for informational purposes only and
160 | do not modify the License. You may add Your own attribution
161 | notices within Derivative Works that You distribute, alongside
162 | or as an addendum to the NOTICE text from the Work, provided
163 | that such additional attribution notices cannot be construed
164 | as modifying the License.
165 |
166 | You may add Your own copyright statement to Your modifications and
167 | may provide additional or different license terms and conditions
168 | for use, reproduction, or distribution of Your modifications, or
169 | for any such Derivative Works as a whole, provided Your use,
170 | reproduction, and distribution of the Work otherwise complies with
171 | the conditions stated in this License.
172 |
173 | 5. Submission of Contributions. Unless You explicitly state otherwise,
174 | any Contribution intentionally submitted for inclusion in the Work
175 | by You to the Licensor shall be under the terms and conditions of
176 | this License, without any additional terms or conditions.
177 | Notwithstanding the above, nothing herein shall supersede or modify
178 | the terms of any separate license agreement you may have executed
179 | with Licensor regarding such Contributions.
180 |
181 | 6. Trademarks. This License does not grant permission to use the trade
182 | names, trademarks, service marks, or product names of the Licensor,
183 | except as required for reasonable and customary use in describing the
184 | origin of the Work and reproducing the content of the NOTICE file.
185 |
186 | 7. Disclaimer of Warranty. Unless required by applicable law or
187 | agreed to in writing, Licensor provides the Work (and each
188 | Contributor provides its Contributions) on an "AS IS" BASIS,
189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
190 | implied, including, without limitation, any warranties or conditions
191 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
192 | PARTICULAR PURPOSE. You are solely responsible for determining the
193 | appropriateness of using or redistributing the Work and assume any
194 | risks associated with Your exercise of permissions under this License.
195 |
196 | 8. Limitation of Liability. In no event and under no legal theory,
197 | whether in tort (including negligence), contract, or otherwise,
198 | unless required by applicable law (such as deliberate and grossly
199 | negligent acts) or agreed to in writing, shall any Contributor be
200 | liable to You for damages, including any direct, indirect, special,
201 | incidental, or consequential damages of any character arising as a
202 | result of this License or out of the use or inability to use the
203 | Work (including but not limited to damages for loss of goodwill,
204 | work stoppage, computer failure or malfunction, or any and all
205 | other commercial damages or losses), even if such Contributor
206 | has been advised of the possibility of such damages.
207 |
208 | 9. Accepting Warranty or Additional Liability. While redistributing
209 | the Work or Derivative Works thereof, You may choose to offer,
210 | and charge a fee for, acceptance of support, warranty, indemnity,
211 | or other liability obligations and/or rights consistent with this
212 | License. However, in accepting such obligations, You may act only
213 | on Your own behalf and on Your sole responsibility, not on behalf
214 | of any other Contributor, and only if You agree to indemnify,
215 | defend, and hold each Contributor harmless for any liability
216 | incurred by, or claims asserted against, such Contributor by reason
217 | of your accepting any such warranty or additional liability.
218 |
219 | END OF TERMS AND CONDITIONS
220 |
221 | APPENDIX: How to apply the Apache License to your work.
222 |
223 | To apply the Apache License to your work, attach the following
224 | boilerplate notice, with the fields enclosed by brackets "[]"
225 | replaced with your own identifying information. (Don't include
226 | the brackets!) The text should be enclosed in the appropriate
227 | comment syntax for the file format. We also recommend that a
228 | file or class name and description of purpose be included on the
229 | same "printed page" as the copyright notice for easier
230 | identification within third-party archives.
231 |
232 | Copyright 2007 Marko Lahma
233 |
234 | Licensed under the Apache License, Version 2.0 (the "License");
235 | you may not use this file except in compliance with the License.
236 | You may obtain a copy of the License at
237 |
238 | http://www.apache.org/licenses/LICENSE-2.0
239 |
240 | Unless required by applicable law or agreed to in writing, software
241 | distributed under the License is distributed on an "AS IS" BASIS,
242 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
243 | See the License for the specific language governing permissions and
244 | limitations under the License.
245 |
246 | SharpIpp.dll
247 | ------------
248 | MIT License
249 |
250 | Copyright (c) 2023 Daniel Klecha
251 | Copyright (c) 2020 Evgeny Zelenov
252 |
253 | Permission is hereby granted, free of charge, to any person obtaining a copy
254 | of this software and associated documentation files (the "Software"), to deal
255 | in the Software without restriction, including without limitation the rights
256 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
257 | copies of the Software, and to permit persons to whom the Software is
258 | furnished to do so, subject to the following conditions:
259 |
260 | The above copyright notice and this permission notice shall be included in all
261 | copies or substantial portions of the Software.
262 |
263 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
264 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
265 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
266 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
267 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
268 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
269 | SOFTWARE.
270 |
271 | System.IO.Abstractions.dll
272 | TestableIO.System.IO.Abstractions.dll
273 | TestableIO.System.IO.Abstractions.Wrappers.dll
274 | ----------------------------------------------
275 | The MIT License (MIT)
276 |
277 | Copyright (c) Tatham Oddie and Contributors
278 |
279 | All rights reserved.
280 |
281 | Permission is hereby granted, free of charge, to any person obtaining a copy
282 | of this software and associated documentation files (the "Software"), to deal
283 | in the Software without restriction, including without limitation the rights
284 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
285 | copies of the Software, and to permit persons to whom the Software is
286 | furnished to do so, subject to the following conditions:
287 |
288 | The above copyright notice and this permission notice shall be included in all
289 | copies or substantial portions of the Software.
290 |
291 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
292 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
293 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
294 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
295 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
296 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
297 | SOFTWARE.
298 |
299 |
--------------------------------------------------------------------------------
/SharpIppNextServer/Services/PrinterService.cs:
--------------------------------------------------------------------------------
1 | using SharpIpp;
2 | using SharpIpp.Protocol.Models;
3 | using System.Collections.Concurrent;
4 | using SharpIpp.Protocol;
5 | using SharpIpp.Models;
6 | using Microsoft.Extensions.Options;
7 | using SharpIpp.Exceptions;
8 | using SharpIppNextServer.Models;
9 | using System.Text;
10 |
11 | namespace SharpIppNextServer.Services;
12 |
13 | public class PrinterService(
14 | ISharpIppServer sharpIppServer,
15 | IHttpContextAccessor httpContextAccessor,
16 | ILogger logger,
17 | IOptions printerOptions,
18 | IDateTimeOffsetProvider dateTimeOffsetProvider) : IDisposable, IAsyncDisposable
19 | {
20 | private bool disposedValue;
21 | private int _newJobIndex = dateTimeOffsetProvider.UtcNow.Day * 1000;
22 | private bool _isPaused;
23 | private readonly ConcurrentDictionary _jobs = new();
24 | private readonly DateTimeOffset _startTime = dateTimeOffsetProvider.UtcNow.AddMinutes(-1);
25 |
26 | private int GetNextValue()
27 | {
28 | return Interlocked.Increment(ref _newJobIndex);
29 | }
30 |
31 | public async Task ProcessRequestAsync(Stream inputStream, Stream outputStream)
32 | {
33 | try
34 | {
35 | IIppRequest request = await sharpIppServer.ReceiveRequestAsync(inputStream);
36 | IIppResponseMessage response = await GetResponseAsync(request);
37 | IIppResponseMessage rawResponse = await sharpIppServer.CreateRawResponseAsync(response);
38 | ImproveRawResponse(request, rawResponse);
39 | await sharpIppServer.SendRawResponseAsync(rawResponse, outputStream);
40 | }
41 | catch (IppRequestException ex)
42 | {
43 | logger.LogError(ex, "Unable to process request");
44 | var response = new IppResponseMessage
45 | {
46 | RequestId = ex.RequestMessage.RequestId,
47 | Version = ex.RequestMessage.Version,
48 | StatusCode = ex.StatusCode
49 | };
50 | var operation = new IppSection { Tag = SectionTag.OperationAttributesTag };
51 | operation.Attributes.Add(new IppAttribute(Tag.Charset, JobAttribute.AttributesCharset, "utf-8"));
52 | operation.Attributes.Add(new IppAttribute(Tag.NaturalLanguage, JobAttribute.AttributesNaturalLanguage, "en"));
53 | response.Sections.Add(operation);
54 | await sharpIppServer.SendRawResponseAsync(response, outputStream);
55 | }
56 | catch (Exception ex)
57 | {
58 | logger.LogError(ex, "Unable to process request");
59 | if (httpContextAccessor.HttpContext != null)
60 | httpContextAccessor.HttpContext.Response.StatusCode = 500;
61 | }
62 | }
63 |
64 | private async Task GetResponseAsync(IIppRequest request)
65 | {
66 | return request switch
67 | {
68 | CancelJobRequest x => await GetCancelJobResponseAsync(x),
69 | CreateJobRequest x => GetCreateJobResponse(x),
70 | CUPSGetPrintersRequest x => GetCUPSGetPrintersResponse(x),
71 | GetJobAttributesRequest x => GetGetJobAttributesResponse(x),
72 | GetJobsRequest x => GetGetJobsResponse(x),
73 | GetPrinterAttributesRequest x => GetGetPrinterAttributesResponse(x),
74 | HoldJobRequest x => await GetHoldJobResponseAsync(x),
75 | PausePrinterRequest x => GetPausePrinterResponse(x),
76 | PrintJobRequest x => await GetPrintJobResponseAsync(x),
77 | PrintUriRequest x => GetPrintUriResponse(x),
78 | PurgeJobsRequest x => await GetPurgeJobsResponseAsync(x),
79 | ReleaseJobRequest x => await GetReleaseJobResponseAsync(x),
80 | RestartJobRequest x => await GetRestartJobResponseAsync(x),
81 | ResumePrinterRequest x => GetResumePrinterResponse(x),
82 | SendDocumentRequest x => await GetSendDocumentResponseAsync(x),
83 | SendUriRequest x => await GetSendUriResponseAsync(x),
84 | ValidateJobRequest x => GetValidateJobResponse(x),
85 | _ => throw new NotImplementedException()
86 | };
87 | }
88 |
89 | private void ImproveRawResponse(IIppRequest request, IIppResponseMessage rawResponse)
90 | {
91 | switch(request)
92 | {
93 | case GetPrinterAttributesRequest x:
94 | ImproveGetPrinterAttributesRawResponse(x, rawResponse);
95 | break;
96 | }
97 | }
98 |
99 | private void ImproveGetPrinterAttributesRawResponse(GetPrinterAttributesRequest request, IIppResponseMessage rawResponse)
100 | {
101 | var section = rawResponse.Sections.FirstOrDefault(x => x.Tag == SectionTag.PrinterAttributesTag);
102 | if(section is null)
103 | return;
104 | bool IsRequired(string attributeName) => !section.Attributes.Any(x => x.Name.Equals(attributeName))
105 | && IsAttributeRequired(request, attributeName);
106 | var options = printerOptions.Value;
107 | if(IsRequired("printer-device-id"))
108 | section.Attributes.Add(new IppAttribute(Tag.TextWithoutLanguage, "printer-device-id", GetPrinterDeviceId()));
109 | if(IsRequired("printer-uuid"))
110 | section.Attributes.Add(new IppAttribute(Tag.Uri, "printer-uuid", $"urn:uuid:{options.UUID}"));
111 | if(IsRequired("printer-dns-sd-name"))
112 | section.Attributes.Add(new IppAttribute(Tag.NameWithoutLanguage, "printer-dns-sd-name", options.DnsSdName));
113 | if(IsRequired("printer-make-and-model"))
114 | section.Attributes.Add(new IppAttribute(Tag.TextWithoutLanguage, "printer-make-and-model", options.Name));
115 | if(IsRequired("printer-firmware-name"))
116 | section.Attributes.Add(new IppAttribute(Tag.NameWithoutLanguage, "printer-firmware-name", options.FirmwareName));
117 | if(IsRequired("printer-firmware-string-version"))
118 | section.Attributes.Add(new IppAttribute(Tag.TextWithoutLanguage, "printer-firmware-string-version", options.FirmwareName));
119 | }
120 |
121 | private ValidateJobResponse GetValidateJobResponse(ValidateJobRequest request)
122 | {
123 | logger.LogInformation("Job has been validated");
124 | return new ValidateJobResponse
125 | {
126 | RequestId = request.RequestId,
127 | Version = request.Version,
128 | StatusCode = IppStatusCode.SuccessfulOk
129 | };
130 | }
131 |
132 | private static string GetPrinterDeviceId()
133 | {
134 | return new StringBuilder()
135 | .Append("MFG:danielklecha;") //Manufacturer
136 | .Append("MDL:SharpIppNext1;") //Model
137 | .Append("CMD:Automatic,JPEG;") //Command Set
138 | .Append("CLS:PRINTER") //Class
139 | .Append("DES:SIN1;") //Designator or Description
140 | .Append("CID:SharpIppNext_1;") //Compatible ID
141 | .Append("LEDMDIS:USB#FF#CC#00,USB#07#01#02,USB#FF#04#01;") //Legacy Device ID String
142 | .Append("SN:SIN279BJ23J07PX;") //Serial Number
143 | .Append("S:038000C480a00001002c240005ac1400032;") //Status
144 | .Append("Z:05000008000009,12000,17000000000000,181;") //Vendor-Specific
145 | .ToString();
146 | }
147 |
148 | private async Task GetSendUriResponseAsync(SendUriRequest request)
149 | {
150 | var response = new SendUriResponse
151 | {
152 | RequestId = request.RequestId,
153 | Version = request.Version,
154 | StatusCode = IppStatusCode.ClientErrorNotPossible
155 | };
156 | var jobId = GetJobId(request);
157 | if (!jobId.HasValue)
158 | return response;
159 | response.JobId = jobId.Value;
160 | response.JobUri = $"{GetPrinterUrl()}/{jobId.Value}";
161 | if (!_jobs.TryGetValue(jobId.Value, out var job))
162 | return response;
163 | var copy = new PrinterJob(job);
164 | if (request.OperationAttributes?.LastDocument ?? false)
165 | {
166 | if (!await copy.TrySetStateAsync(JobState.Pending, dateTimeOffsetProvider.UtcNow))
167 | return response;
168 | logger.LogInformation("Job {id} has been moved to queue", job.Id);
169 | }
170 | FillWithDefaultValues(request.OperationAttributes ??= new());
171 | job.Requests.Add(request);
172 | logger.LogInformation("Document has been added to job {id}", job.Id);
173 | if (!_jobs.TryUpdate(jobId.Value, copy, job))
174 | return response;
175 | response.StatusCode = IppStatusCode.SuccessfulOk;
176 | return response;
177 | }
178 |
179 | private async Task GetSendDocumentResponseAsync(SendDocumentRequest request)
180 | {
181 | var response = new SendDocumentResponse
182 | {
183 | RequestId = request.RequestId,
184 | Version = request.Version,
185 | StatusCode = IppStatusCode.ClientErrorNotPossible
186 | };
187 | var jobId = GetJobId(request);
188 | if (!jobId.HasValue)
189 | return response;
190 | response.JobId = jobId.Value;
191 | response.JobUri = $"{GetPrinterUrl()}/{jobId.Value}";
192 | if (!_jobs.TryGetValue(jobId.Value, out var job))
193 | return response;
194 | var copy = new PrinterJob(job);
195 | if (request.OperationAttributes?.LastDocument ?? false)
196 | {
197 | if (!await copy.TrySetStateAsync(JobState.Pending, dateTimeOffsetProvider.UtcNow))
198 | return response;
199 | logger.LogInformation("Job {id} has been moved to queue", job.Id);
200 | }
201 | FillWithDefaultValues(request.OperationAttributes ??= new());
202 | job.Requests.Add(request);
203 | logger.LogInformation("Document has been added to job {id}", job.Id);
204 | if (!_jobs.TryUpdate(jobId.Value, copy, job))
205 | return response;
206 | response.JobState = JobState.Pending;
207 | response.StatusCode = IppStatusCode.SuccessfulOk;
208 | return response;
209 | }
210 |
211 | private ReleaseJobResponse GetResumePrinterResponse(ResumePrinterRequest request)
212 | {
213 | _isPaused = false;
214 | logger.LogInformation("Printer has been resumed");
215 | return new ReleaseJobResponse
216 | {
217 | RequestId = request.RequestId,
218 | Version = request.Version
219 | };
220 | }
221 |
222 | private async Task GetRestartJobResponseAsync(RestartJobRequest request)
223 | {
224 | var response = new ReleaseJobResponse
225 | {
226 | RequestId = request.RequestId,
227 | Version = request.Version,
228 | StatusCode = IppStatusCode.ClientErrorNotPossible
229 | };
230 | var jobId = GetJobId(request);
231 | if (!jobId.HasValue)
232 | return response;
233 | if (!_jobs.TryGetValue(jobId.Value, out var job))
234 | return response;
235 | var copy = new PrinterJob(job);
236 | if (!await copy.TrySetStateAsync(JobState.Pending, dateTimeOffsetProvider.UtcNow))
237 | return response;
238 | if (!_jobs.TryUpdate(jobId.Value, copy, job))
239 | return response;
240 | response.StatusCode = IppStatusCode.SuccessfulOk;
241 | logger.LogInformation("Job {id} has been restarted", jobId);
242 | return response;
243 | }
244 |
245 | private async Task GetReleaseJobResponseAsync(ReleaseJobRequest request)
246 | {
247 | var response = new ReleaseJobResponse
248 | {
249 | RequestId = request.RequestId,
250 | Version = request.Version,
251 | StatusCode = IppStatusCode.ClientErrorNotPossible
252 | };
253 | var jobId = GetJobId(request);
254 | if (!jobId.HasValue)
255 | return response;
256 | if (!_jobs.TryGetValue(jobId.Value, out var job))
257 | return response;
258 | var copy = new PrinterJob(job);
259 | if (!await copy.TrySetStateAsync(JobState.Pending, dateTimeOffsetProvider.UtcNow))
260 | return response;
261 | if (!_jobs.TryUpdate(jobId.Value, copy, job))
262 | return response;
263 | response.StatusCode = IppStatusCode.SuccessfulOk;
264 | logger.LogInformation("Job {id} has been released", jobId);
265 | return response;
266 | }
267 |
268 | private async Task GetPurgeJobsResponseAsync(PurgeJobsRequest request)
269 | {
270 | foreach (var id in _jobs.Values.Where(x => x.State != JobState.Processing).Select(x => x.Id))
271 | {
272 | if (_jobs.TryRemove(id, out var job))
273 | await job.DisposeAsync();
274 | }
275 | logger.LogInformation("System purged jobs");
276 | return new PurgeJobsResponse
277 | {
278 | RequestId = request.RequestId,
279 | Version = request.Version,
280 | StatusCode = IppStatusCode.SuccessfulOk
281 | };
282 | }
283 |
284 | private PrintUriResponse GetPrintUriResponse(PrintUriRequest request)
285 | {
286 | var response = new PrintUriResponse
287 | {
288 | RequestId = request.RequestId,
289 | Version = request.Version,
290 | JobState = JobState.Pending,
291 | StatusCode = IppStatusCode.ClientErrorNotPossible
292 | };
293 | var job = new PrinterJob(GetNextValue(), request.OperationAttributes?.RequestingUserName, dateTimeOffsetProvider.UtcNow);
294 | response.JobId = job.Id;
295 | response.JobUri = $"{GetPrinterUrl()}/{job.Id}";
296 | FillWithDefaultValues(job.Id, request.OperationAttributes ??= new());
297 | FillWithDefaultValues(request.JobTemplateAttributes ??= new());
298 | job.Requests.Add(request);
299 | if (!_jobs.TryAdd(job.Id, job))
300 | return response;
301 | response.StatusCode = IppStatusCode.SuccessfulOk;
302 | logger.LogInformation("Job {id} has been added to queue", job.Id);
303 | return response;
304 | }
305 |
306 | private PausePrinterResponse GetPausePrinterResponse(PausePrinterRequest request)
307 | {
308 | _isPaused = true;
309 | logger.LogInformation("Printer has been paused");
310 | return new PausePrinterResponse
311 | {
312 | RequestId = request.RequestId,
313 | Version = request.Version
314 | };
315 | }
316 |
317 | private async Task GetHoldJobResponseAsync(HoldJobRequest request)
318 | {
319 | var response = new HoldJobResponse
320 | {
321 | RequestId = request.RequestId,
322 | Version = request.Version,
323 | StatusCode = IppStatusCode.ClientErrorNotPossible
324 | };
325 | var jobId = GetJobId(request);
326 | if (!jobId.HasValue)
327 | return response;
328 | if (!_jobs.TryGetValue(jobId.Value, out var job))
329 | return response;
330 | var copy = new PrinterJob(job);
331 | if (!await copy.TrySetStateAsync(null, dateTimeOffsetProvider.UtcNow))
332 | return response;
333 | if (!_jobs.TryUpdate(jobId.Value, copy, job))
334 | return response;
335 | response.StatusCode = IppStatusCode.SuccessfulOk;
336 | logger.LogInformation("Job {id} has been held", jobId);
337 | return response;
338 | }
339 |
340 | private static bool IsAttributeRequired(GetPrinterAttributesRequest request, string attributeName)
341 | {
342 | return request.OperationAttributes is null
343 | || request.OperationAttributes.RequestedAttributes is null
344 | || request.OperationAttributes.RequestedAttributes.Length == 0
345 | || request.OperationAttributes.RequestedAttributes.All(x => x == string.Empty)
346 | || request.OperationAttributes.RequestedAttributes.Any(x => x.Equals("all", StringComparison.InvariantCultureIgnoreCase))
347 | || request.OperationAttributes.RequestedAttributes.Contains(attributeName);
348 | }
349 |
350 | private GetPrinterAttributesResponse GetGetPrinterAttributesResponse(GetPrinterAttributesRequest request)
351 | {
352 | var options = printerOptions.Value;
353 | var allAttributes = PrinterAttribute.GetAttributes(request.Version).ToList();
354 | bool IsRequired(string attributeName) => IsAttributeRequired(request, attributeName);
355 | logger.LogInformation("System returned printer attributes");
356 | return new GetPrinterAttributesResponse
357 | {
358 | RequestId = request.RequestId,
359 | Version = request.Version,
360 | StatusCode = IppStatusCode.SuccessfulOk,
361 | PrinterState = !IsRequired(PrinterAttribute.PrinterState)
362 | ? null
363 | : _jobs.Values.Any(x => x.State == JobState.Pending || x.State == JobState.Processing) ? PrinterState.Processing : PrinterState.Idle,
364 | PrinterStateReasons = !IsRequired(PrinterAttribute.PrinterStateReasons) ? null : ["none"],
365 | CharsetConfigured = !IsRequired(PrinterAttribute.CharsetConfigured) ? null : "utf-8",
366 | CharsetSupported = !IsRequired(PrinterAttribute.CharsetSupported) ? null : ["utf-8"],
367 | NaturalLanguageConfigured = !IsRequired(PrinterAttribute.NaturalLanguageConfigured) ? null : "en-us",
368 | GeneratedNaturalLanguageSupported = !IsRequired(PrinterAttribute.GeneratedNaturalLanguageSupported) ? null : ["en-us"],
369 | PrinterIsAcceptingJobs = !IsRequired(PrinterAttribute.PrinterIsAcceptingJobs) ? null : true,
370 | PrinterMakeAndModel = !IsRequired(PrinterAttribute.PrinterMakeAndModel) ? null : options.Name,
371 | PrinterName = !IsRequired(PrinterAttribute.PrinterName) ? null : options.Name,
372 | PrinterInfo = !IsRequired(PrinterAttribute.PrinterInfo) ? null : options.Name,
373 | IppVersionsSupported = !IsRequired(PrinterAttribute.IppVersionsSupported) ? null : [new IppVersion(1, 0), IppVersion.V1_1, new IppVersion(2, 0)],
374 | DocumentFormatDefault = !IsRequired(PrinterAttribute.DocumentFormatDefault) ? null : options.DocumentFormat,
375 | ColorSupported = !IsRequired(PrinterAttribute.ColorSupported) ? null : true,
376 | PrinterCurrentTime = !IsRequired(PrinterAttribute.PrinterCurrentTime) ? null : dateTimeOffsetProvider.Now,
377 | OperationsSupported = !IsRequired(PrinterAttribute.OperationsSupported) ? null :
378 | [
379 | IppOperation.PrintJob,
380 | IppOperation.PrintUri,
381 | IppOperation.ValidateJob,
382 | IppOperation.CreateJob,
383 | IppOperation.SendDocument,
384 | IppOperation.SendUri,
385 | IppOperation.CancelJob,
386 | IppOperation.GetJobAttributes,
387 | IppOperation.GetJobs,
388 | IppOperation.GetPrinterAttributes,
389 | IppOperation.HoldJob,
390 | IppOperation.ReleaseJob,
391 | IppOperation.RestartJob,
392 | IppOperation.PausePrinter,
393 | IppOperation.ResumePrinter
394 | ],
395 | QueuedJobCount = !IsRequired(PrinterAttribute.QueuedJobCount) ? null : _jobs.Values.Where(x => x.State == JobState.Pending || x.State == JobState.Processing).Count(),
396 | DocumentFormatSupported = !IsRequired(PrinterAttribute.DocumentFormatSupported) ? null : [options.DocumentFormat],
397 | MultipleDocumentJobsSupported = !IsRequired(PrinterAttribute.MultipleDocumentJobsSupported) ? null : true,
398 | CompressionSupported = !IsRequired(PrinterAttribute.CompressionSupported) ? null : [Compression.None],
399 | PrinterLocation = !IsRequired(PrinterAttribute.PrinterLocation) ? null : "Internet",
400 | PrintScalingDefault = !IsRequired(PrinterAttribute.PrintScalingDefault) ? null : options.PrintScaling.FirstOrDefault(),
401 | PrintScalingSupported = !IsRequired(PrinterAttribute.PrintScalingSupported) ? null : options.PrintScaling,
402 | PrinterUriSupported = !IsRequired(PrinterAttribute.PrinterUriSupported) ? null : [GetPrinterUrl("/ipp/print")],
403 | UriAuthenticationSupported = !IsRequired(PrinterAttribute.UriAuthenticationSupported) ? null : [UriAuthentication.None],
404 | UriSecuritySupported = !IsRequired(PrinterAttribute.UriSecuritySupported) ? null : [GetUriSecuritySupported()],
405 | PrinterUpTime = !IsRequired(PrinterAttribute.PrinterUpTime) ? null : (int)(dateTimeOffsetProvider.UtcNow - _startTime).TotalSeconds,
406 | MediaDefault = !IsRequired(PrinterAttribute.MediaDefault) ? null : options.Media.FirstOrDefault(),
407 | MediaSupported = !IsRequired(PrinterAttribute.MediaSupported) ? null : options.Media,
408 | SidesDefault = !IsRequired(PrinterAttribute.SidesDefault) ? null : options.Sides.FirstOrDefault(),
409 | SidesSupported = !IsRequired(PrinterAttribute.SidesSupported) ? null : Enum.GetValues(typeof(Sides)).Cast().Where(x => x != Sides.Unsupported).ToArray(),
410 | PdlOverrideSupported = !IsRequired(PrinterAttribute.PdlOverrideSupported) ? null : "attempted",
411 | MultipleOperationTimeOut = !IsRequired(PrinterAttribute.MultipleOperationTimeOut) ? null : 120,
412 | FinishingsDefault = !IsRequired(PrinterAttribute.FinishingsDefault) ? null : options.Finishings.FirstOrDefault(),
413 | FinishingsSupported = !IsRequired(PrinterAttribute.SidesSupported) ? null : options.Finishings,
414 | PrinterResolutionDefault = !IsRequired(PrinterAttribute.PrinterResolutionDefault) ? null : options.Resolution.FirstOrDefault(),
415 | PrinterResolutionSupported = !IsRequired(PrinterAttribute.PrinterResolutionSupported) ? null : [options.Resolution.FirstOrDefault()],
416 | PrintQualityDefault = !IsRequired(PrinterAttribute.PrintQualityDefault) ? null : options.PrintQuality.FirstOrDefault(),
417 | PrintQualitySupported = !IsRequired(PrinterAttribute.PrintQualitySupported) ? null : options.PrintQuality,
418 | JobPriorityDefault = !IsRequired(PrinterAttribute.JobPriorityDefault) ? null : options.JobPriority,
419 | JobPrioritySupported = !IsRequired(PrinterAttribute.JobPrioritySupported) ? null : options.JobPriority,
420 | CopiesDefault = !IsRequired(PrinterAttribute.CopiesDefault) ? null : options.Copies,
421 | CopiesSupported = !IsRequired(PrinterAttribute.CopiesSupported) ? null : new SharpIpp.Protocol.Models.Range(options.Copies, options.Copies),
422 | OrientationRequestedDefault = !IsRequired(PrinterAttribute.OrientationRequestedDefault) ? null : options.Orientation,
423 | OrientationRequestedSupported = !IsRequired(PrinterAttribute.OrientationRequestedSupported) ? null : Enum.GetValues(typeof(Orientation)).Cast().Where(x => x != Orientation.Unsupported).ToArray(),
424 | PageRangesSupported = !IsRequired(PrinterAttribute.PageRangesSupported) ? null : false,
425 | PagesPerMinute = !IsRequired(PrinterAttribute.PagesPerMinute) ? null : 20,
426 | PagesPerMinuteColor = !IsRequired(PrinterAttribute.PagesPerMinuteColor) ? null : 20,
427 | PrinterMoreInfo = !IsRequired(PrinterAttribute.PrinterMoreInfo) ? null : GetPrinterMoreInfo(),
428 | JobHoldUntilSupported = !IsRequired(PrinterAttribute.JobHoldUntilSupported) ? null : [JobHoldUntil.NoHold],
429 | JobHoldUntilDefault = !IsRequired(PrinterAttribute.JobHoldUntilDefault) ? null : JobHoldUntil.NoHold,
430 | ReferenceUriSchemesSupported = !IsRequired(PrinterAttribute.ReferenceUriSchemesSupported) ? null : [UriScheme.Ftp, UriScheme.Http, UriScheme.Https],
431 | OutputBinDefault = !IsRequired(PrinterAttribute.OutputBinDefault) ? null : options.OutputBin.FirstOrDefault(),
432 | OutputBinSupported = !IsRequired(PrinterAttribute.OutputBinSupported) ? null : options.OutputBin,
433 | MediaColDefault = !IsRequired(PrinterAttribute.MediaColDefault) ? null : new MediaCol
434 | {
435 | MediaBackCoating = MediaCoating.None,
436 | MediaBottomMargin = 10,
437 | MediaColor = "black",
438 | MediaLeftMargin = 10,
439 | MediaRightMargin = 10,
440 | MediaTopMargin = 10,
441 | MediaFrontCoating = MediaCoating.None,
442 | MediaGrain = MediaGrain.XDirection,
443 | MediaHoleCount = 0,
444 | MediaInfo = "my black color",
445 | MediaOrderCount = 1
446 | },
447 | PrintColorModeDefault = !IsRequired(PrinterAttribute.PrintColorModeDefault) ? null : options.PrintColorModes.FirstOrDefault(),
448 | PrintColorModeSupported = !IsRequired(PrinterAttribute.PrintColorModeSupported) ? null : options.PrintColorModes
449 | };
450 | }
451 |
452 | private UriSecurity GetUriSecuritySupported()
453 | {
454 | var request = httpContextAccessor.HttpContext?.Request ?? throw new Exception("Unable to access HttpContext");
455 | return request.IsHttps ? UriSecurity.Tls : UriSecurity.None;
456 | }
457 |
458 | private GetJobsResponse GetGetJobsResponse(GetJobsRequest request)
459 | {
460 | IEnumerable jobs = _jobs.Values;
461 | jobs = request.OperationAttributes?.WhichJobs switch
462 | {
463 | WhichJobs.Completed => jobs.Where(x => x.State == JobState.Completed || x.State == JobState.Aborted || x.State == JobState.Canceled),
464 | WhichJobs.NotCompleted => jobs.Where(x => x.State == JobState.Processing || x.State == JobState.Pending),
465 | _ => jobs.Where(x => x.State.HasValue)
466 | };
467 | if (request.OperationAttributes?.MyJobs ?? false)
468 | jobs = jobs.Where(x => x.UserName?.Equals(request.OperationAttributes.RequestingUserName) ?? false);
469 | jobs = jobs.OrderByDescending(x => x.State).ThenByDescending(x => x.Id);
470 | if (request.OperationAttributes?.Limit.HasValue ?? false)
471 | jobs = jobs.Take(request.OperationAttributes.Limit.Value);
472 | logger.LogInformation("System returned jobs attributes");
473 | return new GetJobsResponse
474 | {
475 | RequestId = request.RequestId,
476 | Version = request.Version,
477 | StatusCode = IppStatusCode.SuccessfulOk,
478 | Jobs = jobs.Select(x => GetJobDescriptionAttributes(x, request.OperationAttributes?.RequestedAttributes, true)).ToArray()
479 | };
480 | }
481 |
482 | private GetJobAttributesResponse GetGetJobAttributesResponse(GetJobAttributesRequest request)
483 | {
484 | var response = new GetJobAttributesResponse
485 | {
486 | RequestId = request.RequestId,
487 | Version = request.Version,
488 | StatusCode = IppStatusCode.ClientErrorNotPossible,
489 | JobAttributes = new()
490 | };
491 | var jobId = GetJobId(request);
492 | if (!jobId.HasValue)
493 | return response;
494 | if (!_jobs.TryGetValue(jobId.Value, out var job))
495 | return response;
496 | response.JobAttributes = GetJobDescriptionAttributes(job, request.OperationAttributes?.RequestedAttributes, false);
497 | response.StatusCode = IppStatusCode.SuccessfulOk;
498 | logger.LogInformation("System returned job attributes for job {id}", jobId);
499 | return response;
500 | }
501 |
502 | private JobDescriptionAttributes GetJobDescriptionAttributes(PrinterJob job, string[]? requestedAttributes, bool isBatch)
503 | {
504 | var jobAttributes = job.Requests.Select(x => x switch
505 | {
506 | CreateJobRequest createJobRequest => createJobRequest.JobTemplateAttributes,
507 | PrintJobRequest printJobRequest => printJobRequest.JobTemplateAttributes,
508 | PrintUriRequest printUriRequest => printUriRequest.JobTemplateAttributes,
509 | _ => null,
510 | }).FirstOrDefault(x => x != null);
511 | var jobName = job.Requests.Select(x => x switch
512 | {
513 | CreateJobRequest createJobRequest => createJobRequest.OperationAttributes?.JobName,
514 | PrintJobRequest printJobRequest => printJobRequest.OperationAttributes?.JobName,
515 | PrintUriRequest printUriRequest => printUriRequest.OperationAttributes?.JobName,
516 | _ => null,
517 | }).FirstOrDefault(x => x != null);
518 | var ippAttributeFidelity = job.Requests.Select(x => x switch
519 | {
520 | CreateJobRequest createJobRequest => createJobRequest.OperationAttributes?.IppAttributeFidelity,
521 | PrintJobRequest printJobRequest => printJobRequest.OperationAttributes?.IppAttributeFidelity,
522 | PrintUriRequest printUriRequest => printUriRequest.OperationAttributes?.IppAttributeFidelity,
523 | _ => null,
524 | }).FirstOrDefault(x => x != null);
525 | var compression = job.Requests.Select(x => x switch
526 | {
527 | PrintJobRequest printJobRequest => printJobRequest.OperationAttributes?.Compression,
528 | PrintUriRequest printUriRequest => printUriRequest.OperationAttributes?.Compression,
529 | SendDocumentRequest sendDocumentRequest => sendDocumentRequest.OperationAttributes?.Compression,
530 | SendUriRequest sendUriRequest => sendUriRequest.OperationAttributes?.Compression,
531 | _ => null,
532 | }).FirstOrDefault(x => x != null);
533 |
534 |
535 | bool IsRequired(string attributeName)
536 | {
537 | if (requestedAttributes is null || requestedAttributes.Length == 0)
538 | return !isBatch;
539 | if(requestedAttributes.All(x => x == "all"))
540 | return true;
541 | return requestedAttributes.Contains(attributeName);
542 | }
543 | var attributes = new JobDescriptionAttributes
544 | {
545 | JobId = job.Id,
546 | JobName = !IsRequired(JobAttribute.JobName) ? null : jobName,
547 | JobUri = $"{GetPrinterUrl()}/{job.Id}",
548 | JobPrinterUri = !IsRequired(JobAttribute.JobPrinterUri) ? null : GetPrinterUrl(),
549 | JobState = !IsRequired(JobAttribute.JobState) ? null : job.State,
550 | JobStateReasons = !IsRequired(JobAttribute.JobState) ? null : [JobStateReason.None],
551 | DateTimeAtCreation = !IsRequired(JobAttribute.DateTimeAtCreation) ? null : job.CreatedDateTime,
552 | TimeAtCreation = !IsRequired(JobAttribute.TimeAtCreation) ? null : (int)(job.CreatedDateTime - _startTime).TotalSeconds,
553 | DateTimeAtProcessing = !IsRequired(JobAttribute.DateTimeAtProcessing) ? null : job.ProcessingDateTime ?? DateTimeOffset.MinValue,
554 | TimeAtProcessing = !IsRequired(JobAttribute.TimeAtProcessing) ? null : job.ProcessingDateTime.HasValue ? (int)(job.ProcessingDateTime.Value - _startTime).TotalSeconds : -1,
555 | DateTimeAtCompleted = !IsRequired(JobAttribute.DateTimeAtCompleted) ? null : job.CompletedDateTime ?? DateTimeOffset.MinValue,
556 | TimeAtCompleted = !IsRequired(JobAttribute.TimeAtCompleted) ? null : job.CompletedDateTime.HasValue ? (int)(job.CompletedDateTime.Value - _startTime).TotalSeconds : -1,
557 | JobOriginatingUserName = !IsRequired(JobAttribute.JobOriginatingUserName) ? null : job.UserName,
558 | JobPrinterUpTime = !IsRequired(JobAttribute.JobPrinterUpTime) ? null : (int)(dateTimeOffsetProvider.UtcNow - _startTime).TotalSeconds
559 | };
560 | return attributes;
561 | }
562 |
563 | private static CUPSGetPrintersResponse GetCUPSGetPrintersResponse(CUPSGetPrintersRequest request)
564 | {
565 | return new CUPSGetPrintersResponse
566 | {
567 | RequestId = request.RequestId,
568 | Version = request.Version,
569 | StatusCode = IppStatusCode.SuccessfulOk
570 | };
571 | }
572 |
573 | private CreateJobResponse GetCreateJobResponse(CreateJobRequest request)
574 | {
575 | var response = new CreateJobResponse
576 | {
577 | RequestId = request.RequestId,
578 | Version = request.Version,
579 | JobState = JobState.Pending,
580 | StatusCode = IppStatusCode.ClientErrorNotPossible,
581 | JobStateReasons = [JobStateReason.None]
582 | };
583 | var job = new PrinterJob(GetNextValue(), request.OperationAttributes?.RequestingUserName, dateTimeOffsetProvider.UtcNow);
584 | response.JobId = job.Id;
585 | response.JobUri = $"{GetPrinterUrl()}/{job.Id}";
586 | FillWithDefaultValues(job.Id, request.OperationAttributes ??= new());
587 | FillWithDefaultValues(request.JobTemplateAttributes ??= new());
588 | job.Requests.Add(request);
589 | if (!_jobs.TryAdd(job.Id, job))
590 | return response;
591 | response.StatusCode = IppStatusCode.SuccessfulOk;
592 | logger.LogInformation("Job {id} has been added to queue", job.Id);
593 | return response;
594 | }
595 |
596 | private async Task GetCancelJobResponseAsync(CancelJobRequest request)
597 | {
598 | var response = new CancelJobResponse
599 | {
600 | RequestId = request.RequestId,
601 | Version = request.Version,
602 | StatusCode = IppStatusCode.ClientErrorNotPossible
603 | };
604 | var jobId = GetJobId(request);
605 | if (!jobId.HasValue)
606 | return response;
607 | if (!_jobs.TryGetValue(jobId.Value, out var job))
608 | return response;
609 | var copy = new PrinterJob(job);
610 | if (!await copy.TrySetStateAsync(JobState.Canceled, dateTimeOffsetProvider.UtcNow))
611 | return response;
612 | if (!_jobs.TryUpdate(jobId.Value, copy, job))
613 | return response;
614 | response.StatusCode = IppStatusCode.SuccessfulOk;
615 | logger.LogInformation("Job {id} has been canceled", jobId);
616 | return response;
617 | }
618 |
619 | public async Task GetPendingJobAsync()
620 | {
621 | if (_isPaused)
622 | return null;
623 | foreach (var job in _jobs.Values.Where(x => x.State == JobState.Pending).OrderBy(x => x.Id))
624 | {
625 | var copy = new PrinterJob(job);
626 | if (!await copy.TrySetStateAsync(JobState.Processing, dateTimeOffsetProvider.UtcNow))
627 | continue;
628 | if (!_jobs.TryUpdate(job.Id, copy, job))
629 | continue;
630 | return copy;
631 | }
632 | return null;
633 | }
634 |
635 | public async Task AddCompletedJobAsync(int jobId)
636 | {
637 | if (!_jobs.TryGetValue(jobId, out var job))
638 | return;
639 | var copy = new PrinterJob(job);
640 | if (!await copy.TrySetStateAsync(JobState.Completed, dateTimeOffsetProvider.UtcNow))
641 | return;
642 | if (!_jobs.TryUpdate(jobId, copy, job))
643 | return;
644 | logger.LogInformation("Job {id} has been completed", job.Id);
645 | }
646 |
647 | public async Task AddAbortedJobAsync(int jobId, Exception ex)
648 | {
649 | if (!_jobs.TryGetValue(jobId, out var job))
650 | return;
651 | var copy = new PrinterJob(job);
652 | if (!await copy.TrySetStateAsync(JobState.Aborted, dateTimeOffsetProvider.UtcNow))
653 | return;
654 | if (!_jobs.TryUpdate(jobId, copy, job))
655 | return;
656 | logger.LogError(ex, "Job {id} has been aborted", job.Id);
657 | }
658 |
659 | private async Task GetPrintJobResponseAsync(PrintJobRequest request)
660 | {
661 | var response = new PrintJobResponse
662 | {
663 | RequestId = request.RequestId,
664 | Version = request.Version,
665 | JobState = JobState.Pending,
666 | StatusCode = IppStatusCode.ClientErrorNotPossible,
667 | JobStateReasons = [JobStateReason.None]
668 | };
669 | var job = new PrinterJob(GetNextValue(), request.OperationAttributes?.RequestingUserName, dateTimeOffsetProvider.UtcNow);
670 | response.JobId = job.Id;
671 | response.JobUri = $"{GetPrinterUrl()}/{job.Id}";
672 | FillWithDefaultValues(job.Id, request.OperationAttributes ??= new());
673 | FillWithDefaultValues(request.JobTemplateAttributes ??= new());
674 | job.Requests.Add(request);
675 | if (!await job.TrySetStateAsync(JobState.Pending, dateTimeOffsetProvider.UtcNow))
676 | return response;
677 | if (!_jobs.TryAdd(job.Id, job))
678 | return response;
679 | response.StatusCode = IppStatusCode.SuccessfulOk;
680 | logger.LogInformation("Job {id} has been added to queue", job.Id);
681 | return response;
682 | }
683 |
684 | private string GetPrinterUrl(string? path = null)
685 | {
686 | var request = httpContextAccessor.HttpContext?.Request ?? throw new Exception("Unable to access HttpContext");
687 | return $"ipp://{request.Host}{request.PathBase}{(path is null ? request.Path : path)}";
688 | }
689 |
690 | private string GetPrinterMoreInfo()
691 | {
692 | var request = httpContextAccessor.HttpContext?.Request ?? throw new Exception("Unable to access HttpContext");
693 | return $"{request.Scheme}://{request.Host}{request.PathBase}";
694 | }
695 |
696 | private static int? GetJobId(IIppJobRequest request)
697 | {
698 | if(request.OperationAttributes is not JobOperationAttributes jobOperationAttributes)
699 | return null;
700 | if (jobOperationAttributes.JobUri != null && int.TryParse(jobOperationAttributes.JobUri.Segments.LastOrDefault(), out int idFromUri))
701 | return idFromUri;
702 | return jobOperationAttributes.JobId;
703 | }
704 |
705 | private void FillWithDefaultValues(JobTemplateAttributes? attributes)
706 | {
707 | if (attributes == null)
708 | return;
709 | var options = printerOptions.Value;
710 | attributes.PrintScaling ??= options.PrintScaling.FirstOrDefault();
711 | attributes.Sides ??= options.Sides.FirstOrDefault();
712 | attributes.Media ??= options.Media.FirstOrDefault();
713 | attributes.PrinterResolution ??= options.Resolution.FirstOrDefault();
714 | attributes.Finishings ??= options.Finishings.FirstOrDefault();
715 | attributes.PrintQuality ??= options.PrintQuality.FirstOrDefault();
716 | attributes.JobPriority ??= options.JobPriority;
717 | attributes.Copies ??= options.Copies;
718 | attributes.OrientationRequested ??= options.Orientation;
719 | attributes.JobHoldUntil ??= options.JobHoldUntil;
720 | attributes.PrintColorMode ??= options.PrintColorModes.FirstOrDefault();
721 | }
722 |
723 | private void FillWithDefaultValues(SendDocumentOperationAttributes? attributes)
724 | {
725 | if (attributes is null)
726 | return;
727 | var options = printerOptions.Value;
728 | if (string.IsNullOrEmpty(attributes.DocumentFormat))
729 | attributes.DocumentFormat = options.DocumentFormat;
730 | }
731 |
732 | private void FillWithDefaultValues(int jobId, PrintJobOperationAttributes? attributes)
733 | {
734 | if (attributes is null)
735 | return;
736 | var options = printerOptions.Value;
737 | if (string.IsNullOrEmpty(attributes.DocumentFormat))
738 | attributes.DocumentFormat = options.DocumentFormat;
739 | FillWithDefaultValues(jobId, attributes as CreateJobOperationAttributes);
740 | }
741 |
742 | private void FillWithDefaultValues(int jobId, CreateJobOperationAttributes? attributes)
743 | {
744 | if (attributes is null)
745 | return;
746 | if (string.IsNullOrEmpty(attributes.JobName))
747 | attributes.JobName = $"Job {jobId}";
748 | }
749 |
750 | public async ValueTask DisposeAsync()
751 | {
752 | await DisposeAsyncCore().ConfigureAwait(false);
753 | Dispose(disposing: false);
754 | GC.SuppressFinalize(this);
755 | }
756 |
757 | protected virtual async ValueTask DisposeAsyncCore()
758 | {
759 | foreach (var job in _jobs.Values)
760 | await job.DisposeAsync();
761 | _jobs.Clear();
762 | }
763 |
764 | protected virtual void Dispose(bool disposing)
765 | {
766 | if (disposedValue)
767 | return;
768 | if (disposing)
769 | {
770 | foreach (var job in _jobs.Values)
771 | job.Dispose();
772 | _jobs.Clear();
773 | }
774 | disposedValue = true;
775 | }
776 |
777 | public void Dispose()
778 | {
779 | Dispose(disposing: true);
780 | GC.SuppressFinalize(this);
781 | }
782 | }
783 |
--------------------------------------------------------------------------------