// script "HTTP server.cs"
/// Simple HTTP server that executes your functions defined in this script.
/// Add any functions to the Functions class, and run this script. Several examples are included.
/// Then any script or program on this computer or any device in the world can use URL like "http://ThisComputer:4455/FunctionName?a=100" to execute these functions.
/// More info in <see cref="HttpServerSession.Listen"/> documentation (click Listen and press F1).
using System.Net;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
HttpServerSession.Listen<HttpSession>();
class HttpSession : HttpServerSession {
protected override void MessageReceived(HSMessage m, HSResponse r) {
//Auth((u, p) => p == "password278351"); //enable this if want to protect this HTTP server with a password
//print.it(m.Method, m.TargetPath, m.UrlParameters, m.Text, m.Headers);
Dictionary<string, string> d = null;
if (m.Method == "GET") {
d = m.UrlParameters;
} else if (m.Method == "POST") {
d = m.Urlencoded;
} else {
r.Status = HttpStatusCode.BadRequest;
return;
}
if (m.TargetPath.Starts("/") && typeof(Functions).GetMethod(m.TargetPath[1..]) is { } mi) {
var p = mi.GetParameters();
var a = new object[p.Length];
bool contentUsed = false;
try {
for (int i = 0; i < p.Length; i++) {
var v = p[i];
var name = v.Name;
var t = v.ParameterType;
if (t == typeof(HSMessage)) a[i] = m;
else if (t == typeof(HSResponse)) a[i] = r;
else if (t == typeof(HSContentPart)) a[i] = m.Multipart?.GetValueOrDefault(name);
else {
string s = null;
bool isArg = true == d?.TryGetValue(name, out s);
if (!isArg) if (true == m.Multipart?.TryGetValue(name, out var u)) { isArg = true; s = u.Text ?? u.Content?.ToStringUTF8(); }
if (isArg) {
if (s != null) {
if (t == typeof(string)) a[i] = s;
else if (s[0] is '{' or '[') a[i] = _FromJson(s);
else a[i] = Convert.ChangeType(s, t);
}
} else if (!contentUsed && !v.IsOptional && m.Text is string text) {
contentUsed = true;
if (t == typeof(string)) a[i] = text;
else if (m.ContentType?.MediaType == "application/json" || (text.Length >= 2 && text[0] is '{' or '[')) a[i] = _FromJson(text);
}
object _FromJson(string s) {
if (t.IsAssignableTo(typeof(JsonNode))) return JsonNode.Parse(s);
return JsonSerializer.Deserialize(s, t);
}
}
a[i] ??= v.IsOptional ? v.DefaultValue : throw new Exception($"Parameter {name} not optional");
}
}
catch (Exception e1) {
r.Status = HttpStatusCode.BadRequest;
r.SetContentText(e1.Message);
return;
}
var g = mi.Invoke(null, a);
if (g != null) {
if (mi.ReturnTypeCustomAttributes.IsDefined(typeof(JSONAttribute), false) && g.GetType() is { } t1) {
r.Content = JsonSerializer.SerializeToUtf8Bytes(g, t1, s_defaultSerializerOptions.Value);
r.Headers["Content-Type"] = "application/json; charset=utf-8";
} else {
r.SetContentText(g.ToString());
}
}
} else {
r.Status = HttpStatusCode.NotFound;
}
}
static readonly Lazy<JsonSerializerOptions> s_defaultSerializerOptions = new(() => new(JsonSerializerDefaults.Web));
}
/// <summary>
/// If a function has this attribute, the returned object will be converted to JSON string and returned as response content + corresponding <c>Content-Type</c> header.
/// Example: <c>[return: JSON] object ReturnJsonExample() { ... return objectOfAnyType; }</c>.
/// </summary>
[AttributeUsage(AttributeTargets.ReturnValue)]
sealed class JSONAttribute : Attribute { }
/// <summary>
/// Add your functions to this class. To call public functions, clients use HTTP GET or POST request with URL like <c>"http://ThisComputer:4455/FunctionName"</c>.
/// Clients can specify parameter names and values in URL like <c><![CDATA["http://ThisComputer:4455/FunctionName?a=text&b=100"]]></c>. Or POST urlencoded or multipart parameters or any data.
/// Clients must specify parameter names. The order does not matter. Can omit optional parameters.
/// Your functions can have parameters of these types:
/// <br/>• string.
/// <br/>• Types that <b>Convert.ChangeType</b> can convert from string (int, bool, etc).
/// <br/>• <b>JsonNode</b>, <b>JsonObject</b>, <b>JsonArray</b> and types that <b>JsonSerializer.Deserialize</b> can deserialize. The argument value must be JSON.
/// <br/>• <b>HSContentPart</b> (if want to access posted binary data etc).
/// <br/>• <b>HSMessage</b>, <b>HSResponse</b> (if want to access everything).
/// Also clients can pass a simple or JSON string in HTTP message content rather than in URL etc; the server assigns the string or JSON-deserialized object to the first unspecified non-optional parameter of type string or <b>JsonNode</b>, <b>JsonObject</b>, <b>JsonArray</b> or a type that <b>JsonSerializer.Deserialize</b> can deserialize.
/// Your functions can optionally return a non-null value of any type as the response text.
/// Functions that have overloads are not supported.
/// Functions are executed in new thread for each new connection.
/// </summary>
static partial class Functions {
#region examples
//client example: internet.http.Get("http://localhost:4455/Simple");
//client example: internet.http.Get("http://localhost:4455/Simple", auth: ":password278351");
public static void Simple() {
print.it("Simple");
}
//client example: string s = internet.http.Get("http://localhost:4455/Returns").Text(); print.it(s);
public static string Returns() {
return "RESPONSE";
}
//client example: internet.http.Get("http://localhost:4455/Parameters?a=aa&c=10&d=true");
//client example: internet.http.Post("http://localhost:4455/Parameters", internet.formContent(("a", "aa"), ("c", 10)));
public static void Parameters(string a, string b = null, int c = 0, bool d = false) {
print.it(a, b, c, d);
}
//client example: int r = internet.http.Get("http://localhost:4455/Add?x=4&y=7").Text().ToInt(); print.it(r);
public static int Add(int x, int y) => x + y;
//client example: var p = new POINT(3, 4); var p2 = internet.http.Post("http://localhost:4455/Json", internet.jsonContent(p)).Json<POINT>(); print.it(p2);
public static void Json(HSMessage m, HSResponse r) {
POINT p = m.Json<POINT>();
print.it(p);
var p2 = new POINT(5, 6);
r.SetContentJson(p2); //return JSON
}
//client example: internet.http.Post("http://localhost:4455/JsonSimple", internet.jsonContent(new R1(5, "text"))); record R1(int i, string s);
//client example with anonymous type: internet.http.Post("http://localhost:4455/JsonSimple", internet.jsonContent(new { i = 5, s = "text" }));
public static void JsonSimple(R1 x) {
print.it(x);
}
public record R1(int i, string s);
//client example: string s = internet.http.Get("http://localhost:4455/ReturnJsonSimple").Text(); print.it(s);
//client example: var v = internet.http.Get("http://localhost:4455/ReturnJsonSimple").Json<R1>(); print.it(v); record R1(int i, string s);
[return: JSON]
public static R1 ReturnJsonSimple() => new(4, "example"); //use a defined type
//public static object ReturnJsonSimple() => new { s = "S", i = 5, b = true, a = new object[] { "a0", "a1" } }; //use anonymous type
#endregion
}
Changes
2023-06-22:
- Returns error descriptions in response body, not in status reason phrase.
- Updated client functions in other posts below, to match the above change.
2024-05-06:
- Simpler to use JSON. See examples near the bottom.
2024-05-09:
- Simpler to return JSON. See examples near the bottom.