├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── GopherServer.Core ├── Configuration │ ├── ExtensionMappingCollection.cs │ ├── ExtensionMappingConfigSection.cs │ ├── ExtensionMappingElement.cs │ └── ServerSettings.cs ├── GopherServer.Core.csproj ├── Helpers │ ├── FileTypeHelpers.cs │ ├── HtmlExtensions.cs │ ├── HtmlToText.cs │ ├── HttpClient.cs │ ├── ImageToGif.cs │ ├── StringExtensions.cs │ └── TypeExtensions.cs ├── Models │ ├── DirectoryItem.cs │ ├── ExternalUrlItem.cs │ └── ItemType.cs ├── Providers │ ├── IServerProvider.cs │ ├── ProviderFactory.cs │ └── ServerProviderBase.cs ├── Results │ ├── BaseResult.cs │ ├── ByteResult.cs │ ├── DirectoryResult.cs │ ├── ErrorResult.cs │ ├── GifResult.cs │ ├── HtmlResult.cs │ ├── ProxyResult.cs │ ├── TextResult.cs │ └── UrlResult.cs └── Routes │ ├── NamedGroupRoute.cs │ ├── PrebuiltRoutes.cs │ ├── Route.cs │ └── TypedRoute.cs ├── GopherServer.Providers.FileProvider ├── FileProvider.cs ├── GopherServer.Providers.FileProvider.csproj ├── Results │ ├── DirectoryInfoResult.cs │ └── FileResult.cs └── Settings.cs ├── GopherServer.Providers.MacintoshGarden ├── Extensions │ └── AngleSharpExtensions.cs ├── GopherServer.Providers.MacintoshGarden.csproj ├── MacGardenController.cs ├── MacintoshGardenProvider.cs ├── Models │ ├── DownloadDetails.cs │ ├── DownloadLink.cs │ ├── SearchResult.cs │ ├── SearchResults.cs │ └── SoftwareItem.cs └── Results │ ├── SearchResults.cs │ └── SoftwareResult.cs ├── GopherServer.Providers.Rss ├── Data │ ├── Db.cs │ ├── Feed.cs │ ├── FeedCache.cs │ ├── User.cs │ └── UserFeed.cs ├── GopherResults │ ├── FeedItemResult.cs │ ├── FeedListingResult.cs │ └── FeedResult.cs ├── GopherServer.Providers.Rss.csproj ├── RssController.cs ├── RssProvider.cs ├── Syndication │ ├── FeedDetails.cs │ └── Syndication.cs └── sqlite3.dll ├── GopherServer.Providers.WpJson ├── Extensions │ └── WordPressExtensions.cs ├── GopherServer.Providers.WpJson.csproj ├── WordPressClient.cs └── WordPressProvider.cs ├── GopherServer.sln ├── GopherServer ├── GopherServer.csproj ├── Program.cs ├── Server.cs └── app.sample.config ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | 290 | *.db 291 | *.db-shm 292 | *.db-wal 293 | app.config 294 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/GopherServer/bin/Debug/netcoreapp2.2/GopherServer.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/GopherServer", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/GopherServer/GopherServer.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/GopherServer/GopherServer.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/GopherServer/GopherServer.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /GopherServer.Core/Configuration/ExtensionMappingCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace GopherServer.Core.Configuration 4 | { 5 | public class ExtensionMappingCollection : ConfigurationElementCollection 6 | { 7 | protected override ConfigurationElement CreateNewElement() => new ExtensionMappingElement(); 8 | protected override object GetElementKey(ConfigurationElement element) => ((ExtensionMappingElement)element).FileExtension; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /GopherServer.Core/Configuration/ExtensionMappingConfigSection.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace GopherServer.Core.Configuration 4 | { 5 | public class ExtensionMappingConfigSection : ConfigurationSection 6 | { 7 | [ConfigurationProperty("", IsDefaultCollection = true)] 8 | [ConfigurationCollection(typeof(ExtensionMappingCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")] 9 | public ExtensionMappingCollection ExtensionMappings 10 | { 11 | get => (ExtensionMappingCollection)this[""]; 12 | set => this[""] = value; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GopherServer.Core/Configuration/ExtensionMappingElement.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace GopherServer.Core.Configuration 4 | { 5 | public class ExtensionMappingElement : ConfigurationElement 6 | { 7 | [ConfigurationProperty("fileExtension", IsKey = true, IsRequired = true)] 8 | public string FileExtension 9 | { 10 | get => (string)base["fileExtension"]; 11 | set => base["fileExtension"] = value; 12 | } 13 | 14 | [ConfigurationProperty("gopherType", IsRequired = true)] 15 | public string GopherType 16 | { 17 | get => (string)base["gopherType"]; 18 | set => base["gopherType"] = value; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GopherServer.Core/Configuration/ServerSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace GopherServer.Core.Configuration 4 | { 5 | public static class ServerSettings 6 | { 7 | private static ExtensionMappingCollection extensionMappings; 8 | 9 | public static string BoundIP => ConfigurationManager.AppSettings["boundIP"]; 10 | public static int BoundPort => int.Parse(ConfigurationManager.AppSettings["boundPort"]); 11 | public static string ProviderName => ConfigurationManager.AppSettings["providerName"]; 12 | public static string PublicHostname => ConfigurationManager.AppSettings["publicHostname"]; 13 | public static int PublicPort => int.Parse(ConfigurationManager.AppSettings["publicPort"]); 14 | public static bool ResampleImages => bool.Parse(ConfigurationManager.AppSettings["resampleImages"]); 15 | public static bool ResizeImages => bool.Parse(ConfigurationManager.AppSettings["resizeImages"]); 16 | 17 | public static ExtensionMappingCollection FileTypeMappings 18 | { 19 | get 20 | { 21 | if (extensionMappings != null) 22 | return extensionMappings; 23 | 24 | var config = 25 | ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); 26 | 27 | var section = config.GetSection("gopherFileMappings") as ExtensionMappingConfigSection; 28 | 29 | extensionMappings = section.ExtensionMappings; 30 | 31 | return extensionMappings; 32 | } 33 | } 34 | 35 | public static int? MaximumWidth 36 | { 37 | get 38 | { 39 | int value; 40 | if (int.TryParse(ConfigurationManager.AppSettings["maximumWidth"], out value)) 41 | return value; 42 | return null; 43 | } 44 | } 45 | 46 | public static int? MaximumHeight 47 | { 48 | get 49 | { 50 | int value; 51 | if (int.TryParse(ConfigurationManager.AppSettings["maximumHeight"], out value)) 52 | return value; 53 | return null; 54 | } 55 | } 56 | 57 | public static int? MaximumBitDepth 58 | { 59 | get 60 | { 61 | int value; 62 | if (int.TryParse(ConfigurationManager.AppSettings["maximumBitDepth"], out value)) 63 | return value; 64 | return null; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /GopherServer.Core/GopherServer.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /GopherServer.Core/Helpers/FileTypeHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using GopherServer.Core.Configuration; 5 | using GopherServer.Core.Models; 6 | 7 | namespace GopherServer.Core.Helpers 8 | { 9 | public static class FileTypeHelpers 10 | { 11 | public static ItemType GetItemTypeFromFileName(string filename) 12 | { 13 | var types = ServerSettings.FileTypeMappings.OfType(); 14 | var extension = Path.GetExtension(filename); 15 | var fileType = types.FirstOrDefault(f => string.Equals(f.FileExtension, extension, StringComparison.InvariantCultureIgnoreCase)); 16 | 17 | if (fileType == null) 18 | return ItemType.BINARY; // File tends to return asci in Seamonkey at least... 19 | var gopherType = ItemType.Types.Values.FirstOrDefault(p => p.Name == fileType.GopherType); 20 | if (gopherType == null) 21 | return ItemType.BINARY; 22 | 23 | return gopherType; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GopherServer.Core/Helpers/HtmlExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net; 4 | using HtmlAgilityPack; 5 | using GopherServer.Core.Models; 6 | 7 | namespace GopherServer.Core.Helpers 8 | { 9 | public static class HtmlExtensions 10 | { 11 | /// 12 | /// Decodes the HTML into a standard string 13 | /// 14 | /// 15 | /// 16 | public static string CleanHtml(this string text) => WebUtility.HtmlDecode(text); 17 | 18 | /// 19 | /// Converts HTML into text 20 | /// 21 | /// 22 | /// 23 | public static string HtmlToText(this string html) => new HtmlToText().ConvertHtml(html); 24 | //var htmlDoc = new HtmlDocument(); 25 | //htmlDoc.LoadHtml(html); 26 | //return htmlDoc.DocumentNode.InnerText; 27 | 28 | public static string GetTextFromXPath(this HtmlNode node, string xPath) => node.SelectSingleNode(xPath).InnerText; 29 | public static string GetUrlFromXPath(this HtmlNode node, string xPath) => node.SelectSingleNode(xPath).Attributes["href"].Value; 30 | 31 | /// 32 | /// Converst the specified HTML in to a list of directory items 33 | /// 34 | /// 35 | /// 36 | public static List ToDirectoryItems(this string html, string urlSelector = "URL:", string proxySelector = "PROXY:", string gifSelector = "GIF:") 37 | { 38 | // We're going to use the HTML agility pack to parse out the HTML into info 39 | var htmlDoc = new HtmlDocument(); 40 | htmlDoc.LoadHtml(html); 41 | 42 | var nodes = htmlDoc.DocumentNode.ChildNodes; 43 | 44 | return nodes.SelectMany(n=>n.ToDirectoryItems(urlSelector, proxySelector,gifSelector)).ToList(); 45 | } 46 | 47 | /// 48 | /// Turns a HTML node into Directory items (links, images, text) 49 | /// 50 | /// 51 | /// 52 | private static List ToDirectoryItems(this HtmlNode node, string urlSelector = "URL:", string proxySelector = "PROXY:", string gifSelector = "GIF:") 53 | { 54 | var items = new List(); 55 | 56 | var item = new DirectoryItem(); 57 | if (node.Name == "a") 58 | { 59 | // Link 60 | if (!string.IsNullOrEmpty(node.InnerText)) 61 | { 62 | var httpLink = new DirectoryItem(); 63 | httpLink.ItemType = ItemType.HTML; 64 | httpLink.Description = node.InnerText.CleanHtml() + " (HTTP)"; 65 | httpLink.Selector = urlSelector + node.GetAttributeValue("href", ""); 66 | items.Add(httpLink); 67 | 68 | var gopherLink = new DirectoryItem(); 69 | gopherLink.ItemType = ItemType.DIRECTORY; 70 | gopherLink.Description = node.InnerText.CleanHtml() + " (Gopher Proxy)"; 71 | gopherLink.Selector = proxySelector + node.GetAttributeValue("href", ""); 72 | items.Add(gopherLink); 73 | } 74 | } 75 | else if (node.Name == "img") 76 | { 77 | // Image 78 | item.ItemType = ItemType.GIF; 79 | item.Description = node.GetAttributeValue("alt", null) ?? "Image"; 80 | item.Selector = gifSelector + node.GetAttributeValue("src", ""); 81 | items.Add(item); 82 | } 83 | else if (node.NodeType == HtmlNodeType.Text && node.ParentNode.Name != "a") 84 | { 85 | var text = node.InnerText.CleanHtml(); 86 | items.AddRange(text.WrapToDirectoryItems(80)); 87 | // items.Add(new DirectoryItem(" ")); 88 | } 89 | 90 | if (node.HasChildNodes) 91 | { 92 | items.AddRange(node.ChildNodes.SelectMany(n=>n.ToDirectoryItems(urlSelector, proxySelector, gifSelector))); 93 | } 94 | 95 | return items; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /GopherServer.Core/Helpers/HtmlToText.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using HtmlAgilityPack; 3 | 4 | namespace GopherServer.Core.Helpers 5 | { 6 | /// 7 | /// Straight from the HTML Agility Samples https://github.com/zzzprojects/html-agility-pack/blob/master/src/Samples/Html2Txt/HtmlConvert.cs 8 | /// 9 | public class HtmlToText 10 | { 11 | #region Public Methods 12 | 13 | public string Convert(string path) 14 | { 15 | HtmlDocument doc = new HtmlDocument(); 16 | doc.Load(path); 17 | 18 | StringWriter sw = new StringWriter(); 19 | ConvertTo(doc.DocumentNode, sw); 20 | sw.Flush(); 21 | 22 | return sw.ToString(); 23 | } 24 | 25 | public string ConvertHtml(string html) 26 | { 27 | HtmlDocument doc = new HtmlDocument(); 28 | doc.LoadHtml(html); 29 | 30 | StringWriter sw = new StringWriter(); 31 | ConvertTo(doc.DocumentNode, sw); 32 | sw.Flush(); 33 | 34 | return sw.ToString(); 35 | } 36 | 37 | public void ConvertTo(HtmlNode node, TextWriter outText) 38 | { 39 | string html; 40 | switch (node.NodeType) 41 | { 42 | case HtmlNodeType.Comment: 43 | // don't output comments 44 | break; 45 | 46 | case HtmlNodeType.Document: 47 | ConvertContentTo(node, outText); 48 | break; 49 | 50 | case HtmlNodeType.Text: 51 | // script and style must not be output 52 | string parentName = node.ParentNode.Name; 53 | if ((parentName == "script") || (parentName == "style")) 54 | break; 55 | 56 | // get text 57 | html = ((HtmlTextNode)node).Text; 58 | 59 | // is it in fact a special closing node output as text? 60 | if (HtmlNode.IsOverlappedClosingElement(html)) 61 | break; 62 | 63 | // check the text is meaningful and not a bunch of whitespaces 64 | if (html.Trim().Length > 0) 65 | { 66 | outText.Write(HtmlEntity.DeEntitize(html)); 67 | } 68 | break; 69 | 70 | case HtmlNodeType.Element: 71 | switch (node.Name) 72 | { 73 | case "p": 74 | // treat paragraphs as crlf 75 | outText.Write("\r\n"); 76 | break; 77 | } 78 | 79 | if (node.HasChildNodes) 80 | { 81 | ConvertContentTo(node, outText); 82 | } 83 | break; 84 | } 85 | } 86 | 87 | #endregion 88 | 89 | #region Private Methods 90 | 91 | private void ConvertContentTo(HtmlNode node, TextWriter outText) 92 | { 93 | foreach (HtmlNode subnode in node.ChildNodes) 94 | { 95 | ConvertTo(subnode, outText); 96 | } 97 | } 98 | 99 | #endregion 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /GopherServer.Core/Helpers/HttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace GopherServer.Core.Helpers 4 | { 5 | public static class HttpHelpers 6 | { 7 | private static HttpClient client = new HttpClient(); 8 | 9 | public static byte[] DownloadFile(string url) => client.GetByteArrayAsync(url).Result; 10 | 11 | public static string GetUrl(string url) => System.Text.Encoding.UTF8.GetString(client.GetByteArrayAsync(url).Result); 12 | // return client.GetStringAsync(url).Result; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GopherServer.Core/Helpers/ImageToGif.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Drawing.Imaging; 4 | using System.IO; 5 | 6 | namespace GopherServer.Core.Helpers 7 | { 8 | public static class ImageToGif 9 | { 10 | /// 11 | /// Converts the specified image to a gif 12 | /// 13 | /// 14 | /// 15 | public static byte[] ToGif(this Image image) 16 | { 17 | using (var stream = new MemoryStream()) 18 | { 19 | ToGif(image, stream); 20 | return stream.ToArray(); 21 | } 22 | } 23 | 24 | 25 | /// 26 | /// Converts the image byte array to gif 27 | /// 28 | /// 29 | /// 30 | public static byte[] ConvertToGif(this byte[] bytes) 31 | { 32 | using (var stream = new MemoryStream(bytes)) 33 | { 34 | using (var image = Image.FromStream(stream)) 35 | return image.ToGif(); 36 | } 37 | } 38 | 39 | /// 40 | /// Saves the passed URL to gif 41 | /// 42 | /// 43 | /// 44 | public static byte[] ConvertImageToGif(string url) 45 | { 46 | return HttpHelpers.DownloadFile(url).ConvertToGif(); 47 | } 48 | 49 | /// 50 | /// Saves the specified image to the specified stream 51 | /// 52 | /// 53 | /// 54 | public static void ToGif(this Image image, Stream stream) 55 | { 56 | if (Configuration.ServerSettings.ResizeImages == false && 57 | Configuration.ServerSettings.ResampleImages == false) 58 | { 59 | image.Save(stream, ImageFormat.Gif); 60 | image.Dispose(); 61 | return; 62 | } 63 | 64 | Bitmap original; 65 | if (Configuration.ServerSettings.ResizeImages) 66 | { 67 | original = new Bitmap(image, ResizeKeepAspect(image.Size, 68 | Configuration.ServerSettings.MaximumWidth.GetValueOrDefault(512), 69 | Configuration.ServerSettings.MaximumHeight.GetValueOrDefault(512))); 70 | 71 | if (Configuration.ServerSettings.ResampleImages == false) 72 | original.Save(stream, ImageFormat.Gif); 73 | } 74 | else original = new Bitmap(image); 75 | 76 | if (Configuration.ServerSettings.ResampleImages) 77 | { 78 | // This might not be the best way...no differing here... 79 | var rectangle = new Rectangle(0, 0, original.Width, original.Height); 80 | 81 | PixelFormat format; 82 | switch (Configuration.ServerSettings.MaximumBitDepth) 83 | { 84 | case 1: 85 | format = PixelFormat.Format1bppIndexed; 86 | break; 87 | case 4: 88 | format = PixelFormat.Format4bppIndexed; 89 | break; 90 | case 8: 91 | format = PixelFormat.Format8bppIndexed; 92 | break; 93 | default: 94 | throw new Exception("Invalid MaximumBitDepth specified in configuration. Must 1, 4 or 8."); 95 | } 96 | 97 | using (var bmp1bpp = original.Clone(rectangle, format)) 98 | bmp1bpp.Save(stream, ImageFormat.Gif); 99 | } 100 | 101 | if (original != null) 102 | original.Dispose(); 103 | 104 | if (image != null) 105 | image.Dispose(); 106 | } 107 | 108 | public static Size ResizeKeepAspect(Size src, int maxWidth, int maxHeight) 109 | { 110 | var rnd = Math.Min(maxWidth / (decimal)src.Width, maxHeight / (decimal)src.Height); 111 | return new Size((int)Math.Round(src.Width * rnd), (int)Math.Round(src.Height * rnd)); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /GopherServer.Core/Helpers/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using GopherServer.Core.Models; 5 | 6 | namespace GopherServer.Core.Helpers 7 | { 8 | public static class StringExtensions 9 | { 10 | /// 11 | /// Replaces characters that stuff our clients up 12 | /// 13 | /// 14 | /// 15 | public static string CleanString(this string text) 16 | { 17 | return text.Replace("\r", "") 18 | .Replace("\t", "") 19 | .Replace("\n", "") 20 | .Trim(); 21 | } 22 | 23 | /// 24 | /// Wraps text to the specified column length 25 | /// 26 | /// 27 | /// 28 | /// 29 | public static List WrapText(this string text, int cols = 80) 30 | { 31 | var words = text.Split(new string[] { " ", "\r\n", "\n" }, StringSplitOptions.None); 32 | 33 | List wordList = new List(); 34 | 35 | string line = ""; 36 | foreach (string word in words) 37 | { 38 | if (!string.IsNullOrWhiteSpace(word)) 39 | { 40 | var newLine = string.Join(" ", line, word).Trim(); 41 | if (newLine.Length >= cols) 42 | { 43 | wordList.Add(line); 44 | line = word; 45 | } 46 | else 47 | { 48 | line = newLine; 49 | } 50 | } 51 | } 52 | 53 | if (line.Length > 0) 54 | wordList.Add(line); 55 | 56 | return wordList; 57 | } 58 | 59 | /// 60 | /// Wraps the specified string into DirectoryItem Info lines 61 | /// 62 | /// 63 | /// 64 | /// 65 | public static List WrapToDirectoryItems(this string text, int cols = 80) 66 | { 67 | return text.WrapText(cols).Select(l => new DirectoryItem(l)).ToList(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /GopherServer.Core/Helpers/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GopherServer.Core.Helpers 4 | { 5 | public static class TypeExtensions 6 | { 7 | /// 8 | /// Converts string to the specified type. Useful for regex matches! 9 | /// 10 | /// 11 | /// 12 | /// 13 | public static T ToType(this string value) => (T)Convert.ChangeType(value, typeof(T)); 14 | 15 | public static object ToType(this string value, Type type) => Convert.ChangeType(value, type); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GopherServer.Core/Models/DirectoryItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace GopherServer.Core.Models 6 | { 7 | public class DirectoryItem 8 | { 9 | public DirectoryItem() 10 | { } 11 | 12 | /// 13 | /// Add an info item 14 | /// 15 | /// 16 | public DirectoryItem(string infoText) 17 | { 18 | this.ItemType = ItemType.INFO; 19 | this.Description = infoText; 20 | } 21 | 22 | /// 23 | /// Add a custom type 24 | /// 25 | /// 26 | /// 27 | /// 28 | public DirectoryItem(ItemType type, string description, string selector) 29 | { 30 | this.ItemType = type; 31 | this.Description = description; 32 | this.Selector = selector; 33 | } 34 | 35 | /// 36 | /// Quick add a directory link 37 | /// 38 | /// 39 | /// 40 | public DirectoryItem(string linkText, string directorySelector) 41 | { 42 | this.ItemType = ItemType.DIRECTORY; 43 | this.Description = linkText; 44 | this.Selector = directorySelector; 45 | } 46 | 47 | public ItemType ItemType { get; set; } 48 | 49 | public string Description { get; set; } 50 | 51 | /// 52 | /// Selector shouldn't be longer than 255 characters according to the RFC 53 | /// Look into limiting it - we might need to implement a selector cache 54 | /// 55 | public string Selector { get; set; } 56 | 57 | public string Host { get; set; } 58 | 59 | public int Port { get; set; } 60 | 61 | public string[] Extras { get; set; } 62 | 63 | public override string ToString() 64 | { 65 | var sb = new StringBuilder(); 66 | if (this.ItemType != null) 67 | { 68 | sb.Append(this.ItemType.Code); 69 | sb.Append(this.Description); 70 | sb.Append("\t"); 71 | sb.Append(this.Selector); 72 | sb.Append("\t"); 73 | sb.Append(this.Host); 74 | sb.Append("\t"); 75 | sb.Append(this.Port); 76 | 77 | if (this.Extras != null) 78 | { 79 | foreach (var i in Extras) 80 | { 81 | sb.Append("\t"); 82 | sb.Append(i); 83 | } 84 | } 85 | } 86 | else 87 | { 88 | sb.Append(this.Description); 89 | } 90 | 91 | return sb.ToString(); 92 | } 93 | 94 | public static DirectoryItem Parse(string line) 95 | { 96 | var item = new DirectoryItem(); 97 | var parts = line.Split('\t'); 98 | if (parts.Length == 0) 99 | throw new Exception("No item type: " + line); 100 | 101 | item.ItemType = ItemType.Types[parts[0][0]]; 102 | item.Description = string.Concat(parts[0].Skip(1)); 103 | 104 | if (parts.Length > 1) 105 | item.Selector = parts[1]; 106 | else 107 | item.Selector = ""; 108 | 109 | if (parts.Length > 2) 110 | item.Host = parts[2]; 111 | else 112 | item.Host = "null.host"; 113 | 114 | if (parts.Length > 3) 115 | { 116 | int port; 117 | if (int.TryParse(parts[3], out port)) 118 | item.Port = port; 119 | else 120 | item.Port = 0; 121 | } 122 | else 123 | item.Port = 0; 124 | 125 | if (parts.Length >= 4) 126 | { 127 | item.Extras = parts.Skip(4).ToArray(); 128 | } 129 | 130 | return item; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /GopherServer.Core/Models/ExternalUrlItem.cs: -------------------------------------------------------------------------------- 1 | namespace GopherServer.Core.Models 2 | { 3 | public class ExternalUrlItem : DirectoryItem 4 | { 5 | public ExternalUrlItem(string text, string url) 6 | { 7 | this.ItemType = ItemType.HTML; 8 | this.Description = text; 9 | this.Selector = "URL:" + url; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GopherServer.Core/Models/ItemType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace GopherServer.Core.Models 6 | { 7 | public class ItemType 8 | { 9 | public ItemType(char? gopherType, string name, string shortName) 10 | { 11 | this.Code = gopherType; 12 | this.Name = name; 13 | this.ShortName = shortName; 14 | } 15 | 16 | public char? Code { get; set; } 17 | 18 | public string Name { get; set; } 19 | 20 | public string ShortName { get; set; } 21 | 22 | public static ItemType FILE = new ItemType('0', "FILE", "TXT"); 23 | public static ItemType DIRECTORY = new ItemType('1', "DIRECTORY", "DIR"); 24 | public static ItemType PHONEBOOK = new ItemType('2', "PHONEBOOK", "PHO"); 25 | public static ItemType ERROR = new ItemType('3', "ERROR", "ERR"); 26 | public static ItemType BINHEX = new ItemType('4', "BINHEX", "HEX"); 27 | public static ItemType DOSARCHIVE = new ItemType('5', "DOSARCHIVE", "ARC"); 28 | public static ItemType UUENCODED = new ItemType('6', "UUENCODED", "UUE"); 29 | public static ItemType INDEXSEARCH = new ItemType('7', "INDEXSEARCH", "QRY"); 30 | public static ItemType TELNET = new ItemType('8', "TELNET", "TEL"); 31 | public static ItemType BINARY = new ItemType('9', "BINARY", "BIN"); 32 | 33 | public static ItemType REDUNDANT = new ItemType('+', "REDUNDANT", "DUP"); 34 | public static ItemType TN3270 = new ItemType('T', "TN3270", "TN3"); 35 | public static ItemType GIF = new ItemType('g', "GIF", "GIF"); 36 | public static ItemType IMAGE = new ItemType('I', "IMAGE", "IMG"); 37 | 38 | public static ItemType INFO = new ItemType('i', "INFO", "NFO"); 39 | public static ItemType HTML = new ItemType('h', "HTML", "HTM"); 40 | public static ItemType AUDIO = new ItemType('s', "AUDIO", "SND"); 41 | public static ItemType PNG = new ItemType('p', "PNG", "PNG"); 42 | public static ItemType DOC = new ItemType('d', "DOC", "DOC"); 43 | 44 | public static ItemType COMMENT = new ItemType(null, "COMMENT", "COMMENT"); 45 | 46 | 47 | private static Dictionary _typeDictionary; 48 | 49 | /// 50 | /// Builds a Gopher type dictionary from our static ItemType Listing. 51 | /// This allows us to things like Gophertypes.Types['0'] to get the FILE type. 52 | /// 53 | private static void BuildDictionary() 54 | { 55 | _typeDictionary = new Dictionary(); 56 | var type = typeof(ItemType); 57 | var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static).Where(f=>f.FieldType == typeof(ItemType)); 58 | 59 | foreach (var p in fields) 60 | { 61 | var itemType = p.GetValue(null) as ItemType; 62 | if (itemType != null && itemType.Code != null) 63 | _typeDictionary.Add(itemType.Code.Value, itemType); 64 | } 65 | } 66 | 67 | /// 68 | /// Returns a gopher Types by the thier character representations. 69 | /// Eg GopherTypes.Types['0'] is the same as GopherTypes.FILE 70 | /// 71 | public static Dictionary Types 72 | { 73 | get 74 | { 75 | if (_typeDictionary == null) 76 | BuildDictionary(); 77 | return _typeDictionary; 78 | } 79 | } 80 | 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /GopherServer.Core/Providers/IServerProvider.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Results; 2 | 3 | namespace GopherServer.Core.Providers 4 | { 5 | public interface IServerProvider 6 | { 7 | BaseResult GetResult(string selector); 8 | void Init(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /GopherServer.Core/Providers/ProviderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using GopherServer.Core.Configuration; 5 | 6 | namespace GopherServer.Core.Providers 7 | { 8 | public class ProviderFactory 9 | { 10 | /// 11 | /// Gets the provider specified 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static IServerProvider GetProvider(string hostname, int port) 17 | { 18 | // TODO: everything 19 | 20 | var assemblyName = ServerSettings.ProviderName; 21 | 22 | var asm = Assembly.Load(assemblyName); 23 | if (asm == null) 24 | throw new TypeLoadException("Unable to find any assembly named '" + assemblyName + "'"); 25 | 26 | var type = asm.GetTypes().FirstOrDefault(x => typeof(IServerProvider).IsAssignableFrom(x)); 27 | if (type == null) 28 | throw new TypeLoadException("No IServerProvider found in " + asm.FullName); 29 | 30 | return (IServerProvider)Activator.CreateInstance(type, hostname, port); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GopherServer.Core/Providers/ServerProviderBase.cs: -------------------------------------------------------------------------------- 1 |  2 | using GopherServer.Core.Results; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace GopherServer.Core.Providers 11 | { 12 | public abstract class ServerProviderBase : IServerProvider 13 | { 14 | public ServerProviderBase(string hostname, int port) 15 | { } 16 | 17 | public abstract void Init(); 18 | 19 | public abstract BaseResult GetResult(string selector); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/BaseResult.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using GopherServer.Core.Models; 3 | 4 | namespace GopherServer.Core.Results 5 | { 6 | public abstract class BaseResult 7 | { 8 | public ItemType ItemType { get; internal set; } 9 | 10 | public abstract void WriteResult(Stream stream); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/ByteResult.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using GopherServer.Core.Models; 3 | 4 | namespace GopherServer.Core.Results 5 | { 6 | public class ByteResult : BaseResult 7 | { 8 | public ByteResult() 9 | { } 10 | 11 | public ByteResult(byte[] resultBytes, ItemType type) 12 | { 13 | this.ItemType = type; 14 | this.ResultBytes = resultBytes; 15 | } 16 | 17 | public byte[] ResultBytes { get; set; } 18 | 19 | public override void WriteResult(Stream stream) 20 | { 21 | using (var writer = new BinaryWriter(stream)) 22 | { 23 | writer.Write(this.ResultBytes); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/DirectoryResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using GopherServer.Core.Models; 6 | 7 | namespace GopherServer.Core.Results 8 | { 9 | public class DirectoryResult : BaseResult 10 | { 11 | public List Items { get; internal set; } 12 | 13 | public DirectoryResult() 14 | { 15 | this.ItemType = ItemType.DIRECTORY; 16 | this.Items = new List(); 17 | } 18 | 19 | public DirectoryResult(List list) 20 | { 21 | this.Items = list; 22 | this.ItemType = ItemType.DIRECTORY; 23 | } 24 | 25 | public override void WriteResult(Stream stream) 26 | { 27 | var itemsString = this.ToString(); 28 | using (var streamWriter = new StreamWriter(stream, Encoding.ASCII)) 29 | { 30 | streamWriter.Write(itemsString); 31 | // Period on a line by itself 32 | streamWriter.WriteLine("."); 33 | // Server should close connection after this point. 34 | } 35 | itemsString = null; 36 | } 37 | 38 | 39 | /// 40 | /// Returns a the directory listing as a string 41 | /// 42 | /// 43 | public override string ToString() => string.Join("\r\n", this.Items.Select(i => i.ToString())); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/ErrorResult.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Models; 2 | 3 | namespace GopherServer.Core.Results 4 | { 5 | public class ErrorResult : DirectoryResult 6 | { 7 | public ErrorResult(string error) : base() 8 | { 9 | this.Items.Add(new DirectoryItem(ItemType.ERROR, error, "")); 10 | this.Items.Add(new DirectoryItem("Return Home", "")); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/GifResult.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Helpers; 2 | 3 | namespace GopherServer.Core.Results 4 | { 5 | /// 6 | /// Converts an Image URL to a GifResult 7 | /// 8 | public class GifResult : ByteResult 9 | { 10 | public GifResult(string url) 11 | { 12 | this.ItemType = Models.ItemType.GIF; 13 | this.ResultBytes = ImageToGif.ConvertImageToGif(url); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/HtmlResult.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Helpers; 2 | 3 | namespace GopherServer.Core.Results 4 | { 5 | /// 6 | /// Returns a HTML document as a DirectoryResult 7 | /// 8 | public class HtmlResult : DirectoryResult 9 | { 10 | public HtmlResult(string url) 11 | { 12 | var html = HttpHelpers.GetUrl(url); 13 | this.Items = html.CleanHtml().ToDirectoryItems(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/ProxyResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace GopherServer.Core.Results 5 | { 6 | public class ProxyResult : BaseResult 7 | { 8 | public ProxyResult(string url) : base() 9 | { 10 | this.Url = url; 11 | } 12 | 13 | public ProxyResult(string url, string referrer) 14 | { 15 | this.Url = url; 16 | this.Referrer = referrer; 17 | } 18 | 19 | public string Referrer { get; private set; } 20 | public string Url { get; private set; } 21 | 22 | public override void WriteResult(Stream stream) 23 | { 24 | System.Net.HttpWebRequest ProxyRequest = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(Url); 25 | System.Net.WebResponse ServerResponse = null; 26 | if (!string.IsNullOrEmpty(this.Referrer)) 27 | ProxyRequest.Referer = this.Referrer; 28 | 29 | /* Send the proxy request to the remote server or fail. */ 30 | try 31 | { 32 | ServerResponse = ProxyRequest.GetResponse(); 33 | } 34 | catch (System.Net.WebException WebEx) 35 | { 36 | new ErrorResult("Error: " + WebEx.ToString()).WriteResult(stream); 37 | } 38 | 39 | if (ServerResponse != null) 40 | { 41 | using (var instream = ServerResponse.GetResponseStream()) 42 | { 43 | try 44 | { 45 | // Copy with 512K Buffer 46 | instream.CopyTo(stream, 524288); 47 | } 48 | catch (System.IO.IOException ioException) 49 | { 50 | // User probably canncelled 51 | Console.WriteLine("IOException: {0}", ioException); 52 | } 53 | finally 54 | { 55 | instream.Close(); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/TextResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using System.IO; 4 | 5 | namespace GopherServer.Core.Results 6 | { 7 | public class TextResult : BaseResult 8 | { 9 | public TextResult() { } 10 | 11 | public TextResult(string text) 12 | { 13 | this.Text = text; 14 | } 15 | 16 | public TextResult(List text) 17 | { 18 | this.Text = string.Join("\r\n", text); 19 | } 20 | 21 | public string Text { get; set; } 22 | 23 | public override void WriteResult(Stream stream) 24 | { 25 | using (var streamWriter = new StreamWriter(stream, Encoding.ASCII)) 26 | { 27 | streamWriter.Write(this.Text); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /GopherServer.Core/Results/UrlResult.cs: -------------------------------------------------------------------------------- 1 | namespace GopherServer.Core.Results 2 | { 3 | /// 4 | /// Returns a HTML page with a link to the request url 5 | /// 6 | public class UrlResult : TextResult 7 | { 8 | public UrlResult(string url) 9 | { 10 | this.Text = string.Format("

Follow {0}", url); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GopherServer.Core/Routes/NamedGroupRoute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using GopherServer.Core.Helpers; 6 | using GopherServer.Core.Results; 7 | 8 | namespace GopherServer.Core.Routes 9 | { 10 | public class NamedGroupRoute : Route 11 | { 12 | public NamedGroupRoute(string name, string pattern, Delegate resultMethod) 13 | { 14 | this.Name = name; 15 | this.RegexString = pattern; 16 | this.Delegate = resultMethod; 17 | this.BuildRegex(pattern); 18 | } 19 | 20 | protected Delegate Delegate { get; set; } 21 | 22 | ///

23 | /// Returns a dictionary of groups and their values for the route regex on the specified selector. 24 | /// 25 | /// 26 | /// 27 | private Dictionary GetValues(string selector) 28 | { 29 | var groupValues = new Dictionary(); 30 | // Get the groupnames 31 | var groups = Regex.Match(selector).Groups; 32 | foreach (var groupName in Regex.GetGroupNames()) 33 | groupValues.Add(groupName, groups[groupName].Value); 34 | 35 | return groupValues; 36 | } 37 | 38 | public override BaseResult Execute(string selector) 39 | { 40 | var methodParams = this.Delegate.Method.GetParameters(); 41 | 42 | // Build our arguments 43 | var selectorValues = this.GetValues(selector); 44 | 45 | var args = methodParams.Select(p => selectorValues[p.Name].ToType(p.ParameterType)).ToArray(); 46 | 47 | return (BaseResult)this.Delegate.DynamicInvoke(args); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /GopherServer.Core/Routes/PrebuiltRoutes.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Results; 2 | 3 | namespace GopherServer.Core.Routes 4 | { 5 | /// 6 | /// Contains pre-built routes you can use directly in your Provider 7 | /// 8 | public static class PrebuiltRoutes 9 | { 10 | /// 11 | /// Returns the client a link to the specfied URL 12 | /// 13 | /// 14 | /// 15 | public static TypedRoute UrlResult() => new TypedRoute("Url", @"URL:(.+)", url => new UrlResult(url)); 16 | 17 | public static TypedRoute GifRoute() => new TypedRoute("Gif", @"GIF:(.+)", url => new GifResult(url)); 18 | 19 | public static TypedRoute HtmlProxy() => new TypedRoute("Html", @"HTML:(.+)", url => new HtmlResult(url)); 20 | 21 | public static TypedRoute ProxyRoute() => new TypedRoute("Proxy", @"PROXY:(.+)", url => new ProxyResult(url)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GopherServer.Core/Routes/Route.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using GopherServer.Core.Results; 4 | 5 | namespace GopherServer.Core.Routes 6 | { 7 | /// 8 | /// Helper class to assist with providers handling selectors 9 | /// 10 | public class Route 11 | { 12 | public Route() { } 13 | 14 | /// 15 | /// Create a route 16 | /// 17 | /// Name of route 18 | /// Regex partern for matching selectors 19 | /// Action to perform if this route is found 20 | public Route(string name, string pattern, Func action) 21 | { 22 | this.Name = name; 23 | this.RegexString = pattern; 24 | this.Action = action; 25 | this.BuildRegex(pattern); 26 | } 27 | 28 | internal Regex Regex { get; set; } 29 | public string Name { get; set; } 30 | public string RegexString { get; set; } 31 | public Func Action { get; set; } 32 | 33 | internal void BuildRegex(string pattern) => this.Regex = new Regex(pattern); 34 | public bool IsMatch(string selector) => Regex.IsMatch(selector); 35 | public virtual BaseResult Execute(string selector) => Action(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GopherServer.Core/Routes/TypedRoute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GopherServer.Core.Results; 3 | 4 | namespace GopherServer.Core.Routes 5 | { 6 | /// 7 | /// For Regex patterns which returns a *single* group 8 | /// 9 | /// 10 | public class TypedRoute : Route 11 | { 12 | public TypedRoute(string name, string pattern, Func action) 13 | { 14 | this.Name = name; 15 | this.RegexString = pattern; 16 | this.TypedAction = action; 17 | this.BuildRegex(pattern); 18 | } 19 | 20 | public Func TypedAction { get; set; } 21 | 22 | public T GetValue(string selector) 23 | { 24 | var v = Regex.Match(selector).Groups[1].Value; 25 | return (T)Convert.ChangeType(v, typeof(T)); 26 | } 27 | 28 | public override BaseResult Execute(string selector) 29 | { 30 | var value = this.GetValue(selector); 31 | return TypedAction(value); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GopherServer.Providers.FileProvider/FileProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using GopherServer.Core.Providers; 4 | using GopherServer.Core.Results; 5 | 6 | namespace GopherServer.Providers.FileProvider 7 | { 8 | public class FileProvider : ServerProviderBase 9 | { 10 | public FileProvider(string hostname, int port) : base(hostname, port) 11 | { } 12 | 13 | public string BaseDirectory { get; set; } 14 | 15 | public override void Init() => this.BaseDirectory = Settings.RootDirectory; 16 | 17 | public override BaseResult GetResult(string selector) 18 | { 19 | if (string.IsNullOrEmpty(selector)) 20 | return new DirectoryListingResult(BaseDirectory, BaseDirectory); 21 | 22 | if (selector.Contains("..")) 23 | return new ErrorResult("Invalid Path"); 24 | 25 | selector = selector.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); 26 | 27 | var path = Path.Combine(BaseDirectory, selector); 28 | 29 | var indexPath = Path.Combine(path, "index.gopher"); 30 | 31 | if (File.Exists(indexPath)) 32 | return new TextResult(File.ReadAllLines(indexPath).ToList()); 33 | 34 | if (File.Exists(path)) 35 | { 36 | return new FileResult(path, BaseDirectory); 37 | } 38 | 39 | if (Directory.Exists(path)) 40 | return new DirectoryListingResult(path, BaseDirectory); 41 | 42 | return new ErrorResult("Invalid Path"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /GopherServer.Providers.FileProvider/GopherServer.Providers.FileProvider.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /GopherServer.Providers.FileProvider/Results/DirectoryInfoResult.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using GopherServer.Core.Models; 4 | using GopherServer.Core.Results; 5 | 6 | namespace GopherServer.Providers.FileProvider 7 | { 8 | public class DirectoryListingResult : DirectoryResult 9 | { 10 | public DirectoryListingResult(string path, string baseDirectory) 11 | { 12 | var dir = new DirectoryInfo(path); 13 | var directories = dir.GetDirectories(); 14 | var files = dir.GetFiles(); 15 | 16 | // List Directories first 17 | Items.AddRange(directories.Select(d => new DirectoryItem(d.Name, d.FullName.Replace(baseDirectory, string.Empty)))); 18 | Items.AddRange(files.Select(f => new DirectoryItem(GetItemType(f.FullName), f.Name, f.FullName.Replace(baseDirectory, string.Empty)))); 19 | } 20 | 21 | private ItemType GetItemType(string fullName) 22 | { 23 | // TODO: Handle mapping these 24 | //return ItemType.FILE; 25 | return Core.Helpers.FileTypeHelpers.GetItemTypeFromFileName(fullName); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /GopherServer.Providers.FileProvider/Results/FileResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using GopherServer.Core.Results; 4 | 5 | namespace GopherServer.Providers.FileProvider 6 | { 7 | internal class FileResult : BaseResult 8 | { 9 | private string baseDirectory; 10 | private string path; 11 | 12 | public FileResult(string path, string baseDirectory) 13 | { 14 | this.path = path; 15 | this.baseDirectory = baseDirectory; 16 | } 17 | 18 | public override void WriteResult(Stream stream) => File.OpenRead(this.path).CopyTo(stream); 19 | } 20 | } -------------------------------------------------------------------------------- /GopherServer.Providers.FileProvider/Settings.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace GopherServer.Providers.FileProvider 4 | { 5 | public static class Settings 6 | { 7 | public static string RootDirectory => ConfigurationManager.AppSettings["FileProvider.RootDirectory"]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Extensions/AngleSharpExtensions.cs: -------------------------------------------------------------------------------- 1 | using AngleSharp.Dom; 2 | using AngleSharp.Html.Dom; 3 | 4 | namespace GopherServer.Providers.MacintoshGarden.Extensions 5 | { 6 | public static class AngleSharpExtensions 7 | { 8 | public static string TryGetContent(this IElement element, string defaultText = "") 9 | { 10 | if (element == null) 11 | return defaultText; 12 | 13 | return element.TextContent ?? defaultText; 14 | } 15 | 16 | public static string TryGetHref(this IElement element, string defaultText = "") 17 | { 18 | var href = element as IHtmlAnchorElement; 19 | if (href == null) 20 | return defaultText; 21 | 22 | return href.Href ?? defaultText; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/GopherServer.Providers.MacintoshGarden.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/MacGardenController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using GopherServer.Core.Models; 5 | using GopherServer.Core.Results; 6 | using GopherServer.Providers.MacintoshGarden.Results; 7 | using GopherServer.Providers.MacintoshGarden.Models; 8 | 9 | namespace GopherServer.Providers.MacintoshGarden 10 | { 11 | public class MacGardenController 12 | { 13 | private string[] validHosts = new string[] { "www.macintoshgarden.org", "macintoshgarden.org", "mirror.macintoshgarden.org" }; 14 | 15 | public BaseResult ShowApp(string url) => new SoftwareResult(new SoftwareItem(url)); 16 | 17 | internal DirectoryResult Search(string search) 18 | { 19 | var searchUrl = "http://macintoshgarden.org/search/node/" + WebUtility.UrlEncode(search + " type:app,game"); 20 | var searchResults = new Models.SearchResults(searchUrl); 21 | return new Results.SearchResult(searchResults); 22 | } 23 | 24 | internal DirectoryResult SearchPage(string url) 25 | { 26 | var searchResults = new Models.SearchResults(url); 27 | return new Results.SearchResult(searchResults); 28 | } 29 | 30 | public BaseResult DoDownload(string url) 31 | { 32 | var uri = new Uri(url); 33 | 34 | if (!validHosts.Contains(uri.Host)) 35 | return new ErrorResult("Invalid host"); 36 | 37 | return new ProxyResult(url, "http://macintoshgarden.org"); 38 | } 39 | 40 | public DirectoryResult ShowHome() 41 | { 42 | var result = new DirectoryResult(); 43 | result.Items.Add(new DirectoryItem("Macintosh Garden - Gopher Edition")); 44 | result.Items.Add(new DirectoryItem("=================================")); 45 | result.Items.Add(new DirectoryItem("")); 46 | result.Items.Add(new DirectoryItem(ItemType.INDEXSEARCH, "Search the Garden", "/search/")); 47 | 48 | return result; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/MacintoshGardenProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using GopherServer.Core.Providers; 5 | using GopherServer.Core.Results; 6 | using GopherServer.Core.Routes; 7 | 8 | namespace GopherServer.Providers.MacintoshGarden 9 | { 10 | public class MacintoshGardenProvider : ServerProviderBase 11 | { 12 | public List Routes { get; private set; } 13 | public MacGardenController Controller { get; set; } 14 | 15 | public MacintoshGardenProvider(string hostname, int port) : base(hostname, port) 16 | { } 17 | 18 | public override void Init() 19 | { 20 | Controller = new MacintoshGarden.MacGardenController(); 21 | 22 | this.Routes = new List() { 23 | new TypedRoute("Bin", "BIN:(.+)", Controller.DoDownload), // Proxy Download 24 | new TypedRoute("Search", @"\/search\/*\t(.+)", Controller.Search), // Search 25 | new TypedRoute("Search", @"\/search\/(http://macintoshgarden.org\/.+)", Controller.SearchPage), 26 | new TypedRoute("App", @"\/app\/(.+)", Controller.ShowApp), // App Result 27 | PrebuiltRoutes.GifRoute(), // Screenshot 28 | }; 29 | } 30 | 31 | /// 32 | /// Processes the selector and performs the appropriate action 33 | /// 34 | /// 35 | /// 36 | public override BaseResult GetResult(string selector) 37 | { 38 | // This is where we read our selectors... 39 | // it's a shame we can't reuse the route code out of MVC (or can we ?) 40 | 41 | try 42 | { 43 | if (string.IsNullOrEmpty(selector) || selector == "1") // some clients seem to use 1 44 | return Controller.ShowHome(); 45 | 46 | // Check our routes 47 | var route = Routes.FirstOrDefault(r => r.IsMatch(selector)); 48 | 49 | if (route == null) 50 | return new ErrorResult("Selector '" + selector + "' was not found/is not supported."); 51 | else 52 | return route.Execute(selector); 53 | } 54 | catch (Exception ex) 55 | { 56 | // TODO: Some kind of common logging? 57 | Console.WriteLine(ex); 58 | return new ErrorResult("Error occurred processing your request."); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Models/DownloadDetails.cs: -------------------------------------------------------------------------------- 1 | namespace GopherServer.Providers.MacintoshGarden.Models 2 | { 3 | public class DownloadDetails 4 | { 5 | public string Title { get; set; } 6 | public string Size { get; set; } 7 | public string Os { get; set; } 8 | public DownloadLink[] Links { get;set;} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Models/DownloadLink.cs: -------------------------------------------------------------------------------- 1 | namespace GopherServer.Providers.MacintoshGarden.Models 2 | { 3 | public class DownloadLink 4 | { 5 | public string Text { get; set; } 6 | public string Url { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Models/SearchResult.cs: -------------------------------------------------------------------------------- 1 | namespace GopherServer.Providers.MacintoshGarden.Models 2 | { 3 | public class SearchResult 4 | { 5 | public string Name { get; set; } 6 | public string Url { get; set; } 7 | public string Selector { get; set; } 8 | public string SearchSnippet { get; set; } 9 | public string Category { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Models/SearchResults.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using AngleSharp; 4 | using AngleSharp.Html.Dom; 5 | 6 | namespace GopherServer.Providers.MacintoshGarden.Models 7 | { 8 | public class SearchResults 9 | { 10 | public string Url { get; set; } 11 | public string Search { get; set; } 12 | public string NextPageLink { get; set; } 13 | public string PreviousPageLink { get; set; } 14 | public string PageNumber { get; set; } 15 | public List Results { get; set; } 16 | 17 | public SearchResults(string url) 18 | { 19 | this.Url = url; 20 | this.Parse(url); 21 | } 22 | 23 | private void Parse(string url) 24 | { 25 | var config = Configuration.Default.WithDefaultLoader(); 26 | using (var doc = BrowsingContext.New(config).OpenAsync(url).Result) 27 | { 28 | 29 | var searchText = doc.QuerySelector("#edit-keys").NodeValue; 30 | var currentPageNode = doc.QuerySelector("#paper > div.box > div > div > ul > li.pager-current"); 31 | var currentPage = currentPageNode == null ? "1" : currentPageNode.TextContent; 32 | 33 | var nextPageNode = doc.QuerySelector("#paper > div.box > div > div > ul > li.pager-next > a") as IHtmlAnchorElement; 34 | var previousPageNode = doc.QuerySelector("#paper > div.box > div > div > ul > li.pager-previous > a") as IHtmlAnchorElement; 35 | 36 | this.Search = searchText; 37 | if (nextPageNode != null) 38 | this.NextPageLink = nextPageNode.Href; 39 | if (previousPageNode != null) 40 | this.PreviousPageLink = previousPageNode.Href; 41 | this.PageNumber = currentPage; 42 | 43 | var searchItemsNodes = doc.QuerySelectorAll("#paper > div.box > div > dl > dt.title a, dd .search-snippet"); 44 | 45 | var results = new List(); 46 | if (searchItemsNodes.Any()) 47 | { 48 | // The first node should be the title href 49 | // the second the search snippet 50 | // eg node[0] == title 51 | // node[1] == snippet 52 | var itemCount = searchItemsNodes.Count(); 53 | for (int i = 0; i < itemCount; i = i + 2) 54 | { 55 | var titleNode = ((IHtmlAnchorElement)searchItemsNodes[i]); 56 | var searchSnippet = searchItemsNodes[i + 1]; 57 | 58 | var result = new SearchResult 59 | { 60 | Name = titleNode.Text, 61 | Url = titleNode.Href, 62 | SearchSnippet = searchSnippet.TextContent, 63 | Selector = "/app/" + titleNode.Href 64 | }; 65 | 66 | results.Add(result); 67 | } 68 | } 69 | 70 | this.Results = results; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Models/SoftwareItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using AngleSharp; 4 | using AngleSharp.Html.Dom; 5 | using GopherServer.Core.Helpers; 6 | using GopherServer.Providers.MacintoshGarden.Extensions; 7 | 8 | namespace GopherServer.Providers.MacintoshGarden.Models 9 | { 10 | public class SoftwareItem 11 | { 12 | public string Title { get; set; } 13 | public string Url { get; set; } 14 | public string Rating { get; set; } 15 | public string Category { get; set; } 16 | public string CategoryLink { get; set; } 17 | public string YearReleased { get; set; } 18 | public string YearReleasedLink { get; set; } 19 | public string Author { get; set; } 20 | public string AuthorLink { get; set; } 21 | public string Publisher { get; set; } 22 | public string PublisherLink { get; set; } 23 | public string[] Screenshots { get; set; } 24 | public string Description { get; set; } 25 | public string ManualUrl { get; set; } 26 | public DownloadDetails[] Downloads { get; set; } 27 | 28 | public SoftwareItem(string url) 29 | { 30 | var html = HttpHelpers.GetUrl(url); 31 | this.Url = url; 32 | this.Parse(url); 33 | } 34 | 35 | public void Parse(string url) 36 | { 37 | var config = Configuration.Default.WithDefaultLoader(); 38 | using (var doc = BrowsingContext.New(config).OpenAsync(url).Result) 39 | { 40 | this.Title = doc.QuerySelector("#paper > h1").TextContent.Trim(); 41 | this.Rating = doc.QuerySelector("#edit-vote-wrapper > div.description > div > span.average-rating > span").TryGetContent("No rating").CleanString(); 42 | 43 | this.Category = doc.QuerySelector("#paper > div.game-preview > div.descr > table > tbody > tr:nth-child(2) > td:nth-child(2) > ul > li > a").TryGetContent("No Category").CleanString(); 44 | this.CategoryLink = doc.QuerySelector("#paper > div.game-preview > div.descr > table > tbody > tr:nth-child(2) > td:nth-child(2) > ul > li > a").TryGetHref(); 45 | 46 | // For the screenshots we just want to link to full images 47 | var screenNodes = doc.QuerySelectorAll("#paper > div.game-preview > div.images a.thickbox"); 48 | 49 | if (screenNodes.Any()) 50 | this.Screenshots = screenNodes.OfType().Select(n => n.Href).ToArray(); 51 | 52 | this.Description = string.Join("\r\n", doc.QuerySelectorAll("#paper > p").Select(n => n.TextContent)); 53 | //this.ManualUrl = doc.QuerySelector("#paper .note.manual a").GetAttribute("href"); 54 | 55 | var downloadNodes = doc.QuerySelectorAll("#paper > div.game-preview > div.descr .note.download"); 56 | 57 | List downloads = new List(); 58 | 59 | foreach (var downloadElement in downloadNodes) 60 | { 61 | // Skip Purchase links 62 | if (downloadElement.QuerySelector("a").TextContent == "Purchase") 63 | continue; 64 | 65 | // Grab the filename > div:nth-child(2) > small 66 | var fileName = downloadElement.QuerySelector("br + small").FirstChild.TextContent.CleanString(); 67 | var fileSize = downloadElement.QuerySelector("br + small > i").TryGetContent().CleanString().TrimStart('('); 68 | var os = downloadElement.LastChild.TextContent.CleanString(); 69 | 70 | // Grab the links with in each element 71 | //var links = downloadElement.QuerySelectorAll("a"); 72 | var links = downloadElement.QuerySelectorAll("a").OfType(); 73 | 74 | downloads.Add(new DownloadDetails() 75 | { 76 | Title = fileName, 77 | Size = fileSize, 78 | Os = os, 79 | Links = links.Select(l => new DownloadLink() { Text = l.Text, Url = l.Href }).ToArray() 80 | }); 81 | } 82 | 83 | this.Downloads = downloads.ToArray(); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Results/SearchResults.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Helpers; 2 | using GopherServer.Core.Models; 3 | using GopherServer.Core.Results; 4 | 5 | namespace GopherServer.Providers.MacintoshGarden.Results 6 | { 7 | public class SearchResult : DirectoryResult 8 | { 9 | public SearchResult(Models.SearchResults results) 10 | { 11 | this.Items.Add(new DirectoryItem("Search Results")); 12 | this.Items.Add(new DirectoryItem("--------------")); 13 | 14 | this.Items.Add(new DirectoryItem(Core.Models.ItemType.INDEXSEARCH, "New Search", "/search/")); 15 | 16 | foreach (var result in results.Results) 17 | { 18 | this.Items.Add(new DirectoryItem(result.Name, result.Selector)); 19 | this.Items.AddRange(result.SearchSnippet.CleanString().WrapToDirectoryItems()); 20 | this.Items.Add(new DirectoryItem("----")); 21 | } 22 | 23 | this.Items.Add(new DirectoryItem("Current Page: " + results.PageNumber)); 24 | 25 | if (!string.IsNullOrEmpty(results.PreviousPageLink)) 26 | this.Items.Add(new DirectoryItem("Previous Page", "/search/" + results.PreviousPageLink)); 27 | if (!string.IsNullOrEmpty(results.NextPageLink)) 28 | this.Items.Add(new DirectoryItem("Next Page", "/search/" + results.NextPageLink)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /GopherServer.Providers.MacintoshGarden/Results/SoftwareResult.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Helpers; 2 | using GopherServer.Core.Models; 3 | using GopherServer.Core.Results; 4 | using GopherServer.Providers.MacintoshGarden.Models; 5 | 6 | namespace GopherServer.Providers.MacintoshGarden.Results 7 | { 8 | public class SoftwareResult : DirectoryResult 9 | { 10 | public SoftwareResult(SoftwareItem item) : base() 11 | { 12 | Items.Add(new DirectoryItem(new string('*', item.Title.Length + 4))); 13 | Items.Add(new DirectoryItem("* " + item.Title + " *")); 14 | Items.Add(new DirectoryItem(new string('*', item.Title.Length + 4))); 15 | 16 | if (item.Screenshots != null) 17 | { 18 | foreach (var screenshot in item.Screenshots) Items.Add(new DirectoryItem(ItemType.GIF, "Screenshot", "GIF:" + screenshot)); 19 | } 20 | 21 | Items.AddRange(item.Description.WrapToDirectoryItems()); 22 | Items.Add(new DirectoryItem("")); 23 | 24 | Items.Add(new DirectoryItem("Downloads")); 25 | Items.Add(new DirectoryItem("=========")); 26 | foreach (var download in item.Downloads) 27 | { 28 | //Items.Add(new DirectoryItem(new string('-', download.Title.Length))); 29 | //Items.Add(new DirectoryItem(download.Title)); 30 | //Items.Add(new DirectoryItem(new string('-', download.Title.Length))); 31 | foreach (var link in download.Links) 32 | { 33 | Items.Add(new DirectoryItem(link.Text + ":")); 34 | Items.Add(new DirectoryItem(Core.Helpers.FileTypeHelpers.GetItemTypeFromFileName(link.Url), download.Title, "BIN:" + link.Url)); 35 | } 36 | Items.Add(new DirectoryItem("Filesize: " + download.Size)); 37 | Items.Add(new DirectoryItem("OS: " + download.Os)); 38 | Items.Add(new DirectoryItem("")); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/Data/Db.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SQLite; 5 | using SQLiteNetExtensions.Extensions; 6 | using GopherServer.Core.Rss.Syndication; 7 | 8 | namespace GopherServer.Core.Rss.Data 9 | { 10 | public class Db 11 | { 12 | SQLiteConnection connection; 13 | 14 | public Db(string dbPath) 15 | { 16 | connection = new SQLiteConnection(dbPath); 17 | CreateDb(); 18 | } 19 | 20 | public void CreateDb() 21 | { 22 | connection.CreateTable(); 23 | connection.CreateTable(); 24 | connection.CreateTable(); 25 | connection.CreateTable(); 26 | } 27 | 28 | public User GetUser(string nickname) 29 | { 30 | var user = connection.Table().FirstOrDefault(u => u.NickName == nickname); 31 | if (user == null) 32 | throw new Exception("No user found for nickanme '" + nickname + "'"); 33 | 34 | return user; 35 | } 36 | 37 | public Feed GetFeed(int feedId) 38 | { 39 | return connection.Find(feedId); 40 | } 41 | 42 | public string GetFeedCache(string nickname, int feedId) 43 | { 44 | var user = this.GetUser(nickname); 45 | // Make sure the user has a feed 46 | if (!connection.Table().Any(f => f.FeedId == feedId && f.UserId == user.Id)) 47 | throw new Exception("Invalid Feed Id for this user."); 48 | 49 | return connection.Table().FirstOrDefault(f => f.FeedId == feedId).CacheData; 50 | } 51 | 52 | internal IEnumerable GetFeeds() 53 | { 54 | return connection.Table(); 55 | } 56 | 57 | public IEnumerable GetUsers() 58 | { 59 | return connection.Table().AsEnumerable(); 60 | } 61 | 62 | public IEnumerable GetUserFeeds(string nickName) 63 | { 64 | var userId = this.GetUser(nickName).Id; 65 | var user = connection.GetWithChildren(userId); 66 | return user.Feeds; 67 | } 68 | 69 | public void AddUser(string nickName) 70 | { 71 | connection.Insert(new User() 72 | { 73 | NickName = nickName, 74 | Created = DateTime.Now, 75 | LastLogin = DateTime.Now, 76 | }); 77 | } 78 | 79 | public int AddFeed(string nickName, string url) 80 | { 81 | // Get the User 82 | var user = GetUser(nickName); 83 | 84 | // Check if the URL already exists 85 | var feed = connection.Table().FirstOrDefault(f => f.Url == url); 86 | if (feed == null) 87 | { 88 | // Create the feed 89 | feed = new Feed(); 90 | feed.Feedname = "TBA"; 91 | feed.Url = url; 92 | 93 | connection.Insert(feed); 94 | } 95 | // Check if the user has the feed 96 | if (connection.GetAllWithChildren().Any(f => f.FeedId == feed.Id && f.User.NickName == nickName)) 97 | return feed.Id; // user already has feed 98 | 99 | 100 | UserFeed userFeed = new UserFeed() 101 | { 102 | FeedId = feed.Id, 103 | UserId = user.Id 104 | }; 105 | 106 | connection.Insert(userFeed); 107 | 108 | return feed.Id; 109 | } 110 | 111 | public void UpdateCache(int feedId, string cacheData) 112 | { 113 | // grab the existing cache (if it exists) 114 | var cache = connection.Table().FirstOrDefault(c => c.FeedId == feedId); 115 | if (cache == null) 116 | { 117 | cache = new FeedCache(); 118 | cache.FeedId = feedId; 119 | cache.CacheData = cacheData; 120 | cache.LastRefreshed = DateTime.Now; 121 | connection.Insert(cache); 122 | return; 123 | } 124 | cache.CacheData = cacheData; 125 | cache.LastRefreshed = DateTime.Now; 126 | connection.Update(cache); 127 | } 128 | 129 | public void UpdateFeed(int feedId, FeedDetails detail) 130 | { 131 | var feed = connection.Find(feedId); 132 | feed.Feedname = detail.Title; 133 | connection.Update(feed); 134 | this.UpdateCache(feedId, detail.FeedXml); 135 | } 136 | 137 | public void UpdateUserLogin(string nickName) 138 | { 139 | var user = GetUser(nickName); 140 | user.LastLogin = DateTime.Now; 141 | connection.Update(user); 142 | } 143 | 144 | public List CachedFeedDataForUser(string nickName) 145 | { 146 | var user = GetUser(nickName); 147 | return user.Feeds.Select(f => f.FeedCache.CacheData).ToList(); 148 | } 149 | 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/Data/Feed.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using SQLite; 3 | using SQLiteNetExtensions.Attributes; 4 | 5 | namespace GopherServer.Core.Rss.Data 6 | { 7 | public class Feed 8 | { 9 | [PrimaryKey, AutoIncrement] 10 | public int Id { get; set; } 11 | 12 | [MaxLength(100)] 13 | public string Feedname { get; set; } 14 | 15 | [Indexed] 16 | [MaxLength(255)] 17 | public string Url { get; set; } 18 | 19 | [OneToOne] 20 | public FeedCache FeedCache { get; set; } 21 | 22 | [ManyToMany(typeof(User))] 23 | public List Users { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/Data/FeedCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SQLite; 3 | using SQLiteNetExtensions.Attributes; 4 | 5 | namespace GopherServer.Core.Rss.Data 6 | { 7 | public class FeedCache 8 | { 9 | [PrimaryKey, AutoIncrement] 10 | public int Id { get; set; } 11 | 12 | public string CacheData { get; set; } 13 | 14 | [ForeignKey(typeof(Feed))] 15 | public int FeedId { get; set; } 16 | 17 | public DateTime LastRefreshed { get; set; } 18 | 19 | [OneToOne] 20 | public Feed Feed { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/Data/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using SQLite; 4 | using SQLiteNetExtensions.Attributes; 5 | 6 | namespace GopherServer.Core.Rss.Data 7 | { 8 | public class User 9 | { 10 | [PrimaryKey, AutoIncrement] 11 | public int Id { get; set; } 12 | 13 | [Indexed] 14 | [MaxLength(16)] 15 | public string NickName { get; set; } 16 | 17 | public DateTime Created { get; set; } 18 | 19 | public DateTime LastLogin { get; set; } 20 | 21 | [ManyToMany(typeof(UserFeed))] 22 | public List Feeds { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/Data/UserFeed.cs: -------------------------------------------------------------------------------- 1 | using SQLite; 2 | using SQLiteNetExtensions.Attributes; 3 | 4 | namespace GopherServer.Core.Rss.Data 5 | { 6 | public class UserFeed 7 | { 8 | [PrimaryKey, AutoIncrement] 9 | public int Id { get; set; } 10 | 11 | [ForeignKey(typeof(User))] 12 | public int UserId { get; set; } 13 | 14 | [ForeignKey(typeof(Feed))] 15 | public int FeedId { get; set; } 16 | 17 | [ManyToOne] 18 | public Feed Feed { get; set; } 19 | 20 | [ManyToOne] 21 | public User User { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/GopherResults/FeedItemResult.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.ServiceModel.Syndication; 3 | using GopherServer.Core.Helpers; 4 | using GopherServer.Core.Models; 5 | using GopherServer.Core.Results; 6 | 7 | namespace GopherServer.Core.Rss.GopherResults 8 | { 9 | public class FeedItemResult : DirectoryResult 10 | { 11 | public FeedItemResult(string nickname, int feedId, string xml, string itemId) : base() 12 | { 13 | var feed = new Syndication.FeedDetails(xml); 14 | // Find the item 15 | var item = feed.Feed.Items.FirstOrDefault(i => i.Id == itemId); 16 | var content = item.Content as TextSyndicationContent; 17 | var text = content != null ? content.Text : item.Summary.Text; 18 | 19 | this.Items.Add(new DirectoryItem(item.Title.Text)); 20 | this.Items.Add(new DirectoryItem("------------")); 21 | this.Items.Add(new DirectoryItem(item.PublishDate.UtcDateTime.ToString())); 22 | this.Items.AddRange(text.HtmlToText().WrapToDirectoryItems()); 23 | 24 | if (item.Links.Any()) 25 | this.Items.Add(new ExternalUrlItem("Read More...", item.Links.First().Uri.ToString())); 26 | 27 | this.Items.Add(new DirectoryItem("---")); 28 | this.Items.Add(new DirectoryItem("Return to '" + feed.Title + "'...", string.Format("/feeds/{0}/{1}/", nickname, feedId))); 29 | this.Items.Add(new DirectoryItem("Return to Feed List...", string.Format("/feeds/{0}/", nickname))); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/GopherResults/FeedListingResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using GopherServer.Core.Models; 4 | using GopherServer.Core.Results; 5 | using GopherServer.Core.Rss.Data; 6 | 7 | namespace GopherServer.Core.Rss.GopherResults 8 | { 9 | public class FeedListingResult : DirectoryResult 10 | { 11 | public FeedListingResult(string nickname, IEnumerable feeds) : base() 12 | { 13 | this.Items.Add(new DirectoryItem("Feeds for '" + nickname + "'")); 14 | this.Items.Add(new DirectoryItem("")); 15 | this.Items.Add(new DirectoryItem(ItemType.INDEXSEARCH, "Add Feed", string.Format("/user/{0}/add/", nickname))); 16 | this.Items.Add(new DirectoryItem(" Enter the URL of the feed when prompted.")); 17 | this.Items.Add(new DirectoryItem("")); 18 | 19 | if (feeds == null || feeds.Count() == 0) 20 | { 21 | this.Items.Add(new DirectoryItem("No feeds found.")); 22 | return; 23 | } 24 | 25 | this.Items.Add(new DirectoryItem("Combined View", "/feeds/" + nickname + "/all/")); 26 | 27 | foreach (var feed in feeds) 28 | { 29 | this.Items.Add(new DirectoryItem(feed.Feedname, string.Format("/feeds/{0}/{1}/", nickname, feed.Id))); 30 | this.Items.Add(new DirectoryItem(feed.Url)); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/GopherResults/FeedResult.cs: -------------------------------------------------------------------------------- 1 | using GopherServer.Core.Results; 2 | using GopherServer.Core.Helpers; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using GopherServer.Core.Models; 9 | 10 | namespace GopherServer.Core.Rss.GopherResults 11 | { 12 | public class FeedResult : DirectoryResult 13 | { 14 | public FeedResult(string nickname, int feedId, string xml) : base() 15 | { 16 | try 17 | { 18 | var feed = new Syndication.FeedDetails(xml); 19 | 20 | 21 | 22 | this.Items.Add(new DirectoryItem(feed.Title)); 23 | this.Items.Add(new DirectoryItem("---------------")); 24 | this.Items.Add(new DirectoryItem("Last Updated: " + feed.LastUpdated.ToString())); 25 | this.Items.Add(new DirectoryItem("Delete Feed", string.Format("/user/{0}/delete/{1}/", nickname, feedId))); 26 | this.Items.Add(new DirectoryItem("")); 27 | this.Items.Add(new DirectoryItem("Return to Feed List", string.Format("/feeds/{0}/", nickname))); 28 | this.Items.Add(new DirectoryItem("")); 29 | 30 | foreach (var item in feed.Feed.Items) 31 | { 32 | this.Items.Add(new DirectoryItem(item.Title.Text, string.Format("/feeds/{0}/{1}/{2}", nickname, feedId, item.Id))); 33 | //this.Items.Add(new DirectoryItem("Author(s): " + string.Join(", ", item.Authors.Select(a => a.Name)))); 34 | this.Items.Add(new DirectoryItem("Published: " + item.PublishDate.UtcDateTime.ToString())); 35 | this.Items.AddRange(item.Summary.Text.HtmlToText().WrapToDirectoryItems()); 36 | 37 | this.Items.Add(new DirectoryItem("---")); 38 | } 39 | } 40 | catch (Exception) 41 | { 42 | this.Items.Add(new DirectoryItem("Error Processing Feed.")); 43 | } 44 | 45 | this.Items.Add(new DirectoryItem("Return to Feed List", string.Format("/feeds/{0}/", nickname))); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/GopherServer.Providers.Rss.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | Always 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/RssController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using GopherServer.Core.Models; 4 | using GopherServer.Core.Results; 5 | using GopherServer.Core.Rss.Data; 6 | using GopherServer.Core.Rss.GopherResults; 7 | 8 | namespace GopherServer.Core.Rss 9 | { 10 | public class RssController 11 | { 12 | public Db Db { get; private set; } 13 | 14 | public RssController(Db db) => this.Db = db; 15 | 16 | public FeedListingResult GetUserFeeds(string nickname) 17 | { 18 | Db.UpdateUserLogin(nickname); 19 | var feeds = Db.GetUserFeeds(nickname); 20 | return new FeedListingResult(nickname, feeds); 21 | } 22 | 23 | public DirectoryResult GetCombinedFeeds(string nickname) 24 | { 25 | throw new NotImplementedException(); 26 | } 27 | 28 | public DirectoryResult GetFeed(string nickname, int feedId) 29 | { 30 | var feed = Db.GetFeedCache(nickname, feedId); 31 | return new FeedResult(nickname, feedId, feed); 32 | } 33 | 34 | public BaseResult RegisterUser(string nickname) 35 | { 36 | try 37 | { 38 | Db.AddUser(nickname); 39 | return GetUserFeeds(nickname); 40 | } 41 | catch (Exception) 42 | { 43 | // User probably already exists 44 | return new ErrorResult("Unable to register '" + nickname + '"'); 45 | } 46 | } 47 | 48 | public BaseResult AddFeed(string nickname, string feedUrl) 49 | { 50 | if (Syndication.Syndication.TestValidFeed(feedUrl)) 51 | { 52 | var id = Db.AddFeed(nickname, feedUrl); 53 | Syndication.Syndication.UpdateFeed(Db, id); 54 | return GetUserFeeds(nickname); 55 | } 56 | 57 | return new ErrorResult("Invalid Feed"); 58 | } 59 | 60 | internal BaseResult DeleteFeed(string nickname, int feedId) 61 | { 62 | // TODO remove feed code 63 | return GetUserFeeds(nickname); 64 | } 65 | 66 | internal DirectoryResult GetHomePage() 67 | { 68 | return new DirectoryResult(new List() 69 | { 70 | new DirectoryItem("Welcome to Gopher RSS"), 71 | new DirectoryItem("---------------------"), 72 | new DirectoryItem(ItemType.INDEXSEARCH, "Register", "/register/"), 73 | new DirectoryItem("Enter a nickname to register."), 74 | new DirectoryItem(" Note there is no security on this. If someone"), 75 | new DirectoryItem(" can guess your nickname then they can edit"), 76 | new DirectoryItem(" your feeds."), 77 | new DirectoryItem(""), 78 | new DirectoryItem(ItemType.INDEXSEARCH, "Login", "/feeds/"), 79 | new DirectoryItem("Enter your nickname to retrieve your feeds."), 80 | new DirectoryItem(""), 81 | new DirectoryItem("Powered by GopherServer - https://github.com/pgodwin/GopherServer/"), 82 | }); 83 | } 84 | 85 | public BaseResult GetFeedItem(string nickname, int feedId, string itemId) 86 | { 87 | var xml = Db.GetFeedCache(nickname, feedId); 88 | return new FeedItemResult(nickname, feedId, xml, itemId); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/RssProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using GopherServer.Core.Providers; 5 | using GopherServer.Core.Results; 6 | using GopherServer.Core.Routes; 7 | using GopherServer.Core.Rss.Data; 8 | 9 | namespace GopherServer.Core.Rss 10 | { 11 | /// 12 | /// Rss Provider 13 | /// 14 | public class RssProvider : ServerProviderBase 15 | { 16 | Db db; 17 | 18 | public RssProvider(string hostname, int port) : base(hostname, port) 19 | { } 20 | 21 | public List Routes { get; set; } 22 | 23 | public RssController Controller { get; private set; } 24 | 25 | public override void Init() 26 | { 27 | db = new Db("rss.db"); 28 | this.Controller = new RssController(db); 29 | this.Routes = BuildRoutes(); 30 | 31 | var feedDownloader = new System.Threading.Timer(e => { Syndication.Syndication.UpdateFeeds(db); }, null, 0, (int)TimeSpan.FromMinutes(5).TotalMilliseconds); 32 | } 33 | 34 | private List BuildRoutes() 35 | { 36 | var routes = new List() 37 | { 38 | // Named groups require that the parameter name matches the group name 39 | // User Feed Listing 40 | new NamedGroupRoute("UserFeeds", @"\/feeds\/(?\w+)\/$", new Func(this.Controller.GetUserFeeds)), 41 | 42 | // User Feed Listing (ie 'login') 43 | new NamedGroupRoute("UserFeedsQuery", @"\/feeds\/\t(?.*)", new Func(this.Controller.GetUserFeeds)), 44 | 45 | // Combined view of the user's feeds 46 | new NamedGroupRoute("CombinedUserFeeds", @"\/feeds\/(?\w+)\/all\/$", new Func(this.Controller.GetCombinedFeeds)), 47 | 48 | // View of selected Feed 49 | new NamedGroupRoute("SpecificUserFeed", @"\/feeds\/(?\w+)\/(?\d+)\/$", new Func(this.Controller.GetFeed)), 50 | 51 | // View Feed Item 52 | new NamedGroupRoute("FeedItem", @"\/feeds\/(?\w+)\/(?\d+)\/(?.+)", new Func(this.Controller.GetFeedItem)), 53 | 54 | // Registration 55 | new NamedGroupRoute("Registration", @"\/register\/*\t(?\w+)", new Func(this.Controller.RegisterUser)), 56 | 57 | // Add Feed 58 | new NamedGroupRoute("AddFeed", @"\/user\/(?\w+)\/add\/\t(?.+)", new Func(this.Controller.AddFeed)), 59 | 60 | // Delete User Feed 61 | new NamedGroupRoute("DeleteFeed", @"\/user\/(?.\w+)\/delete\/(?\d+)\/$", new Func(this.Controller.DeleteFeed)), 62 | }; 63 | 64 | return routes; 65 | } 66 | 67 | public override BaseResult GetResult(string selector) 68 | { 69 | try 70 | { 71 | if (string.IsNullOrEmpty(selector) || selector == "1") // some clients seem to use 1 72 | return this.Controller.GetHomePage(); 73 | 74 | // Check our routes 75 | var route = this.Routes.FirstOrDefault(r => r.IsMatch(selector)); 76 | 77 | if (route == null) 78 | return new ErrorResult("Selector '" + selector + "' was not found/is not supported."); 79 | else 80 | { 81 | Console.WriteLine("Matched Route: {0}", route.Name); 82 | return route.Execute(selector); 83 | } 84 | } 85 | catch (Exception ex) 86 | { 87 | // TODO: Some kind of common logging? 88 | Console.WriteLine(ex); 89 | return new ErrorResult("Error occurred processing your request."); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/Syndication/FeedDetails.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.ServiceModel.Syndication; 4 | using System.Xml; 5 | using GopherServer.Core.Helpers; 6 | 7 | namespace GopherServer.Core.Rss.Syndication 8 | { 9 | public class FeedDetails 10 | { 11 | public string Title { get; set; } 12 | public string FeedXml { get; set; } 13 | public DateTime LastUpdated { get; set; } 14 | public SyndicationFeed Feed { get; set; } 15 | 16 | public FeedDetails(string xml) 17 | { 18 | this.FeedXml = xml; 19 | 20 | using (XmlReader reader = XmlReader.Create(new StringReader(xml))) 21 | { 22 | this.Feed = SyndicationFeed.Load(reader); 23 | } 24 | 25 | this.Title = this.Feed.Title.Text; 26 | this.LastUpdated = this.Feed.LastUpdatedTime.UtcDateTime; 27 | } 28 | 29 | public static FeedDetails FromUrl(string url) 30 | { 31 | return new FeedDetails(HttpHelpers.GetUrl(url)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/Syndication/Syndication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GopherServer.Core.Rss.Data; 3 | 4 | namespace GopherServer.Core.Rss.Syndication 5 | { 6 | public static class Syndication 7 | { 8 | public static void UpdateFeeds(Db db) 9 | { 10 | foreach (var feed in db.GetFeeds()) 11 | { 12 | try 13 | { 14 | var detail = FeedDetails.FromUrl(feed.Url); 15 | db.UpdateFeed(feed.Id, detail); 16 | } 17 | catch (Exception) 18 | { 19 | // ahh logging where are you! 20 | } 21 | } 22 | } 23 | 24 | public static void UpdateFeed(Db db, int feedId) 25 | { 26 | var feed = db.GetFeed(feedId); 27 | var detail = FeedDetails.FromUrl(feed.Url); 28 | db.UpdateFeed(feed.Id, detail); 29 | } 30 | 31 | public static bool TestValidFeed(string url) 32 | { 33 | try 34 | { 35 | var detail = FeedDetails.FromUrl(url); 36 | return true; 37 | } 38 | catch (Exception) 39 | { 40 | return false; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GopherServer.Providers.Rss/sqlite3.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgodwin/GopherServer/5cf9346efbd498b31dffede16cfa8ff081c8ee62/GopherServer.Providers.Rss/sqlite3.dll -------------------------------------------------------------------------------- /GopherServer.Providers.WpJson/Extensions/WordPressExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using WordPressRestApiStandard.Models; 3 | using GopherServer.Core.Helpers; 4 | using GopherServer.Core.Models; 5 | 6 | namespace GopherServer.Core.WpJson.Extensions 7 | { 8 | public static class WordPressExtensions 9 | { 10 | /// 11 | /// Converts the specified posts to Directory Items 12 | /// 13 | /// 14 | /// 15 | public static List ToDirectoryItems(this List posts) 16 | { 17 | //return posts.Select(p => new DirectoryItem 18 | //{ 19 | // Description = p.Title.Rendered, 20 | // ItemType = ItemType.DIRECTORY, 21 | // Selector = "/posts/" + p.Id 22 | //}).ToList(); 23 | var items = new List(); 24 | 25 | foreach (var p in posts) 26 | { 27 | // Add a directory for the title 28 | items.Add(new DirectoryItem(ItemType.DIRECTORY, p.Title.Rendered.CleanHtml(), "/posts/" + p.Id)); 29 | 30 | // Add the blurb 31 | items.AddRange(p.Excerpt.Rendered.CleanHtml().HtmlToText().WrapToDirectoryItems(80)); 32 | } 33 | 34 | return items; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GopherServer.Providers.WpJson/GopherServer.Providers.WpJson.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /GopherServer.Providers.WpJson/WordPressClient.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using HtmlAgilityPack; 5 | using WordPressRestApiStandard; 6 | using WordPressRestApiStandard.Models; 7 | using WordPressRestApiStandard.QueryModel; 8 | using GopherServer.Core.Helpers; 9 | using GopherServer.Core.Models; 10 | using GopherServer.Core.Results; 11 | using GopherServer.Core.WpJson.Extensions; 12 | 13 | namespace GopherServer.Core.WpJson 14 | { 15 | public class WordPressClient 16 | { 17 | public static WordPressApiClient client; 18 | 19 | public WordPressClient(string url) 20 | { 21 | client = new WordPressApiClient(url); 22 | } 23 | 24 | /// 25 | /// Produces a basic Homepage for the blog 26 | /// 27 | /// 28 | public DirectoryResult GetHomePage() 29 | { 30 | var result = new DirectoryResult(); 31 | result.Items.Add(new DirectoryItem("Welcome")); 32 | result.Items.Add(new DirectoryItem("-------")); 33 | 34 | result.Items.Add(new DirectoryItem("Latest Posts")); 35 | 36 | var latestPosts = client.GetPosts(new PostsQuery { PerPage = 10 }).Result; 37 | result.Items.AddRange(latestPosts.ToDirectoryItems()); 38 | 39 | result.Items.Add(new DirectoryItem("---")); 40 | result.Items.Add(new DirectoryItem(ItemType.DIRECTORY, "Categories", "/categories/")); 41 | result.Items.Add(new DirectoryItem(ItemType.INDEXSEARCH, "Search", "/search/")); 42 | 43 | // TODO: Add Tags and Pages 44 | 45 | return result; 46 | } 47 | 48 | /// 49 | /// Peforms a search of the blog 50 | /// 51 | /// 52 | /// 53 | public DirectoryResult Search(string q) 54 | { 55 | var posts = client.GetPosts(new PostsQuery { Search = q, PerPage = 100, OrderBy = OrderBy.title.ToString() }).Result; 56 | var directory = new DirectoryResult(); 57 | 58 | directory.Items.Add(new DirectoryItem("Search for '" + q + "'")); 59 | 60 | if (posts.Count > 0) 61 | directory.Items.AddRange(posts.ToDirectoryItems()); 62 | else 63 | directory.Items.Add(new DirectoryItem("No results found.")); 64 | 65 | // TODO - add paging! 66 | 67 | return directory; 68 | } 69 | 70 | /// 71 | /// Gets the first 100 categories from the blog ordered by their use. 72 | /// 73 | /// 74 | public DirectoryResult GetCategories() 75 | { 76 | List result = client.GetCategories(new CategoriesQuery() { HideEmpty = true, OrderBy = "count", Order="desc", PerPage = 100 }).Result; 77 | 78 | var directory = new DirectoryResult(); 79 | directory.Items.Add(new DirectoryItem("Categories")); 80 | 81 | directory.Items.AddRange(result.Select(c => new DirectoryItem() 82 | { 83 | Description = string.Format("{0} ({1})", c.Name, c.Count), 84 | ItemType = ItemType.DIRECTORY, 85 | Selector = "/category/" + c.Id 86 | })); 87 | 88 | return directory; 89 | 90 | } 91 | 92 | /// 93 | /// Returns the post as a DirectoryListing for the spcified ID 94 | /// 95 | /// 96 | /// 97 | public DirectoryResult GetPost(int id) 98 | { 99 | var result = new DirectoryResult(); 100 | 101 | var post = client.GetPost(new PostQuery { }, id).Result; 102 | result.Items.Add(new DirectoryItem(post.Title.Rendered.CleanHtml())); 103 | result.Items.Add(new DirectoryItem("---")); 104 | result.Items.Add(new DirectoryItem("Author: " + post.Author)); 105 | result.Items.Add(new DirectoryItem("Date Posted: " + post.DateGmt.ToString())); 106 | result.Items.Add(new DirectoryItem(" ")); 107 | result.Items.Add(new DirectoryItem(ItemType.DOC, "Text Version", "/posts/text/" + id)); 108 | result.Items.Add(new DirectoryItem(ItemType.HTML, "Web Link", "URL:" + post.Link)); 109 | result.Items.Add(new DirectoryItem("---")); 110 | 111 | result.Items.AddRange(post.Content.Rendered.ToDirectoryItems()); 112 | 113 | result.Items.Add(new DirectoryItem("----------------------------")); 114 | 115 | return result; 116 | } 117 | 118 | public TextResult GetPostText(int id) 119 | { 120 | var post = client.GetPost(new PostQuery { }, id).Result; 121 | return new TextResult(post.Content.Rendered.CleanHtml().HtmlToText().WrapText(80)); 122 | } 123 | 124 | 125 | /// 126 | /// Gets the posts for the specified category. 127 | /// 128 | /// 129 | /// 130 | public DirectoryResult GetCategoryPosts(int category) 131 | { 132 | var posts = client.GetPosts(new PostsQuery { Categories = new List() { category }, PerPage = 100 }).Result; 133 | return new DirectoryResult(posts.ToDirectoryItems()); 134 | 135 | } 136 | 137 | /// 138 | /// Converts the specified URL to a GIF 139 | /// 140 | /// 141 | /// 142 | public ByteResult GetGif(string url) 143 | { 144 | return new ByteResult(ImageToGif.ConvertImageToGif(url), ItemType.GIF); 145 | } 146 | 147 | /// 148 | /// Returns a HTML page to redirect to the propper address 149 | /// 150 | /// 151 | /// 152 | public UrlResult Redirect(string url) 153 | { 154 | return new UrlResult(url); 155 | } 156 | 157 | public DirectoryResult ProxyPage(string url) 158 | { 159 | var result = new DirectoryResult(); 160 | 161 | var html = HttpHelpers.GetUrl(url); 162 | 163 | HtmlDocument doc = new HtmlDocument(); 164 | doc.LoadHtml(html); 165 | var title = doc.DocumentNode.Descendants("title").SingleOrDefault(); 166 | 167 | result.Items.Add(new DirectoryItem("Title: " + title)); 168 | result.Items.Add(new DirectoryItem("Url: " + url)); 169 | result.Items.Add(new DirectoryItem("---------------")); 170 | result.Items.Add(new DirectoryItem("")); 171 | result.Items.AddRange(html.ToDirectoryItems()); 172 | 173 | return result; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /GopherServer.Providers.WpJson/WordPressProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Linq; 5 | using GopherServer.Core.Providers; 6 | using GopherServer.Core.Results; 7 | using GopherServer.Core.Routes; 8 | 9 | namespace GopherServer.Core.WpJson 10 | { 11 | /// 12 | /// Provides a Gopher Provider to the Wordpress REST API 13 | /// 14 | public class WordPressProvider : ServerProviderBase 15 | { 16 | internal WordPressClient client; 17 | 18 | // The routes we're going to use later to perform actions 19 | private List routes; 20 | 21 | public string Hostname { get; private set; } 22 | public int Port { get; private set; } 23 | public string WordPressUrl { get; private set; } 24 | 25 | public WordPressProvider(string hostname, int port) : base(hostname, port) 26 | { 27 | Hostname = hostname; 28 | Port = port; 29 | } 30 | 31 | public override void Init() 32 | { 33 | WordPressUrl = ConfigurationManager.AppSettings[GetType().Name + ".Url"]; 34 | 35 | // TODO Read in Config 36 | client = new WordPressClient(WordPressUrl); 37 | 38 | // Build our route list for teh selector 39 | routes = new List() 40 | { 41 | // Get Post 42 | new TypedRoute("Posts", @"\/posts\/(\d+)", client.GetPost), 43 | 44 | // Get Post as Text 45 | new TypedRoute("Posts", @"\/posts\/text\/(\d+)", client.GetPostText), 46 | 47 | // Categories List 48 | new Route("Categories", @"\/categories\/", client.GetCategories), 49 | 50 | // Posts by Category 51 | new TypedRoute("CategoryPosts", @"\/category\/(\d+)", client.GetCategoryPosts), 52 | 53 | // Search 54 | new TypedRoute("Search", @"\/search\/*\t(.+)", client.Search), 55 | 56 | // Get Image 57 | //new TypedRoute("Gif", @"\/gif\/(.+)", client.GetGif), 58 | PrebuiltRoutes.GifRoute(), 59 | 60 | // External Link 61 | //new TypedRoute("Url", @"URL:(.+)", client.Redirect), 62 | PrebuiltRoutes.UrlResult(), 63 | 64 | // Proxy Link 65 | //new TypedRoute("Proxy", @"\/proxy\/(.+)", client.ProxyPage), 66 | PrebuiltRoutes.HtmlProxy(), 67 | 68 | }; 69 | } 70 | 71 | 72 | /// 73 | /// Processes the selector and performs the appropriate action 74 | /// 75 | /// 76 | /// 77 | public override BaseResult GetResult(string selector) 78 | { 79 | // This is where we read our selectors... 80 | // it's a shame we can't reuse the route code out of MVC (or can we ?) 81 | try 82 | { 83 | if (string.IsNullOrEmpty(selector) || selector == "1") // some clients seem to use 1 84 | return client.GetHomePage(); 85 | 86 | // Check our routes 87 | var route = routes.FirstOrDefault(r => r.IsMatch(selector)); 88 | 89 | if (route == null) 90 | return new ErrorResult("Selector '" + selector + "' was not found/is not supported."); 91 | else 92 | return route.Execute(selector); 93 | } 94 | catch (Exception ex) 95 | { 96 | // TODO: Some kind of common logging? 97 | Console.WriteLine(ex); 98 | return new ErrorResult("Error occurred processing your request."); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /GopherServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GopherServer", "GopherServer\GopherServer.csproj", "{9C8F5CF8-7267-4653-A8FE-F6902345814F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GopherServer.Core", "GopherServer.Core\GopherServer.Core.csproj", "{7735DC4B-E628-4D5C-8A3A-BDB19FD11131}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GopherServer.Providers.MacintoshGarden", "GopherServer.Providers.MacintoshGarden\GopherServer.Providers.MacintoshGarden.csproj", "{80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GopherServer.Providers.Rss", "GopherServer.Providers.Rss\GopherServer.Providers.Rss.csproj", "{6CCAB066-D03C-437A-9EB7-6810C77CDE89}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GopherServer.Providers.WpJson", "GopherServer.Providers.WpJson\GopherServer.Providers.WpJson.csproj", "{BE800704-34D3-4A1A-8BB0-39DCAE7A135D}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GopherServer.Providers.FileProvider", "GopherServer.Providers.FileProvider\GopherServer.Providers.FileProvider.csproj", "{BA99491D-F890-4697-94C8-75B18FE22455}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Debug|x64.Build.0 = Debug|Any CPU 35 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Debug|x86.Build.0 = Debug|Any CPU 37 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Release|x64.ActiveCfg = Release|Any CPU 40 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Release|x64.Build.0 = Release|Any CPU 41 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Release|x86.ActiveCfg = Release|Any CPU 42 | {9C8F5CF8-7267-4653-A8FE-F6902345814F}.Release|x86.Build.0 = Release|Any CPU 43 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Debug|x64.ActiveCfg = Debug|Any CPU 46 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Debug|x64.Build.0 = Debug|Any CPU 47 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Debug|x86.ActiveCfg = Debug|Any CPU 48 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Debug|x86.Build.0 = Debug|Any CPU 49 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Release|x64.ActiveCfg = Release|Any CPU 52 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Release|x64.Build.0 = Release|Any CPU 53 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Release|x86.ActiveCfg = Release|Any CPU 54 | {7735DC4B-E628-4D5C-8A3A-BDB19FD11131}.Release|x86.Build.0 = Release|Any CPU 55 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Debug|x64.ActiveCfg = Debug|Any CPU 58 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Debug|x64.Build.0 = Debug|Any CPU 59 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Debug|x86.ActiveCfg = Debug|Any CPU 60 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Debug|x86.Build.0 = Debug|Any CPU 61 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Release|x64.ActiveCfg = Release|Any CPU 64 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Release|x64.Build.0 = Release|Any CPU 65 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Release|x86.ActiveCfg = Release|Any CPU 66 | {80A9B15D-DB4D-4EA0-A701-3E0E5CFF14F2}.Release|x86.Build.0 = Release|Any CPU 67 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Debug|x64.ActiveCfg = Debug|Any CPU 70 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Debug|x64.Build.0 = Debug|Any CPU 71 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Debug|x86.ActiveCfg = Debug|Any CPU 72 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Debug|x86.Build.0 = Debug|Any CPU 73 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Release|x64.ActiveCfg = Release|Any CPU 76 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Release|x64.Build.0 = Release|Any CPU 77 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Release|x86.ActiveCfg = Release|Any CPU 78 | {6CCAB066-D03C-437A-9EB7-6810C77CDE89}.Release|x86.Build.0 = Release|Any CPU 79 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 80 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Debug|Any CPU.Build.0 = Debug|Any CPU 81 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Debug|x64.ActiveCfg = Debug|Any CPU 82 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Debug|x64.Build.0 = Debug|Any CPU 83 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Debug|x86.ActiveCfg = Debug|Any CPU 84 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Debug|x86.Build.0 = Debug|Any CPU 85 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Release|Any CPU.ActiveCfg = Release|Any CPU 86 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Release|Any CPU.Build.0 = Release|Any CPU 87 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Release|x64.ActiveCfg = Release|Any CPU 88 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Release|x64.Build.0 = Release|Any CPU 89 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Release|x86.ActiveCfg = Release|Any CPU 90 | {BE800704-34D3-4A1A-8BB0-39DCAE7A135D}.Release|x86.Build.0 = Release|Any CPU 91 | {BA99491D-F890-4697-94C8-75B18FE22455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 92 | {BA99491D-F890-4697-94C8-75B18FE22455}.Debug|Any CPU.Build.0 = Debug|Any CPU 93 | {BA99491D-F890-4697-94C8-75B18FE22455}.Debug|x64.ActiveCfg = Debug|Any CPU 94 | {BA99491D-F890-4697-94C8-75B18FE22455}.Debug|x64.Build.0 = Debug|Any CPU 95 | {BA99491D-F890-4697-94C8-75B18FE22455}.Debug|x86.ActiveCfg = Debug|Any CPU 96 | {BA99491D-F890-4697-94C8-75B18FE22455}.Debug|x86.Build.0 = Debug|Any CPU 97 | {BA99491D-F890-4697-94C8-75B18FE22455}.Release|Any CPU.ActiveCfg = Release|Any CPU 98 | {BA99491D-F890-4697-94C8-75B18FE22455}.Release|Any CPU.Build.0 = Release|Any CPU 99 | {BA99491D-F890-4697-94C8-75B18FE22455}.Release|x64.ActiveCfg = Release|Any CPU 100 | {BA99491D-F890-4697-94C8-75B18FE22455}.Release|x64.Build.0 = Release|Any CPU 101 | {BA99491D-F890-4697-94C8-75B18FE22455}.Release|x86.ActiveCfg = Release|Any CPU 102 | {BA99491D-F890-4697-94C8-75B18FE22455}.Release|x86.Build.0 = Release|Any CPU 103 | EndGlobalSection 104 | EndGlobal 105 | -------------------------------------------------------------------------------- /GopherServer/GopherServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /GopherServer/Program.cs: -------------------------------------------------------------------------------- 1 | namespace GopherServer 2 | { 3 | public class Program 4 | { 5 | public static void Main(string[] args) 6 | { 7 | var server = new Server(); 8 | server.StartListening(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /GopherServer/Server.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | using System.Threading; 6 | using GopherServer.Core.Configuration; 7 | using GopherServer.Core.Results; 8 | using GopherServer.Core.Providers; 9 | 10 | namespace GopherServer 11 | { 12 | public class StateObject 13 | { 14 | // Client socket. 15 | public Socket workSocket = null; 16 | // Size of receive buffer. 17 | public const int BufferSize = 1024; 18 | // Receive buffer. 19 | public byte[] buffer = new byte[BufferSize]; 20 | // Received data string. 21 | public StringBuilder sb = new StringBuilder(); 22 | } 23 | 24 | public class Server 25 | { 26 | // Thread signal. 27 | public static ManualResetEvent allDone = new ManualResetEvent(false); 28 | 29 | public IServerProvider provider; 30 | 31 | public IPAddress IPAddress { get; private set; } 32 | public int Port { get; private set; } 33 | public string ExternalHostname { get; set; } 34 | public int ExternalPort { get; set; } 35 | 36 | public Server() 37 | { 38 | IPAddress = IPAddress.Parse(ServerSettings.BoundIP); 39 | Port = ServerSettings.BoundPort; 40 | ExternalHostname = ServerSettings.PublicHostname; 41 | ExternalPort = ServerSettings.PublicPort; 42 | provider = ProviderFactory.GetProvider(this.ExternalHostname, this.ExternalPort); 43 | provider.Init(); 44 | } 45 | 46 | public void StartListening() 47 | { 48 | // Data buffer for incoming data. 49 | var bytes = new byte[1024]; 50 | 51 | // Establish the local endpoint for the socket. 52 | // The DNS name of the computer 53 | // running the listener is "host.contoso.com". 54 | //IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName()); 55 | var ipAddress = IPAddress; 56 | var localEndPoint = new IPEndPoint(ipAddress, Port); 57 | 58 | Console.WriteLine($"Listening on {ipAddress}:{Port}"); 59 | 60 | // Create a TCP/IP socket. 61 | var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 62 | 63 | // Bind the socket to the local endpoint and listen for incoming connections. 64 | try 65 | { 66 | listener.Bind(localEndPoint); 67 | listener.Listen(100); 68 | 69 | while (true) 70 | { 71 | allDone.Reset(); // Set the event to nonsignaled state 72 | Console.WriteLine("Waiting for a connection..."); 73 | listener.BeginAccept(new AsyncCallback(AcceptCallback), listener); // Start an asynchronous socket to listen for connections 74 | allDone.WaitOne(); // Wait until a connection is made before continuing 75 | } 76 | } 77 | catch (Exception e) 78 | { 79 | Console.WriteLine(e.ToString()); 80 | } 81 | 82 | Console.WriteLine("\nPress ENTER to continue..."); 83 | Console.Read(); 84 | } 85 | 86 | public void AcceptCallback(IAsyncResult ar) 87 | { 88 | // Signal the main thread to continue. 89 | allDone.Set(); 90 | 91 | // Get the socket that handles the client request. 92 | var listener = (Socket)ar.AsyncState; 93 | var handler = listener.EndAccept(ar); 94 | 95 | // Create the state object. 96 | var state = new StateObject(); 97 | state.workSocket = handler; 98 | handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); 99 | } 100 | 101 | public void ReadCallback(IAsyncResult ar) 102 | { 103 | var selector = string.Empty; 104 | 105 | // Retrieve the state object and the handler socket 106 | // from the asynchronous state object. 107 | var state = (StateObject)ar.AsyncState; 108 | var handler = state.workSocket; 109 | 110 | // Read data from the client socket. 111 | var bytesRead = handler.EndReceive(ar); 112 | 113 | if (bytesRead > 0) 114 | { 115 | // There might be more data, so store the data received so far. 116 | state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead)); 117 | 118 | // Check for end-of-file tag. If it is not there, read more data. 119 | selector = state.sb.ToString(); 120 | 121 | if (selector.IndexOf("\r\n") > -1) 122 | { 123 | // All the data has been read from the client. Display it on the console. 124 | Console.WriteLine($"Read {selector.Length} bytes from socket.\nData: {selector}"); 125 | 126 | // Trim the trailng CRLF 127 | selector = selector.TrimEnd('\r', '\n'); 128 | 129 | // a whole bunch of legacy clients seem to be doing might strip that too 130 | selector = selector.TrimStart('\r', '\n'); 131 | 132 | if (selector == ".") selector = ""; 133 | 134 | // Tell the provider to return a result for the selector 135 | var result = provider.GetResult(selector); 136 | 137 | // This is a bit of a hack - I need a better way of doing this (push it back to the provider I think) 138 | if (result is DirectoryResult) 139 | { 140 | var dir = result as DirectoryResult; 141 | foreach (var item in dir.Items) 142 | { 143 | if (string.IsNullOrEmpty(item.Host)) 144 | { 145 | item.Host = this.ExternalHostname; 146 | item.Port = this.Port; 147 | } 148 | } 149 | } 150 | 151 | //using (var stream = new MemoryStream()) 152 | //{ 153 | // result.WriteResult(stream); 154 | // Send(handler, stream.ToArray()); 155 | //} 156 | 157 | WriteResult(handler, result); 158 | } 159 | else 160 | { 161 | // Not all data received. Get more. 162 | handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); 163 | } 164 | } 165 | } 166 | 167 | private static void Send(Socket handler, String data) 168 | { 169 | // Convert the string data to byte data using ASCII encoding. 170 | var byteData = Encoding.ASCII.GetBytes(data); 171 | 172 | // Begin sending the data to the remote device. 173 | handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler); 174 | } 175 | 176 | private static void Send(Socket handler, byte[] data) 177 | { 178 | // Begin sending the data to the remote device. 179 | handler.BeginSend(data, 0, data.Length, 0, new AsyncCallback(SendCallback), handler); 180 | } 181 | 182 | private static void WriteResult(Socket handler, BaseResult result) 183 | { 184 | try 185 | { 186 | using (var netStream = new NetworkStream(handler)) 187 | result.WriteResult(netStream); 188 | } 189 | catch (Exception ex) 190 | { 191 | Console.WriteLine("Error writing result. {0}", ex); 192 | } 193 | finally 194 | { 195 | try 196 | { 197 | handler.Shutdown(SocketShutdown.Both); 198 | handler.Close(); 199 | } 200 | catch (Exception handlerException) 201 | { 202 | Console.WriteLine("Error attempting to close the handler: {0}", handlerException); 203 | } 204 | } 205 | } 206 | 207 | private static void SendCallback(IAsyncResult ar) 208 | { 209 | try 210 | { 211 | // Retrieve the socket from the state object. 212 | var handler = (Socket)ar.AsyncState; 213 | 214 | // Complete sending the data to the remote device. 215 | var bytesSent = handler.EndSend(ar); 216 | Console.WriteLine("Sent {0} bytes to client.", bytesSent); 217 | 218 | handler.Shutdown(SocketShutdown.Both); 219 | handler.Close(); 220 | } 221 | catch (Exception e) 222 | { 223 | Console.WriteLine(e.ToString()); 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /GopherServer/app.sample.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 pgodwin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GopherServer 2 | A Gopher Server implemented in C#. 3 | 4 | ## What is in here 5 | - Simple Socket Server implemented in GopherServer 6 | - Concept of "Providers' which return a Result for the selector. 7 | - Providers only have two methods to implement: `void Init()` and `BaseResult GetResult(string selector);` 8 | - Models for results, item types, etc and helpers in GopherServer.Core 9 | - Providers configured in app.config 10 | - A few sample providers have been written which demonstrate different examples of Providers. 11 | 12 | Pull requests very welcome! 13 | 14 | # Config Options 15 | All configuration options are currently via the GopherServer.exe.config file. Further work will be undertaken to improve 16 | the way the server and the providers are configured. 17 | 18 | - boundIP: The IP Address the SocketServer will listen on 19 | - boundPort: The port the SocketServer will listen on 20 | - publicHostname: The address to use on for DirectoryResults 21 | - publicPort: The port to use for DirectoryResults 22 | - resizeImages: true|false - whether or not images should be resized (ie when proxying images to gif). 23 | This is useful for low-memory targets where large images will consume too much memory. 24 | - maximumWidth: int - the maximum width a resized image should be 25 | - maximumHeight: int - the maximum height a resized image should be. 26 | - resampleImages: true|false - whether or not images should be resampled to a lower colour depth. 27 | As we're outputting gif images, the bit-depth is always a maximum of 8 (256 colours). 28 | - maximumBitDepth: 1, 4, 8: the bit-depth images should be resampled to. Will depend on your target platform. 29 | Eg Early Macintosh machines it might be best to keep things a 1 or 4bit (16 colours). 30 | - providerName: The GopherServerProvider to use with this server (see below). 31 | 32 | # Providers 33 | 34 | ## WordPressProvider 35 | The WordPressProvider consumes the WordPress REST API and allows a blog to be browsed with a Gopher client(!). 36 | To use it: 37 | - Set `` 38 | - and adjust `` to your URL 39 | 40 | The provider has support for proxying GIF and Webpages over gopher. 41 | Give it a go! 42 | 43 | ## RSS Provider 44 | The RSS provider is an advanced example showing how to bulid a database backed Gopher Provider. 45 | Users can "Register" a nickname on the site and add RSS feeds. The feeds are periodically downloaded 46 | and cached in the DB. The user is then able to view the RSS feed items. All from their Gopher Client. 47 | 48 | ## Macintosh Garden Provider 49 | This provider proxies search results, applications pages, screenshots and downloads. It's a good example 50 | of building a Gopher Proxy against a public website. 51 | 52 | To use it: 53 | - Set `` in your GopherServer.exe.config file. 54 | 55 | ## FileProvider 56 | The FileProvider allows GopherServer to run like a more traditional Gopher server, serving directories and files. 57 | It's still in progress and is currently hardcoded to a specific directory (as of 3/6/2017). This will be developed further. 58 | 59 | To use it: 60 | Make sure you have Sqlite available for your platform (Windows included). 61 | - Set `` in GopherServer.exe.config 62 | 63 | # Mono Support 64 | I've been able to successfully run this under Mono on Ubuntu 16 LTS. This, along with the low-memory footprint of 65 | the providers, means you can host GopherServer on cheap linux hosting such as Amazon Lightsail or other low-end hosts. 66 | 67 | # Client Support 68 | I've successfully tested this server with the following clients: 69 | 70 | ## Win32 71 | - SeaMonkey 1.1.19 (Win32) - https://www.seamonkey-project.org/releases/seamonkey1.1.19/ 72 | - Digger Dwarf (Win32) - http://slugmax.tx0.org/gopher.viste.fr/projects/ddwarf/ 73 | 74 | ## Classic Macintosh 75 | - Netscape 3 (Almost certainly early and later versions) 76 | Netscape is great as it supports images out of the box and already has good mime-mappings. 77 | Screenshot - http://imgur.com/oSTpWSI 78 | - GopherApp (Macintosh System 6+) - http://iubio.bio.indiana.edu/soft/util/gopher/gopherapp/ 79 | You need to edit one of bookmark files in BBEdit or similar. It doesn't seem to support INFO types, 80 | but SOURCE (MacApp Pascal i think) is available! 81 | - GopherApp++ (Macintosh System 6+) - http://iubio.bio.indiana.edu/soft/util/gopher/gopherapp/ 82 | - GopherPup (Macintosh System 7+ I think) - http://iubio.bio.indiana.edu/soft/util/gopher/gopherpup/ 83 | This was cross-platform, but I'm not sure if source was ever released. Available also for Win16, Win32, SGI Mips and Sparc 84 | - TurboGopher (System 6+) 85 | I had some issues with this one. It would download the initial directory, but wouldn't connect again past there. 86 | Screenshot - http://i.imgur.com/obYrUcZ.png 87 | 88 | There's lots of clients out there, if you've had any luck with any of them let me know! 89 | 90 | # TODO 91 | - Write Much better documentation 92 | - Improve the HTML to Text rendering 93 | - Improve the socket server (it's pretty much microsoft's example async socket server as is, lots of room for improvement) 94 | - Improve the configuration and loading of providers 95 | - Write a client 96 | - Replace hacky code 97 | 98 | 99 | 100 | License / Credits: 101 | ================== 102 | This software would not be possible without the hardware of contributors to many open source projects. 103 | Many of these have been released under an MIT license, and this software has also been released under the same license (see LICENSE). 104 | 105 | AngleSharp 106 | ---------- 107 | 108 | The MIT License (MIT) 109 | 110 | Copyright (c) 2013 - 2017 AngleSharp 111 | 112 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 113 | 114 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 115 | 116 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 117 | 118 | Html Agility Pack 119 | ----------------- 120 | 121 | The MIT License (MIT) 122 | Permission is hereby granted, free of charge, to any person obtaining a copy 123 | of this software and associated documentation files (the "Software"), to deal 124 | in the Software without restriction, including without limitation the rights 125 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 126 | copies of the Software, and to permit persons to whom the Software is 127 | furnished to do so, subject to the following conditions: 128 | 129 | The above copyright notice and this permission notice shall be included in all 130 | copies or substantial portions of the Software. 131 | 132 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 133 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 134 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 135 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 136 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 137 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 138 | SOFTWARE. 139 | 140 | NewtonSoft.Json 141 | --------------- 142 | 143 | The MIT License (MIT) 144 | 145 | Copyright (c) 2007 James Newton-King 146 | 147 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 148 | 149 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 152 | 153 | SQLite.Net-PCL 154 | -------------- 155 | 156 | // Copyright (c) 2012 Krueger Systems, Inc. 157 | // Copyright (c) 2013 Øystein Krog (oystein.krog@gmail.com) 158 | // 159 | // Permission is hereby granted, free of charge, to any person obtaining a copy 160 | // of this software and associated documentation files (the "Software"), to deal 161 | // in the Software without restriction, including without limitation the rights 162 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 163 | // copies of the Software, and to permit persons to whom the Software is 164 | // furnished to do so, subject to the following conditions: 165 | // 166 | // The above copyright notice and this permission notice shall be included in 167 | // all copies or substantial portions of the Software. 168 | // 169 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 170 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 171 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 172 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 173 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 174 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 175 | // THE SOFTWARE. 176 | 177 | sqlite-net-extensions 178 | --------------------- 179 | 180 | Copyright (C) 2013 TwinCoders S.L. 181 | 182 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 183 | 184 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 185 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 186 | 187 | WordPressRestAPI 188 | ------------------ 189 | Apache License 190 | Version 2.0, January 2004 191 | http://www.apache.org/licenses/ 192 | 193 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 194 | 195 | 1. Definitions. 196 | 197 | "License" shall mean the terms and conditions for use, reproduction, 198 | and distribution as defined by Sections 1 through 9 of this document. 199 | 200 | "Licensor" shall mean the copyright owner or entity authorized by 201 | the copyright owner that is granting the License. 202 | 203 | "Legal Entity" shall mean the union of the acting entity and all 204 | other entities that control, are controlled by, or are under common 205 | control with that entity. For the purposes of this definition, 206 | "control" means (i) the power, direct or indirect, to cause the 207 | direction or management of such entity, whether by contract or 208 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 209 | outstanding shares, or (iii) beneficial ownership of such entity. 210 | 211 | "You" (or "Your") shall mean an individual or Legal Entity 212 | exercising permissions granted by this License. 213 | 214 | "Source" form shall mean the preferred form for making modifications, 215 | including but not limited to software source code, documentation 216 | source, and configuration files. 217 | 218 | "Object" form shall mean any form resulting from mechanical 219 | transformation or translation of a Source form, including but 220 | not limited to compiled object code, generated documentation, 221 | and conversions to other media types. 222 | 223 | "Work" shall mean the work of authorship, whether in Source or 224 | Object form, made available under the License, as indicated by a 225 | copyright notice that is included in or attached to the work 226 | (an example is provided in the Appendix below). 227 | 228 | "Derivative Works" shall mean any work, whether in Source or Object 229 | form, that is based on (or derived from) the Work and for which the 230 | editorial revisions, annotations, elaborations, or other modifications 231 | represent, as a whole, an original work of authorship. For the purposes 232 | of this License, Derivative Works shall not include works that remain 233 | separable from, or merely link (or bind by name) to the interfaces of, 234 | the Work and Derivative Works thereof. 235 | 236 | "Contribution" shall mean any work of authorship, including 237 | the original version of the Work and any modifications or additions 238 | to that Work or Derivative Works thereof, that is intentionally 239 | submitted to Licensor for inclusion in the Work by the copyright owner 240 | or by an individual or Legal Entity authorized to submit on behalf of 241 | the copyright owner. For the purposes of this definition, "submitted" 242 | means any form of electronic, verbal, or written communication sent 243 | to the Licensor or its representatives, including but not limited to 244 | communication on electronic mailing lists, source code control systems, 245 | and issue tracking systems that are managed by, or on behalf of, the 246 | Licensor for the purpose of discussing and improving the Work, but 247 | excluding communication that is conspicuously marked or otherwise 248 | designated in writing by the copyright owner as "Not a Contribution." 249 | 250 | "Contributor" shall mean Licensor and any individual or Legal Entity 251 | on behalf of whom a Contribution has been received by Licensor and 252 | subsequently incorporated within the Work. 253 | 254 | 2. Grant of Copyright License. Subject to the terms and conditions of 255 | this License, each Contributor hereby grants to You a perpetual, 256 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 257 | copyright license to reproduce, prepare Derivative Works of, 258 | publicly display, publicly perform, sublicense, and distribute the 259 | Work and such Derivative Works in Source or Object form. 260 | 261 | 3. Grant of Patent License. Subject to the terms and conditions of 262 | this License, each Contributor hereby grants to You a perpetual, 263 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 264 | (except as stated in this section) patent license to make, have made, 265 | use, offer to sell, sell, import, and otherwise transfer the Work, 266 | where such license applies only to those patent claims licensable 267 | by such Contributor that are necessarily infringed by their 268 | Contribution(s) alone or by combination of their Contribution(s) 269 | with the Work to which such Contribution(s) was submitted. If You 270 | institute patent litigation against any entity (including a 271 | cross-claim or counterclaim in a lawsuit) alleging that the Work 272 | or a Contribution incorporated within the Work constitutes direct 273 | or contributory patent infringement, then any patent licenses 274 | granted to You under this License for that Work shall terminate 275 | as of the date such litigation is filed. 276 | 277 | 4. Redistribution. You may reproduce and distribute copies of the 278 | Work or Derivative Works thereof in any medium, with or without 279 | modifications, and in Source or Object form, provided that You 280 | meet the following conditions: 281 | 282 | (a) You must give any other recipients of the Work or 283 | Derivative Works a copy of this License; and 284 | 285 | (b) You must cause any modified files to carry prominent notices 286 | stating that You changed the files; and 287 | 288 | (c) You must retain, in the Source form of any Derivative Works 289 | that You distribute, all copyright, patent, trademark, and 290 | attribution notices from the Source form of the Work, 291 | excluding those notices that do not pertain to any part of 292 | the Derivative Works; and 293 | 294 | (d) If the Work includes a "NOTICE" text file as part of its 295 | distribution, then any Derivative Works that You distribute must 296 | include a readable copy of the attribution notices contained 297 | within such NOTICE file, excluding those notices that do not 298 | pertain to any part of the Derivative Works, in at least one 299 | of the following places: within a NOTICE text file distributed 300 | as part of the Derivative Works; within the Source form or 301 | documentation, if provided along with the Derivative Works; or, 302 | within a display generated by the Derivative Works, if and 303 | wherever such third-party notices normally appear. The contents 304 | of the NOTICE file are for informational purposes only and 305 | do not modify the License. You may add Your own attribution 306 | notices within Derivative Works that You distribute, alongside 307 | or as an addendum to the NOTICE text from the Work, provided 308 | that such additional attribution notices cannot be construed 309 | as modifying the License. 310 | 311 | You may add Your own copyright statement to Your modifications and 312 | may provide additional or different license terms and conditions 313 | for use, reproduction, or distribution of Your modifications, or 314 | for any such Derivative Works as a whole, provided Your use, 315 | reproduction, and distribution of the Work otherwise complies with 316 | the conditions stated in this License. 317 | 318 | 5. Submission of Contributions. Unless You explicitly state otherwise, 319 | any Contribution intentionally submitted for inclusion in the Work 320 | by You to the Licensor shall be under the terms and conditions of 321 | this License, without any additional terms or conditions. 322 | Notwithstanding the above, nothing herein shall supersede or modify 323 | the terms of any separate license agreement you may have executed 324 | with Licensor regarding such Contributions. 325 | 326 | 6. Trademarks. This License does not grant permission to use the trade 327 | names, trademarks, service marks, or product names of the Licensor, 328 | except as required for reasonable and customary use in describing the 329 | origin of the Work and reproducing the content of the NOTICE file. 330 | 331 | 7. Disclaimer of Warranty. Unless required by applicable law or 332 | agreed to in writing, Licensor provides the Work (and each 333 | Contributor provides its Contributions) on an "AS IS" BASIS, 334 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 335 | implied, including, without limitation, any warranties or conditions 336 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 337 | PARTICULAR PURPOSE. You are solely responsible for determining the 338 | appropriateness of using or redistributing the Work and assume any 339 | risks associated with Your exercise of permissions under this License. 340 | 341 | 8. Limitation of Liability. In no event and under no legal theory, 342 | whether in tort (including negligence), contract, or otherwise, 343 | unless required by applicable law (such as deliberate and grossly 344 | negligent acts) or agreed to in writing, shall any Contributor be 345 | liable to You for damages, including any direct, indirect, special, 346 | incidental, or consequential damages of any character arising as a 347 | result of this License or out of the use or inability to use the 348 | Work (including but not limited to damages for loss of goodwill, 349 | work stoppage, computer failure or malfunction, or any and all 350 | other commercial damages or losses), even if such Contributor 351 | has been advised of the possibility of such damages. 352 | 353 | 9. Accepting Warranty or Additional Liability. While redistributing 354 | the Work or Derivative Works thereof, You may choose to offer, 355 | and charge a fee for, acceptance of support, warranty, indemnity, 356 | or other liability obligations and/or rights consistent with this 357 | License. However, in accepting such obligations, You may act only 358 | on Your own behalf and on Your sole responsibility, not on behalf 359 | of any other Contributor, and only if You agree to indemnify, 360 | defend, and hold each Contributor harmless for any liability 361 | incurred by, or claims asserted against, such Contributor by reason 362 | of your accepting any such warranty or additional liability. 363 | 364 | END OF TERMS AND CONDITIONS 365 | 366 | APPENDIX: How to apply the Apache License to your work. 367 | 368 | To apply the Apache License to your work, attach the following 369 | boilerplate notice, with the fields enclosed by brackets "{}" 370 | replaced with your own identifying information. (Don't include 371 | the brackets!) The text should be enclosed in the appropriate 372 | comment syntax for the file format. We also recommend that a 373 | file or class name and description of purpose be included on the 374 | same "printed page" as the copyright notice for easier 375 | identification within third-party archives. 376 | 377 | Copyright {yyyy} {name of copyright owner} 378 | 379 | Licensed under the Apache License, Version 2.0 (the "License"); 380 | you may not use this file except in compliance with the License. 381 | You may obtain a copy of the License at 382 | 383 | http://www.apache.org/licenses/LICENSE-2.0 384 | 385 | Unless required by applicable law or agreed to in writing, software 386 | distributed under the License is distributed on an "AS IS" BASIS, 387 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 388 | See the License for the specific language governing permissions and 389 | limitations under the License. 390 | --------------------------------------------------------------------------------