Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Call C# functions from anywhere using URL (HTTP server)
#1
Code:
Copy      Help
// 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;

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];
            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;
                        if (true != d?.TryGetValue(name, out s) && true == m.Multipart?.TryGetValue(name, out var u)) s = u.Text ?? u.Content?.ToStringUTF8();
                        if (s != null) a[i] = t == typeof(string) ? s : Convert.ChangeType(s, t);
                    }

                    a[i] ??= v.DefaultValue is not DBNull ? 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) r.SetContentText(g.ToString());
        }
else {
            r.Status = HttpStatusCode.NotFound;
        }
    }
}


//Add your functions in this class. To call public functions, clients use HTTP GET or POST request with URL like "http://ThisComputer:4455/FunctionName".
//Clients can specify parameter names and values in URL like "http://ThisComputer:4455/FunctionName?a=text&b=100". 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:
//    string and types that implement IConvertible (int, bool, etc);
//    HSContentPart (if want to access posted binary data etc);
//    HSMessage, HSResponse (if want to access everything).
//Your functions can optionally return a non-null value of any type as the response text.
//Functions are executed in new thread for each new connection.

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
    }
    #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.
#2
Hi Gintaras, Although you have explained it clearly.Can you be more specific about what scenarios this script can play a role in?Because I know too little, but I'm very interested. Thank you!
#3
Roles.

To run C# functions or scripts on the server computer from other programs or scripts that can send HTTP requests. For example from curl, QM, AutoHotkey, PowerAutomate, UiPath. It's like a simple RPC (remote procedure call). You specify URL, function name, parameters, and receive a response text. I'll post some examples later.

To run C# functions or scripts on the server computer from other computers on local network. Also from computers, smartphones etc from anywhere on the internet, although probably will need some software to enable it.
#4
[Image: ICXuFo0.jpg]
Thank you so much Gintaras. This is so amazing!
#5
C# client examples.

Code:
Copy      Help
using System.Net.Http;

#region examples
LaHttp.Call("Simple"); //"http://localhost:4455/Simple"

LaHttp.Call("http://:password278351@localhost:4455/Simple");

string s1 = LaHttp.CallS("Returns");
print.it(s1);

LaHttp.Call("Parameters", "a=aa", "c=10", "d=true");

int i1 = 3, i2 = 4;
int i3 = LaHttp.CallS("Add", $"x={i1}", $"y={i2}").ToInt();
print.it(i3);

POINT p1 = new POINT(2, 3);
POINT p2 = LaHttp.CallJ("Json", p1).Json<POINT>();
print.it(p2);
#endregion

/// <summary>
///
Calls C# functions on a computer where <see href="https://www.libreautomate.com/forum/showthread.php?tid=7468">LibreAutomate HTTP server</see> is running.
/// </summary>
///
<example>
///
<code><![CDATA[
/// LaHttp.Call("Simple"); //"http://localhost:4455/Simple"
///
/// LaHttp.Call("http://:password278351@localhost:4455/Simple");
///
/// string s1 = LaHttp.CallS("Returns");
/// print.it(s1);
///
/// LaHttp.Call("Parameters", "a=aa", "c=10", "d=true");
///
/// int i1 = 3, i2 = 4;
/// int i3 = LaHttp.CallS("Add", $"x={i1}", $"y={i2}").ToInt();
/// print.it(i3);
///
/// POINT p1 = new POINT(2, 3);
/// POINT p2 = LaHttp.CallJ("Json", p1).Json<POINT>();
/// print.it(p2);
/// ]]></code>
///
</example>
public class LaHttp {
    /// <summary>
    ///
Calls a function with 0 or more parameters.
    /// </summary>
    ///
<param name="url">
    ///
URL like "http://HOST:4455/FunctionName".
    /// <br/>• HOST can be: localhost (this computer); IP; LAN computer name; web server (like www.example.com).
    /// <br/>• Password can be specified like "http://:password@HOST:4455/FunctionName".
    /// <br/>• If the HTTP server is on the same computer and don't need a password, can be just function name.
    /// </param>
    ///
<param name="a">Parameter like <c>"name=value"</c>.</param>
    public static HttpResponseMessage Call(string url, params string[] a) {
        _Url(ref url, out var auth);
        var c = new MultipartFormDataContent();
        foreach (var s in a) {
            string n, v;
            int i = s.IndexOf('='); if (i < 1) throw new ArgumentException("Expected \"name=value\"", "a");
            n = s[..i]; v = s[++i..];
            if (n[0] == '@') c.AddFile(n[1..], v); else c.Add(n, v);
        }

        return _Post(url, c, auth);
    }

    
    /// <summary>
    ///
Calls a function and passes an object of any type converted to JSON.
    /// </summary>
    ///
<inheritdoc cref="Call(string, string[])"/>
    public static HttpResponseMessage CallJ<T>(string url, T x) {
        _Url(ref url, out var auth);
        return _Post(url, internet.jsonContent(x), auth);
    }

    
    static void _Url(ref string url, out string auth) {
        auth = null;
        if (!url.RxIsMatch("^https?://")) url = "http://localhost:4455/" + url;
        else if (url.RxMatch(@"^https?://(.*?:.*?)@", 1, out RXGroup g)) { auth = g.Value; url = url.Remove(g.Start, g.Length + 1); }
    }

    
    static HttpResponseMessage _Post(string url, HttpContent c, string auth) {
        var r = internet.http.Post(url, c, auth: auth);
        if (!r.IsSuccessStatusCode) {
            var text = r.Text(ignoreError: true);
            if (text.NE()) r.EnsureSuccessStatusCode();
            else throw new HttpRequestException($"Status {(int)r.StatusCode}. {text}", null, r.StatusCode);
        }

        return r;
    }

    
    /// <summary>
    ///
Calls a function with 0 or more parameters, and returns the response text.
    /// </summary>
    ///
<inheritdoc cref="Call(string, string[])"/>
    public static string CallS(string url, params string[] a)
        => Call(url, a).Text();
    
    /// <summary>
    ///
Calls a function, passes an object of any type converted to JSON, and returns the response text.
    /// </summary>
    ///
<inheritdoc cref="Call(string, string[])"/>
    public static string CallJS<T>(string url, T x) //note: not CallS overload. Then goes here if single parameter.
        => CallJ(url, x).Text();
    
    //rejected. It's easier to use code Call(...).Json<TReturn>();.
    //public static TReturn CallJJ<TParam, TReturn>(string url, TParam x)
    //    => Call(url, x).Json<TReturn>();

}

Changes
2023-06-22:
- Supports error descriptions in response body, to match the updated HTTP server code.
- Some functions renamed.
- Changed the type of parameters. Was tuple ("name", "value"), now string "name=value".
- And more improvements.
#6
QM client examples.

Macro Macro3182
 
Code:
Copy      Help
LaHttpCall("Simple") ;;"http://localhost:4455/Simple"

LaHttpCall("http://:password278351@localhost:4455/Simple");

str r1 = LaHttpCall("Returns")
out r1

LaHttpCall("Parameters" "a=aa" "c=10" "d=true")

str text = "any text"
str path = "C:\Test\test.txt"
LaHttpCall("Parameters" F"a={text}" F"@b={path}")

int x(5) y(2)
int r2 = val(LaHttpCall("Add" F"x={x}" F"y={y}"))
out r2

Function LaHttpCall
Code:
Copy      Help
;/
function~ ~url [~a1] [~a2] [~a3] [~a4] [~a5] [~a6] [~a7] [~a8] [~a9] [~a10]

;Calls a C# function on a computer where <link "https://www.libreautomate.com/forum/showthread.php?tid=7468">LibreAutomate HTTP server</link> is running.

;url - URL like "http://HOST:4455/FunctionName".
;;;;HOST can be: localhost (this computer); IP; LAN computer name; web server (like www.example.com).
;;;;Password can be specified like "http://:password@HOST:4455/FunctionName".
;;;;If the HTTP server is on the same computer and don't need a password, can be just function name.
;a1-a10 - C# function parameters, like "name=value". To send a file, use "@name=filepath". Parameters can be in any order, optionals omitted. Don't need urlencoding.

;EXAMPLES
;LaHttpCall("Simple") ;;"http://localhost:4455/Simple"
;
;LaHttpCall("http://:password278351@localhost:4455/Simple");
;
;str r1 = LaHttpCall("Returns")
;out r1
;
;LaHttpCall("Parameters" "a=aa" "c=10" "d=true")
;
;str text = "any text"
;str path = "C:\Test\test.txt"
;LaHttpCall("Parameters" F"a={text}" F"@b={path}")
;
;int x(5) y(2)
;int r2 = val(LaHttpCall("Add" F"x={x}" F"y={y}"))
;out r2


if(findrx(url "^https?://")<0) url-"http://localhost:4455/"
ARRAY(str) a
if(findrx(url F"^https?://(?:(.*?):(.*?)@)?(\[.+?\]|[^\s:/?]+)(:\d+)?/(\w+)$" 0 0 a)<0) end F"{ERR_BADARG} url"

if(a[2].len+a[1].len>0) str auth.from("Authorization: Basic " _s.encrypt(4 F"{a[1]}:{a[2]}"))

Http http.Connect(a[3] "" "" iif(a[4].len val(a[4]+1) iif(url[4]='s' 443 0)))
int i e ok; str r h sn sv
str* p=&a1
for i 0 getopt(nargs)-1
,str& s=p[i]
,e=findc(s '='); if(e<1) end F"{ERR_BADARG} a{i+1}. Must be name=value."
,int isFile=s[0]='@'
,http.PostAdd(sn.get(s isFile e-isFile) sv.get(s e+1) isFile)
if(!http.PostFormData(a[5] 0 r auth 0 0 0 h)) end F"{ERR_FAILED}. {http.lasterror}"
_s.get(h 9 findc(h 13)-9)
if(!_s.beg("200 ")) if(r.len) end F"{ERR_FAILED}. {r}"; else end F"{ERR_FAILED}. HTTP status: {_s}"
ret r

Changes
2023-06-22:
- Supports error descriptions in response body, to match the updated HTTP server code.
#7
AutoHotkey client examples.
 
Code:
Copy      Help
LaHttpCall("http://localhost:4455/Parameters", "a=aa&b=one+two", "password278351")

MsgBox % LaHttpCall("http://localhost:4455/Add", "x=100&y=2")

LaHttpCall(url, params:="", password:="") {
    http := ComObjCreate("WinHttp.Winhttprequest.5.1")
    http.Open("POST", url)
    if not password=""
        http.SetCredentials("-", password, 0)
    http.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded")
    http.Send(params)
    return http.ResponseText
}
#8
This function compiles and executes C# code sent by clients.

Setup

1. Get this C# compiler.
2. Create folder "@HTTP server". Can be any name with @ prefix.
3. Move the HTTP server script into the new folder. Let it always be the first file in the folder.
4. Add this class file to the folder:
 
Code:
Copy      Help
// class "HTTP server C# compiler.cs"
/*/ c CsScript.cs; /*/
using System.Reflection;
using System.Net;

static partial class Functions {
    /// <summary>
    ///
Compiles and executes C# code.
    /// Uses CsScript.cs from https://www.libreautomate.com/forum/showthread.php?tid=7491.
    /// </summary>
    ///
<remarks>
    ///
Let clients pass these POST fields or GET parameters:
    /// <br/>• code - C# code. In code can be used everything from .NET, Au.dll and _r libraries.
    /// <br/>• fun (optional) - a function defined in the C# code, like "Class1.Function1".
    /// <br/>• _c (optional) - C# files to compile together, like "file1.cs|file2.cs". If does not end with ".cs" - C# code to compile together as separate file. More info in CsScript.Compile documentation. The files must be on the server's computer.
    /// <br/>• _r (optional) - additional reference assemblies, like "file1.dll|file2.dll". More info in CsScript.Compile documentation. The files must be on the server's computer.
    /// <br/>• Names starts with _ - not used. Reserved.
    /// <br/>• Other - function arguments (if used fun) or values for the args array.
    /// <para>
    ///
If fun used, creates a library assembly and calls the function, which must be public static, with zero or more parameters of type string, int, bool and other types that implement IConvertible. Clients must use these parameter names, in any order, and can omit optionals.
    /// If fun omitted or empty, creates an executable assembly and calls the entry point (top-level statements or Main). Parameter values are in the args variable (string[]). Clients can use any parameter names, they are ignored.
    /// </para>
    ///
Slow when used the first time in the server process, because loads and JIT-compiles the C# compiler. Afterwards should be ~100 ms, depending on computer speed and antivirus program. Always compiles, does not use caching.
    /// </remarks>
    ///
<returns>An object containing the return value of the invoked method, or null if void.</returns>
    public static object Code(HSMessage m, HSResponse r) {
        MethodInfo mi;
        object[] a;
        try {
            var d = m.Method == "GET" ? m.UrlParameters : m.Urlencoded ?? m.Multipart?.ToDictionary(o => o.Key, o => o.Value.Text) ?? new();
            string code = d["code"];
            bool library = d.TryGetValue("fun", out var fun) && !fun.NE();
            string[] ac = null, ar = null;
            if (d.TryGetValue("_c", out var s1)) ac = s1.Ends(".cs", true) ? s1.Split('|') : new[] { s1 };
            if (d.TryGetValue("_r", out var s2)) ar = s2.Split('|');
            
            List<string> errors = new();
            var c = CsScript.Compile(code, library, ac, ar, errors);
            if (c == null) return _Bad("Errors in C# code:\r\n  " + string.Join("\r\n  ", errors));
            
            if (library) {
                int dot = fun.IndexOf('.'); if (dot < 0) return _Bad("Expected \"fun=Class.Method\"");
                var cla = fun[..dot];
                fun = fun[++dot..];
                var type = c.Assembly.GetType(cla); if (type == null) return _Bad("class not found: " + cla);
                mi = type.GetMethod(fun, BindingFlags.Public | BindingFlags.Static); if (mi == null) return _Bad("public static method not found: " + fun);
                
                //code like in HTTP server.cs
                var p = mi.GetParameters();
                a = new object[p.Length];
                for (int i = 0; i < p.Length; i++) {
                    var v = p[i];
                    var t = v.ParameterType;
                    if (d.TryGetValue(v.Name, out var s)) a[i] = t == typeof(string) ? s : Convert.ChangeType(s, t);
                    a[i] ??= v.DefaultValue is not DBNull ? v.DefaultValue : throw new Exception($"Parameter {v.Name} not optional");
                }
            }
else {
                mi = c.Assembly.EntryPoint;
                var args = d.Where(o => o.Key is not ("code" or "fun" or ['_', ..])).Select(o => o.Value).ToArray(); //BAD: no 100% guarantee the order will be preserved in future .NET versions
                a = new object[] { args };
            }
        }

        catch (Exception e1) { return _Bad(e1.Message); }
        
        return mi.Invoke(null, a);
        
        object _Bad(string s) {
            r.Status = HttpStatusCode.BadRequest;
            r.SetContentText(s);
            return null;
        }
    }
}

C# client examples
 
Code:
Copy      Help
string code = """class C { public static int Add(int a, int b=0){return a+b;} }""";
print.it(LaHttp.CallS("Code", $"code={code}", "fun=C.Add", "a=5", "b=3"));
 
Code:
Copy      Help
string code = """using Au; print.it(args); return 1;""";
print.it(LaHttp.CallS("Code", $"code={code}", "a=5", "b=3"));
 
Code:
Copy      Help
var globals = """
global using System;
global using Au;
"""
;
string code = """print.it(args); return 1;""";
print.it(LaHttp.CallS("Code", $"code={code}", "a=5", "b=3", $"_c={globals}"));
 
Code:
Copy      Help
var globals = folders.Workspace + @"files\Classes\global.cs";
string code = """print.it(args); return 1;""";
print.it(LaHttp.CallS("Code", $"code={code}", "a=5", "b=3", $"_c={globals}"));

LaHttp is in some post in this thread.

QM client example

Code:
Copy      Help
str code=
;class C { public static int Add(int a, int b=0){return a+b;} }
out LaHttpCall("Code" F"code={code}" "fun=C.Add" "a=5" "b=4")

LaHttpCall is in some post in this thread.
#9
After compiling a large amount of C# code, the process occupies too much memory (800M or more). How can it be optimized?
#10
Is it a single compilation with large code? Or many compilations with normal amount of code?

If many, the most likely reason is - cannot unload temporary memory assemblies. It depends on your C# code.

To see whether assemblies are unloaded:
Install LA 1.0.1 (not 1.0.0).
Debug-run the HTTP server script.
In the debug options menu (the last toolbar button) check "Print module loaded/unloaded".
Run your C# codes many times.
Should print "Module loaded: script" each time, and "Module unloaded: script" sometimes randomly.
If prints only "loaded" but no "unloaded", need to edit your codes. More info:
https://learn.microsoft.com/en-us/dotnet...oadability

Examples:
 
Code:
Copy      Help
for (int i = 0; i < 100; i++) {
    //this code ic correct. The process never eats more than 60 MB.
    string code = """class C { public static int Add(int a, int b=0){return a+b;} }""";
    
    //this code creates a "cannot unload assembly" condition. The process eats ~400 MB after 100 runs.
    //string code = """class C { public static int Add(int a, int b=0){ var m = new System.Drawing.Bitmap(1000, 1000); Au.timer2.after(999999, _ => { m.Dispose(); }); return a+b;} }""";
    
    //another example of code that creates a "cannot unload assembly" condition. The process eats ~400 MB after 100 runs.
    //string code = """class C { public static int Add(int a, int b=0){ var m = new System.Drawing.Bitmap(1000, 1000); System.Runtime.InteropServices.GCHandle.Alloc(m); return a+b;} }""";

    
    print.it(LaHttp.CallS("Code", $"code={code}", "fun=C.Add", $"a={i}", "b=3"));
}
#11
Thank you for the prompt.
I'm using HTTP to call LA a third-party DLL in QM, which seems a bit complex.
Currently, I'm using a third-party utility to reduce memory usage, and it's effective.
#12
Spire.Doc.dll is not unloadable. Therefore cannot avoid this memory leak when using it like this. Use it in normal way: place the C# code in a function in LA, and just call the function from QM.
#13
Thank you, the code works fine in LA
#14
AutoHotkey v2 example (edited to add try/catch):
 
Code:
Copy      Help
#Requires AutoHotkey 2.0

LaHttpCall("http://localhost:4455/Parameters", "a=aa&b=x+y", "password278351")

MsgBox(LaHttpCall("http://localhost:4455/Add", "x=100&y=2"))

LaHttpCall(url, params:="", password:="") {
    http := ComObject("WinHttp.Winhttprequest.5.1")
    http.Open("POST", url)
    if (password != "")
        http.SetCredentials("-", password, 0)
    http.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded")
    try {
        http.Send(params)
    } catch {
        MsgBox("No server!")
        ExitApp()
    }
    return http.ResponseText
}
#15
About #1 code:

How to access this service via HTTPS? My default HTTPS port(443) is blocked, so I have to use a different port.
The image below shows the HTTPS settings in a certain software, where I can customize the port number and select a certificate. I can access the services in the software through the settings shown in this image.

(I have already purchased a domain name and applied for an SSL certificate for it. I have also pointed the domain name to the public IP address of this computer.)
https://i.ibb.co/RzccB8F/886.png

The HTTP request link is no longer usable in some instant messaging software. When I click on the link, there's no response, or it prompts me to use HTTPS instead.
So, now I need to access LA HTTP server using HTTPS . Thanks in advance for any advice and assistance.
#16
https://www.libreautomate.com/api/Au.Mor...isten.html
#17
Since, as the docs say,
Quote:This server does not support https (secure connections)
you could try some of the tunneling solutions listed here:
https://github.com/anderspitman/awesome-tunneling
The page has lots of very useful information as well as a solution you might be able to use.
#18
Gintaras  burque505
Thanks for your help, I'm considering using Nginx for implementation.

Nginx is lightweight. It would be great if someone could provide operational guidance for its use in LA, similar to the one below.
https://jerrington.me/posts/2019-01-29-s...ngrok.html


Forum Jump:


Users browsing this thread: 1 Guest(s)