[GH-ISSUE #1387] Provide a High-level Go API #1096

Closed
opened 2026-05-05 12:42:28 -06:00 by gitea-mirror · 6 comments
Owner

Originally created by @velovix on GitHub (Aug 15, 2019).
Original GitHub issue: https://github.com/fatedier/frp/issues/1387

I have a use-case where I need to spin up reverse proxy servers automatically on-demand. This would be very easy to do with FRP if it exposed an API for starting servers in code, but near as I can tell FRP is not designed to be used as a library. I wonder if there is interest in providing this high-level API to other Go programs. If there is, this is something I would like to work on.

The following are the problems I've identified that prevent FRP from being usable as a library, and what I would do to fix them. There may be more issues that I've yet to identify, but I would be more than happy to dig deeper if there's any interest in making this happen.

The Problem

FRP has very friendly-looking functions like server.NewService() that, at first glance, look like exactly the kind of high-level API that I am describing. However, there are a few problems:

  • FRP uses a global configuration object. This makes sense for a command line tool, but makes less sense for a library. Specifically, it prevents creating more than one server with different configurations.
  • server.NewService() and client.NewService() expect to have access to static resources for the monitoring panel. Again, this works for a tool but not for a library.
  • Code is mostly documented inside the implementation instead of as top-level comments viewable on GoDoc.

The Solution

I am only familiar at a surface level with FRP's code, so I suspect some of what I'm saying may be unreasonable or undesirable for reasons I'm not aware of. That said, here is how I would solve the above problems:

  • Pass around configuration objects to functions instead of using a global configuration object. This would allow for more multiple servers to be run at once and I think would lead to a more natural API.
  • Either...
    • Only load assets if the dashboard is enabled. This would be a good enough solution if we don't expect people who use FRP as a library to want the dashboard.
    • Allow users to provide their own implementation of http.FileSystem or some other interface that would override the use of statik.
  • Add library documentation. This one is pretty straightforward :)
Originally created by @velovix on GitHub (Aug 15, 2019). Original GitHub issue: https://github.com/fatedier/frp/issues/1387 I have a use-case where I need to spin up reverse proxy servers automatically on-demand. This would be very easy to do with FRP if it exposed an API for starting servers in code, but near as I can tell FRP is not designed to be used as a library. I wonder if there is interest in providing this high-level API to other Go programs. If there is, this is something I would like to work on. The following are the problems I've identified that prevent FRP from being usable as a library, and what I would do to fix them. There may be more issues that I've yet to identify, but I would be more than happy to dig deeper if there's any interest in making this happen. # The Problem FRP has very friendly-looking functions like `server.NewService()` that, at first glance, look like exactly the kind of high-level API that I am describing. However, there are a few problems: - FRP uses a global configuration object. This makes sense for a command line tool, but makes less sense for a library. Specifically, it prevents creating more than one server with different configurations. - `server.NewService()` and `client.NewService()` expect to have access to static resources for the monitoring panel. Again, this works for a tool but not for a library. - Code is mostly documented inside the implementation instead of as top-level comments viewable on GoDoc. # The Solution I am only familiar at a surface level with FRP's code, so I suspect some of what I'm saying may be unreasonable or undesirable for reasons I'm not aware of. That said, here is how I would solve the above problems: - Pass around configuration objects to functions instead of using a global configuration object. This would allow for more multiple servers to be run at once and I think would lead to a more natural API. - Either... - Only load assets if the dashboard is enabled. This would be a good enough solution if we don't expect people who use FRP as a library to want the dashboard. - Allow users to provide their own implementation of `http.FileSystem` or some other interface that would override the use of `statik`. - Add library documentation. This one is pretty straightforward :)
gitea-mirror 2026-05-05 12:42:28 -06:00
Author
Owner

@fatedier commented on GitHub (Aug 15, 2019):

Thank you for your valuable advise.

I'm glad to see this changes but i need sometime to find the suitable way to do these things.

Pass around configuration objects to functions instead of using a global configuration object.

I'm not sure if it's the suitable way to just make the global configuration object a function parameter. Or we can split it into some small objects for different modules.

Only load assets if the dashboard is enabled. This would be a good enough solution if we don't expect people who use FRP as a library to want the dashboard.

Yes, this is easy to modify.

Allow users to provide their own implementation of http.FileSystem or some other interface that would override the use of statik.

Now it support statik and static file path configured by assets_dir.

Add library documentation

It's a hard work while it really lack of much documentation and unit tests.

<!-- gh-comment-id:521497205 --> @fatedier commented on GitHub (Aug 15, 2019): Thank you for your valuable advise. I'm glad to see this changes but i need sometime to find the suitable way to do these things. > Pass around configuration objects to functions instead of using a global configuration object. I'm not sure if it's the suitable way to just make the global configuration object a function parameter. Or we can split it into some small objects for different modules. > Only load assets if the dashboard is enabled. This would be a good enough solution if we don't expect people who use FRP as a library to want the dashboard. Yes, this is easy to modify. > Allow users to provide their own implementation of http.FileSystem or some other interface that would override the use of statik. Now it support statik and static file path configured by `assets_dir`. > Add library documentation It's a hard work while it really lack of much documentation and unit tests.
Author
Owner

@velovix commented on GitHub (Aug 15, 2019):

Thank you for your feedback! I'm glad there is interest in this.

I'm not sure if it's the suitable way to just make the global configuration object a function parameter. Or we can split it into some small objects for different modules.

Yes, as I look more into the code I agree. I still don't feel that I understand the client configuration enough to come up with a proposal, but for the server would it make sense to use setters?

Perhaps it would look something like this:

Server Setters Proposal
// NewService creates a server that will run on the given address and port.
func NewService(addr string, port int) (*Service, error) {
    // ...
}

// SetUDPPort starts listening for UDP connections on the given port.
func (svr *Service) SetUDPPort(port int) error {
    // ...
}

// SetKCPPort starts listening for KCP connections on the given port.
func (svr *Service) SetKCPPort(port int) error {
    // ...
}

// SetProxyBindAddr sets the proxy bind address to the given value. By default,
// the proxy bind address is set to the bind address.
func (svr *Service) SetProxyBindAddr(addr string) error {
	// ...
}

// SetVhostHTTPPort starts listening for virtual host HTTP requests on the
// given port.
func (svr *Service) SetVHostHTTPPort(port int) error {
	// ...
}


// SetVhostHTTPSPort starts listening for virtual host HTTP requests on the
// given port.
func (svr *Service) SetVhostHTTPSPort(port int) error {
	// ...
}

// SetVhostHTTPTimeout sets the vhost HTTP timeout to the given value, in
// seconds. If not set, the default timeout is 60 seconds.
func (svr *Service) SetVhostHTTPTimeout(timeout int) {
	// ...
}

// StartDashboard starts the dashboard, hosting on the given address and port.
// Static assets will be loaded from the given path. If assetsDir is an empty
// string, assets will be loaded statically.
func (svr *Service) StartDashboard(addr string, port int, assetsDir string) error {
	// ...
}

// SetDashboardCredentials sets the login information for the dashboard, by
// default both the username and password are "admin".
func (svr *Service) SetDashboardCredentials(username, password string) {
	// ...
}

// SetLogFile directs logging to the given file. By default, logging is sent to
// stdout. The maxDays parameter controls how many days logs are kept before
// being discarded.
func (svr *Service) SetLogFile(filepath string, maxDays int64) {
	// ...
}

// SetLogLevel sets the log level. Valid levels are "trace", "debug", "info",
// "warn", and "error". The default log level is "info".
func (svr *Service) SetLogLevel(logLevel string) {
	// ...
}

// SetToken sets the token value used by the server. Once the token is set,
// only clients that have a matching token are authorized to connect. By
// default, no token is required.
func (svr *Service) SetToken(token string) {
	// ...
}

// SetSubDomainHost sets the domain that will be appended to a requested
// sub-domain. If the client requests the proxy be at the sub-domain "test", and
// this value is "frps.com", the proxy will be available at "test.frps.com"
func (svr *Service) SetSubDomainHost(host string) {
	// ...
}

// SetTCPMux enables or disables TCP multiplexing. TCP multiplexing allows for
// multiple requests to share a single TCP connection.
func (svr *Service) SetTCPMux(mux bool) {
	// ...
}

// SetCustom404Page sets a custom 404 page to use for HTTP requests.
func (svr *Service) SetCustom404Page(filepath string) {
	// ...
}

// SetAllowedPorts sets the allowed ports that clients are allowed to bind to.
// An empty set allows for all ports to be bound.
func (svr *Service) SetAllowedPorts(ports map[string]struct{}) {
	// ...
}

// SetMaxPoolCount sets the maximum connection pool size.
func (svr *Service) SetMaxPoolCount(poolCount int) {
	// ...
}

// SetMaxPortsPerClient sets the maximum number of ports each client can bind
// to. A value of zero means no limit.
func (svr *Service) SetMaxPortsPerClient(portCount int) {
	// ...
}

// SetHeartbeatTimeout sets the timeout in seconds to wait for a heartbeat
// before terminating the connection. The default value is 90 seconds.
func (svr *Service) SetHeartbeatTimeout(timeout int) {
	// ...
}

// SetUserConnTimeout sets the timeout in seconds for client work connections.
// The default value is 10 seconds.
func (svr *Service) SetUserConnTimeout(timeout int) {
	// ...
}

Yes, this is easy to modify.

Great! I'll send a pull request.

Now it support statik and static file path configured by assets_dir.

Yes, you're right, I missed that. I guess no additional changes are needed here.

It's a hard work while it really lack of much documentation and unit tests.

Writing meaningful unit testing does seem very challenging for this domain. I'm not really sure how it would be done.

<!-- gh-comment-id:521821032 --> @velovix commented on GitHub (Aug 15, 2019): Thank you for your feedback! I'm glad there is interest in this. > I'm not sure if it's the suitable way to just make the global configuration object a function parameter. Or we can split it into some small objects for different modules. Yes, as I look more into the code I agree. I still don't feel that I understand the client configuration enough to come up with a proposal, but for the server would it make sense to use setters? Perhaps it would look something like this: <details> <summary>Server Setters Proposal</summary> ```go // NewService creates a server that will run on the given address and port. func NewService(addr string, port int) (*Service, error) { // ... } // SetUDPPort starts listening for UDP connections on the given port. func (svr *Service) SetUDPPort(port int) error { // ... } // SetKCPPort starts listening for KCP connections on the given port. func (svr *Service) SetKCPPort(port int) error { // ... } // SetProxyBindAddr sets the proxy bind address to the given value. By default, // the proxy bind address is set to the bind address. func (svr *Service) SetProxyBindAddr(addr string) error { // ... } // SetVhostHTTPPort starts listening for virtual host HTTP requests on the // given port. func (svr *Service) SetVHostHTTPPort(port int) error { // ... } // SetVhostHTTPSPort starts listening for virtual host HTTP requests on the // given port. func (svr *Service) SetVhostHTTPSPort(port int) error { // ... } // SetVhostHTTPTimeout sets the vhost HTTP timeout to the given value, in // seconds. If not set, the default timeout is 60 seconds. func (svr *Service) SetVhostHTTPTimeout(timeout int) { // ... } // StartDashboard starts the dashboard, hosting on the given address and port. // Static assets will be loaded from the given path. If assetsDir is an empty // string, assets will be loaded statically. func (svr *Service) StartDashboard(addr string, port int, assetsDir string) error { // ... } // SetDashboardCredentials sets the login information for the dashboard, by // default both the username and password are "admin". func (svr *Service) SetDashboardCredentials(username, password string) { // ... } // SetLogFile directs logging to the given file. By default, logging is sent to // stdout. The maxDays parameter controls how many days logs are kept before // being discarded. func (svr *Service) SetLogFile(filepath string, maxDays int64) { // ... } // SetLogLevel sets the log level. Valid levels are "trace", "debug", "info", // "warn", and "error". The default log level is "info". func (svr *Service) SetLogLevel(logLevel string) { // ... } // SetToken sets the token value used by the server. Once the token is set, // only clients that have a matching token are authorized to connect. By // default, no token is required. func (svr *Service) SetToken(token string) { // ... } // SetSubDomainHost sets the domain that will be appended to a requested // sub-domain. If the client requests the proxy be at the sub-domain "test", and // this value is "frps.com", the proxy will be available at "test.frps.com" func (svr *Service) SetSubDomainHost(host string) { // ... } // SetTCPMux enables or disables TCP multiplexing. TCP multiplexing allows for // multiple requests to share a single TCP connection. func (svr *Service) SetTCPMux(mux bool) { // ... } // SetCustom404Page sets a custom 404 page to use for HTTP requests. func (svr *Service) SetCustom404Page(filepath string) { // ... } // SetAllowedPorts sets the allowed ports that clients are allowed to bind to. // An empty set allows for all ports to be bound. func (svr *Service) SetAllowedPorts(ports map[string]struct{}) { // ... } // SetMaxPoolCount sets the maximum connection pool size. func (svr *Service) SetMaxPoolCount(poolCount int) { // ... } // SetMaxPortsPerClient sets the maximum number of ports each client can bind // to. A value of zero means no limit. func (svr *Service) SetMaxPortsPerClient(portCount int) { // ... } // SetHeartbeatTimeout sets the timeout in seconds to wait for a heartbeat // before terminating the connection. The default value is 90 seconds. func (svr *Service) SetHeartbeatTimeout(timeout int) { // ... } // SetUserConnTimeout sets the timeout in seconds for client work connections. // The default value is 10 seconds. func (svr *Service) SetUserConnTimeout(timeout int) { // ... } ``` </details> > Yes, this is easy to modify. Great! I'll send a pull request. > Now it support statik and static file path configured by assets_dir. Yes, you're right, I missed that. I guess no additional changes are needed here. > It's a hard work while it really lack of much documentation and unit tests. Writing meaningful unit testing does seem very challenging for this domain. I'm not really sure how it would be done.
Author
Owner

@fatedier commented on GitHub (Aug 16, 2019):

Server Setters Proposal

It seems too complicated.

You can first change to this one:

func NewService(cfg *config.ServerCommonConf) (*Service, error) {
    // ...
}

I will check if it should be splited by myself.

<!-- gh-comment-id:521867062 --> @fatedier commented on GitHub (Aug 16, 2019): > Server Setters Proposal It seems too complicated. You can first change to this one: ```golang func NewService(cfg *config.ServerCommonConf) (*Service, error) { // ... } ``` I will check if it should be splited by myself.
Author
Owner

@fatedier commented on GitHub (Aug 16, 2019):

Only load assets if the dashboard is enabled. This would be a good enough solution if we don't expect people who use FRP as a library to want the dashboard.

I ignored that the static files are compiled into code by statik, it will always consume some memory.

<!-- gh-comment-id:521867944 --> @fatedier commented on GitHub (Aug 16, 2019): > Only load assets if the dashboard is enabled. This would be a good enough solution if we don't expect people who use FRP as a library to want the dashboard. I ignored that the static files are compiled into code by statik, it will always consume some memory.
Author
Owner

@velovix commented on GitHub (Aug 16, 2019):

I will check if it should be splited by myself.

Okay, understood.

I ignored that the static files are compiled into code by statik, it will always consume some memory.

That's true, but since statik's auto-generated files aren't committed to the repository they won't be included when FRP is used as a Go module.

<!-- gh-comment-id:522087641 --> @velovix commented on GitHub (Aug 16, 2019): > I will check if it should be splited by myself. Okay, understood. > I ignored that the static files are compiled into code by statik, it will always consume some memory. That's true, but since statik's auto-generated files aren't committed to the repository they won't be included when FRP is used as a Go module.
Author
Owner

@velovix commented on GitHub (Aug 16, 2019):

That's true, but since statik's auto-generated files aren't committed to the repository they won't be included when FRP is used as a Go module.

Sorry, I'm completely wrong about this. The generated files are committed.

<!-- gh-comment-id:522093391 --> @velovix commented on GitHub (Aug 16, 2019): > That's true, but since statik's auto-generated files aren't committed to the repository they won't be included when FRP is used as a Go module. Sorry, I'm completely wrong about this. The generated files are committed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: github-starred/frp#1096
No description provided.