Private
Public Access
Support configurable Hyperion and HyperHDR receivers with token login
This commit is contained in:
1 parent
c3a2ac8002
commit
90c4c88942
19 files changed
+264
-34
No files matched your search
+1
-1
@@ -80,7 +80,7 @@ static bool handover_tick(void){
|
||||
media_deadline=now+3000;
|
||||
error_free(&e);next_media_probe=now+1000;
|
||||
}
|
||||
bool clear=handover_enabled && piccap_installed() && !retries.paused && !retries.blocked &&
|
||||
bool clear=handover_enabled && target_piccap_local(&target) && piccap_installed() && !retries.paused && !retries.blocked &&
|
||||
protection==PLAYBACK_CLEAR && clear_samples>=2 && now-protection_at<3000 && now>=handover_cooldown;
|
||||
if(capture_source==SOURCE_HISTOGRAM && clear){
|
||||
stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
|
||||
|
||||
+35
-14
@@ -24,6 +24,7 @@
|
||||
#include "layout.h"
|
||||
#include "autostart.h"
|
||||
#include "retry.h"
|
||||
#include "target.h"
|
||||
#define SERVICE "org.webosbrew.histogramhyperion.service"
|
||||
#define PRIORITY 149
|
||||
typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError;
|
||||
@@ -113,6 +114,18 @@ static bool read_layout(void){
|
||||
}
|
||||
bool ok=complete && layout_from_json(data,&led_blends,&led_count);free(data);interpolation_enabled=ok;return ok;
|
||||
}
|
||||
static bool target_login(void){
|
||||
if(!target.token[0])return true;
|
||||
char request[384],answer[4096];size_t used=0;
|
||||
snprintf(request,sizeof(request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token);
|
||||
if(send_line(request)!=0)return false;
|
||||
while(used+1<sizeof(answer)){
|
||||
ssize_t n=recv(output,answer+used,sizeof(answer)-used-1,0);if(n<=0)return false;
|
||||
used+=(size_t)n;answer[used]=0;if(strchr(answer,'\n'))break;
|
||||
}
|
||||
PlaybackJson json={0};if(!playback_json_init(&json))return false;
|
||||
void *root=json.parse(answer);bool ok=playback_true(&json,root,"success");if(root)json.put(root);dlclose(json.lib);return ok;
|
||||
}
|
||||
static void release_capture(bool clear_output){
|
||||
free(led_blends);led_blends=NULL;led_count=0;interpolation_enabled=false;
|
||||
color_valid=false;memset(rgb,0,sizeof(rgb));memset(sent_rgb,0,sizeof(sent_rgb));
|
||||
@@ -129,17 +142,15 @@ static bool start_failure(void *h,void *m,const char *message){
|
||||
}
|
||||
static bool start(void *h,void *m,void *ctx){
|
||||
(void)ctx;const char *p=m?payload(m):NULL;
|
||||
if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; endpoint is localhost:19444\"}");
|
||||
if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; use settings to configure the receiver\"}");
|
||||
if(m)retry_reset(&retries);
|
||||
if(piccap_owned)return respond(h,m,"{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}");
|
||||
if(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}");
|
||||
frames=0;revoked=0;last_error[0]=0;lg_diagnostic[0]=0;
|
||||
output=socket(AF_INET,SOCK_STREAM,0);
|
||||
struct timeval timeout={1,0};
|
||||
if(output>=0){setsockopt(output,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(output,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));}
|
||||
struct sockaddr_in a={.sin_family=AF_INET,.sin_port=htons(19444),.sin_addr={htonl(INADDR_LOOPBACK)}};
|
||||
if(output<0||connect(output,(void*)&a,sizeof(a))!=0){stop_capture();set_error("Hyperion JSON server is unavailable on localhost:19444");return start_failure(h,m,"Cannot connect to local Hyperion");}
|
||||
if(!read_layout()){stop_capture();set_error("Cannot read a valid LED layout from Hyperion");return start_failure(h,m,"Cannot read Hyperion LED layout");}
|
||||
output=target_connect(&target);
|
||||
if(output<0){stop_capture();set_error("Cannot connect to configured receiver IP and JSON port");return start_failure(h,m,"Configured receiver is unavailable");}
|
||||
if(!target_login()){stop_capture();set_error("Receiver token login failed; check the saved API token");return start_failure(h,m,"Receiver authentication failed");}
|
||||
if(!read_layout()){stop_capture();set_error("Cannot read receiver LED layout; check JSON port and API authorization");return start_failure(h,m,"Receiver layout unavailable; check API authorization");}
|
||||
if(!enter_developer_mode()){stop_capture();set_error("Developer initialization needs root. Elevate this own service through Homebrew Channel.");return start_failure(h,m,"Root access is required for temporary LG developer initialization");}
|
||||
capture=vt_init(policy);
|
||||
if(!leave_developer_mode()){stop_capture();retries.blocked=true;set_error("Developer marker cleanup failed; manual intervention required");return start_failure(h,m,"Developer marker cleanup failed");}
|
||||
@@ -172,10 +183,20 @@ static bool handover(void *h,void *m,void *ctx){
|
||||
handover_enabled=enabled;protection=PLAYBACK_UNKNOWN;clear_samples=0;next_media_probe=0;
|
||||
return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
static bool settings(void *h,void *m,void *ctx){
|
||||
(void)ctx;TargetConfig next=target;
|
||||
if(!playback_json.lib || !target_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Enter a valid IPv4/IPv6 address, integer port (1-65535), receiver and optional API token\"}");
|
||||
if(!target_save(TARGET_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}");
|
||||
bool active=capture || piccap_owned || retries.due;
|
||||
stop_capture();target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;
|
||||
if(active && !retries.paused && !retries.blocked)retries.due=milliseconds();
|
||||
return respond(h,m,"{\"returnValue\":true}");
|
||||
}
|
||||
static bool status(void *h,void *m,void *ctx){
|
||||
(void)ctx;char json[4096],safe[512];size_t n=0;
|
||||
(void)ctx;char json[4096],safe[512],endpoint[80];size_t n=0;
|
||||
snprintf(endpoint,sizeof(endpoint),strchr(target.host,':')?"[%s]:%u":"%s:%u",target.host,target.port);
|
||||
for(size_t i=0;last_error[i] && n+2<sizeof(safe);i++){char c=last_error[i];if(c=='"'||c=='\\')safe[n++]='\\';if((unsigned char)c>=32)safe[n++]=c;}safe[n]=0;
|
||||
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.7.2\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
|
||||
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.8.0\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,endpoint,target.host,target.port,target.receiver,target.token[0]?"true":"false",target_piccap_local(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
|
||||
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s[%u,%u,%u]",i?",":"",rgb[i][0],rgb[i][1],rgb[i][2]);
|
||||
used+=snprintf(json+used,sizeof(json)-used,"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",led_count,led_count?"true":"false");
|
||||
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]);
|
||||
@@ -235,11 +256,11 @@ static int tick(void *unused){
|
||||
}
|
||||
snprintf(line+written,capacity-written,"]}\n");
|
||||
int failed=send_line(line);free(line);
|
||||
if(failed){set_error("Hyperion connection lost");stop_capture();return 1;}
|
||||
if(failed){set_error("Receiver connection lost");stop_capture();return 1;}
|
||||
char answer[4096];size_t used=0;bool complete=false;
|
||||
while(used+1<sizeof(answer)){ssize_t len=recv(output,answer+used,sizeof(answer)-used-1,0);if(len<=0)break;used+=(size_t)len;answer[used]=0;if(strchr(answer,'\n')){complete=true;break;}}
|
||||
if(!complete){set_error("Hyperion did not acknowledge color data");stop_capture();return 1;}
|
||||
if(!strstr(answer,"\"success\":true")&&!strstr(answer,"\"success\": true")){set_error("Hyperion rejected the color command");stop_capture();return 1;}
|
||||
if(!complete){set_error("Receiver did not acknowledge color data");stop_capture();return 1;}
|
||||
if(!strstr(answer,"\"success\":true")&&!strstr(answer,"\"success\": true")){set_error("Receiver rejected the color command");stop_capture();return 1;}
|
||||
color_valid=true;frames++;return 1;
|
||||
}
|
||||
static void *symbol(void *lib,const char *name){void *s=dlsym(lib,name);if(!s){fprintf(stderr,"Missing symbol %s\n",name);exit(2);}return s;}
|
||||
@@ -251,7 +272,7 @@ int main(void){
|
||||
vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram");
|
||||
call_once=symbol(ls,"LSCallOneReply");
|
||||
cancel_call=symbol(ls,"LSCallCancel");
|
||||
if(playback_json_init(&playback_json))handover_load();
|
||||
if(playback_json_init(&playback_json)){handover_load();target_load(&playback_json,TARGET_PREF,&target);}
|
||||
reply=symbol(ls,"LSMessageReply");payload=symbol(ls,"LSMessageGetPayload");error_init=symbol(ls,"LSErrorInit");error_free=symbol(ls,"LSErrorFree");
|
||||
bool (*reg)(const char*,void**,LSError*)=symbol(ls,"LSRegister");
|
||||
bool (*category)(void*,const char*,Method*,void*,void*,LSError*)=symbol(ls,"LSRegisterCategory");
|
||||
@@ -262,7 +283,7 @@ int main(void){
|
||||
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
|
||||
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
|
||||
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
|
||||
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{NULL,NULL,0}};
|
||||
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{"settings",settings,0},{NULL,NULL,0}};
|
||||
if(!reg(SERVICE,&handle,&e)||!category(handle,"/",methods,NULL,NULL,&e)||!attach(handle,loop,&e)){fprintf(stderr,"Luna registration failed: %s\n",e.message?e.message:"unknown");error_free(&e);return 3;}
|
||||
service_handle=handle;
|
||||
if(!boot_enabled())retry_pause(&retries);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#ifndef HYPERDRM_TARGET_H
|
||||
#define HYPERDRM_TARGET_H
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
#include "playback.h"
|
||||
#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
|
||||
typedef struct {char host[46],receiver[16],token[257];unsigned port;} TargetConfig;
|
||||
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444};
|
||||
static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));}
|
||||
static bool target_valid(const TargetConfig *t){
|
||||
unsigned char address[16];
|
||||
if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false;
|
||||
if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false;
|
||||
for(const unsigned char *p=(const unsigned char*)t->token;*p;p++)
|
||||
if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='-'||*p=='_'||*p=='.'))return false;
|
||||
return true;
|
||||
}
|
||||
static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){
|
||||
void *root=j->parse(text);if(!root)return false;
|
||||
TargetConfig next=*out;bool valid=j->type(root)==4;
|
||||
const char *names[]={"host","receiver","token"};char *fields[]={next.host,next.receiver,next.token};
|
||||
size_t sizes[]={sizeof(next.host),sizeof(next.receiver),sizeof(next.token)};
|
||||
for(unsigned i=0;valid && i<3;i++){
|
||||
void *v=playback_field(j,root,names[i]);
|
||||
if(!v){if(i<2)valid=false;continue;}
|
||||
if(j->type(v)!=6){valid=false;break;}
|
||||
const char *s=j->string(v);if(strlen(s)>=sizes[i]){valid=false;break;}
|
||||
strcpy(fields[i],s);
|
||||
}
|
||||
void *port=playback_field(j,root,"port");
|
||||
if(!port || j->type(port)!=3)valid=false;
|
||||
else {char *end=NULL;errno=0;long n=strtol(j->string(port),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.port=(unsigned)n;}
|
||||
void *clear=playback_field(j,root,"clearToken");
|
||||
if(clear){if(j->type(clear)!=1)valid=false;else if(j->boolean(clear))next.token[0]=0;}
|
||||
valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid;
|
||||
}
|
||||
static bool target_save(const char *path,const TargetConfig *t){
|
||||
if(!target_valid(t))return false;
|
||||
char tmp[512],data[512];if(snprintf(tmp,sizeof(tmp),"%s.tmp",path)>=(int)sizeof(tmp))return false;
|
||||
int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->receiver,t->token);
|
||||
int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
|
||||
bool ok=write(fd,data,n)==n && fsync(fd)==0;if(close(fd)!=0)ok=false;
|
||||
if(ok)ok=rename(tmp,path)==0;
|
||||
if(!ok)unlink(tmp);
|
||||
return ok;
|
||||
}
|
||||
static bool target_load(PlaybackJson *j,const char *path,TargetConfig *t){
|
||||
int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return false;
|
||||
char data[1024];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);if(n<=0 || n==(ssize_t)sizeof(data)-1)return false;
|
||||
data[n]=0;return target_parse(j,data,t);
|
||||
}
|
||||
static int target_connect(const TargetConfig *t){
|
||||
if(!target_valid(t))return -1;
|
||||
struct sockaddr_storage storage={0};socklen_t length;int family;
|
||||
struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage;
|
||||
if(inet_pton(AF_INET,t->host,&v4->sin_addr)==1){family=AF_INET;v4->sin_family=family;v4->sin_port=htons(t->port);length=sizeof(*v4);}
|
||||
else {family=AF_INET6;v6->sin6_family=family;v6->sin6_port=htons(t->port);length=sizeof(*v6);}
|
||||
int fd=socket(family,SOCK_STREAM,0);if(fd<0)return -1;
|
||||
int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;}
|
||||
if(connect(fd,(void*)&storage,length)!=0){
|
||||
if(errno!=EINPROGRESS){close(fd);return -1;}
|
||||
struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error);
|
||||
if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;}
|
||||
}
|
||||
if(fcntl(fd,F_SETFL,flags)<0){close(fd);return -1;}
|
||||
struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
@@ -17,6 +17,8 @@ static const char *payload(void *m){return m;}
|
||||
static void error_init(LSError *e){(void)e;}
|
||||
static void error_free(LSError *e){(void)e;}
|
||||
#define PICCAP_BINARY "/bin/sh"
|
||||
static int target;
|
||||
static bool target_piccap_local(const int *t){(void)t;return true;}
|
||||
#include "handover.h"
|
||||
static bool mock_call(void *h,const char *uri,const char *data,Callback cb,void *ctx,unsigned long *token,LSError *e){
|
||||
(void)h;(void)data;(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include <assert.h>
|
||||
#include <sys/stat.h>
|
||||
#include <string.h>
|
||||
#include "target.h"
|
||||
int main(void){
|
||||
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
|
||||
assert(target_valid(&t));assert(target_piccap_local(&t));
|
||||
assert(target_parse(&j,"{\"host\":\"10.1.0.91\",\"port\":19445,\"receiver\":\"HyperHDR\",\"token\":\"test-token\"}",&t));
|
||||
assert(t.port==19445 && !strcmp(t.receiver,"HyperHDR") && !strcmp(t.token,"test-token"));assert(!target_piccap_local(&t));
|
||||
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"Hyperion\"}",&t));assert(!strcmp(t.token,"test-token"));
|
||||
const char *bad[]={"{}","[]","{\"host\":\"localhost\",\"port\":19444,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":0,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":65536,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444.5,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":\"19444\",\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"Other\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"token\":\"bad\\\"token\"}"};
|
||||
for(unsigned i=0;i<sizeof(bad)/sizeof(*bad);i++){TargetConfig before=t;assert(!target_parse(&j,bad[i],&t));assert(!memcmp(&t,&before,sizeof(t)));}
|
||||
const char *file="/tmp/hyperdrm-target-test.json";assert(access(file,F_OK)!=0);assert(access("/tmp/hyperdrm-target-test.json.tmp",F_OK)!=0);
|
||||
assert(target_save(file,&t));struct stat st;assert(stat(file,&st)==0 && (st.st_mode&0777)==0600);
|
||||
TargetConfig restored=target;assert(target_load(&j,file,&restored));assert(!strcmp(restored.host,"::1") && restored.port==65535 && !strcmp(restored.token,"test-token"));
|
||||
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"clearToken\":true}",&t));assert(!t.token[0]);assert(target_piccap_local(&t));unlink(file);
|
||||
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);struct sockaddr_in a={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
|
||||
assert(bind(server,(void*)&a,sizeof(a))==0 && listen(server,1)==0);socklen_t length=sizeof(a);assert(getsockname(server,(void*)&a,&length)==0);t.port=ntohs(a.sin_port);
|
||||
int client=target_connect(&t);assert(client>=0);int accepted=accept(server,NULL,NULL);assert(accepted>=0);close(accepted);close(client);close(server);
|
||||
assert(target_connect(&t)<0);
|
||||
puts("PASS: target validation, token retention/removal, private persistence, IPv6 parsing, bounded TCP connect and refusal");return 0;
|
||||
}
|
||||
Reference in new issue
Block a user